For agencies and integrators
Send leads into an operator's pipeline
One endpoint, one key. Post a lead and it becomes a customer and a new deal in that operator's account, with the campaign that produced it attached. Everything below is what the endpoint actually does today.
The endpoint
POST https://landworkpro.com/api/public/leads Content-Type: application/json Authorization: Bearer YOUR_KEY
Either spelling works. /api/public/leads and the same path with a trailing slash both answer terminally, so a redirect can never be counted as a failed delivery by your workflow.
Your key
Send it either way. Both are accepted, because some workflow builders cannot set an Authorization header at all.
Authorization: Bearer YOUR_KEY X-API-Key: YOUR_KEY
The key identifies the operator. A payload cannot name its own destination: the account a lead lands in is resolved from the key and never from the body, so a key can only ever write into the one account it belongs to. Keys are stored as a hash, never as plaintext.
The body
Send a phone or an email. Everything else is optional, and each field accepts a few spellings so you can usually map straight through from your form.
| Field | Also accepted as |
|---|---|
| name | full_name, fullName |
| phone | phone_number, phoneNumber |
| email_address | |
| address | address1, full_address |
| notes | message, comments |
Attribution, which is the part worth wiring
Send the campaign data with the lead and the operator sees which campaign produced the job, not just the lead. Each of these also accepts an attribution_ prefix, so attribution_utm_source works as well as utm_source.
utm_source utm_medium utm_campaign utm_content utm_term gclid fbc fbp
Mapping from GoHighLevel: these line up with its own attributionSource object. One gap worth knowing: fbclid is not accepted today. GoHighLevel carries it natively, so if you map its attribution object straight through, that one field is dropped. Send fbc where you have it.
Retries, and not creating the lead twice
Send an Idempotency-Key header, or an event_id in the body, that is stable across retries of the same lead. The header wins if you send both. This is required, and it is what makes a retry safe.
It matters more than it looks. GoHighLevel counts any non 2xx response as a failed delivery and retries up to twelve times, so a single lead can arrive thirteen times. With a stable key, those thirteen deliveries create one customer and one deal. Without one, the request is refused outright rather than risking thirteen.
When it refuses
Every refusal returns a fixed, machine readable reason beside a sentence for a human. Branch your workflow on the reason, never on the message.
{ "ok": false, "reason": "invalid_api_key", "message": "..." }| reason | Status | What to do |
|---|---|---|
| missing_api_key | 401 | No key was sent. Check the header name and that your workflow is not stripping it. |
| invalid_api_key | 401 | The key is wrong or has been revoked. Ask the operator for a current one. |
| org_not_eligible | 403 | The destination account is closed to inbound leads. This is not something you can fix from your side. Tell the operator. |
| missing_idempotency_key | 400 | Send an Idempotency-Key header, or an event_id in the body, that stays the same across retries of the same lead. |
| no_contact_method | 422 | Send a phone or an email. A lead with neither cannot be contacted, so it is refused. |
| malformed_payload | 400 | The body was not readable JSON, or a field was the wrong shape. Fix and resend. |
| rate_limited | 429 | More than 120 leads in a minute for this account. Back off and retry. |
| lead_unresolved | 503 | We could not record it and we are not pretending we did. Transient by construction: retry with the same idempotency key. |
Nothing here answers 2xx or 3xx. A refusal is always a real error status, so your workflow's own retry logic sees it as one.
Rate limit
120 leads per account per minute, then rate_limited with a 429. It is deliberately generous because GoHighLevel bursts on retry, and a limit that trips during a legitimate retry storm turns a recoverable delivery into a lost lead.
What it can and cannot do
- It can create a customer and a new deal inside the one account the key belongs to.
- It cannot create an account, create a user, or reach any other operator's data.
- The destination comes from the key. Not from the body, not from a header, not from anything the sender controls.
A complete request
curl -X POST https://landworkpro.com/api/public/leads \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ghl-contact-7f3a91" \
-d '{
"name": "Dale Whitfield",
"phone": "615 555 0142",
"email": "dale@example.com",
"address": "1200 County Line Rd, Franklin TN",
"notes": "12 acres, wants it cleared before spring",
"utm_source": "google",
"utm_campaign": "land-clearing-middle-tn",
"gclid": "Cj0KCQjw..."
}'Getting a key
Keys are issued by us, one per operator account. There is no self serve screen: ask the operator to request one, or contact us directly and we will issue it and wire the first delivery with you.
That is deliberate while this is new. We would rather talk to every agency sending leads than hand out credentials and find out later how they were used.
Something here not matching what you are seeing? Tell us. This page is generated from the endpoint's own contract, so if it is wrong we want to know before you do.