Logo

What are you looking for?

Help guide and support information for Leadferno's apps.

Subscribe to webhook events

Leadferno API

Subscribe to webhook events

Leadferno delivers events to your application to enable realtime messaging.

Last updated on 09 Sept, 2026

Leadferno delivers events to your endpoint as HTTP POSTs. You manage subscriptions with the /v0/zapier/* endpoints — despite the name, they are general-purpose and not tied to Zapier.

Subscribe

JavaScriptPOST /v0/zapier/subscribe
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "profile": "<profile-uuid>",
  "url": "https://yourapp.example.com/incoming/leadferno",
  "trigger": "new_message"
}
  • profile and url are required. trigger is validated — an unknown or missing trigger returns 422 "Invalid trigger".

  • Requires staff scope.

  • The response is { "uuid": "<subscription-uuid>" }. Store that UUID — it's the only handle you have to unsubscribe later.

Available triggers

Trigger

Fires when

new_message

A message is sent or received on a thread — inbound and outbound (see Receive and resolve webhook deliveries)

new_lead

An inbound message opens a new thread

new_leadform_lead

A lead arrives through a web lead form

new_contact

A new contact is created

thread_new_tag

A tag is added to a thread

updated_lead_status

A thread's status changes to a specific value (see below)

updated_lead_status needs a target status

JSON{
  "profile": "<profile-uuid>",
  "url": "https://yourapp.example.com/incoming/leadferno",
  "trigger": "updated_lead_status",
  "metadata": { "state": "closed_won" }
}

The event fires only when a thread changes to exactly that status. Valid values: new, open, closed_won, closed_lost, closed_other, inactive. Subscribe once per status you care about.

One subscription per (profile, trigger)

Duplicates are not rejected, and each duplicate produces a duplicate delivery. Keep one subscription per trigger and de-duplicate on your side anyway.

Unsubscribe

JavaScriptDELETE /v0/zapier/unsubscribe/{subscription_uuid}

Store subscription UUIDs on the connection record so that when a customer disconnects your integration, you actually stop the event flow.

See also: Receive and resolve webhook deliveries


Receive and resolve webhook deliveries

Delivery format

  • Method: POST, content type application/json.

  • No authentication header and no signature. Treat every inbound POST as unverified.

Hardening while there's no signature

  • Put a high-entropy secret token in the webhook URL path or query string you register.

  • Validate that every UUID in the payload is a well-formed v4 UUID before using it.

  • For the reference-style payloads, treat the webhook as a trigger to fetch — the authenticated API call you make next is the trustworthy part.

  • Make your handler idempotent, keyed on the payload UUIDs, so a duplicate delivery is a no-op.

new_message fires for messages you send, too

new_message fires whenever a message is delivered on a thread — including messages your own integration sends through the API. When you resolve the message, check direction: egress is outbound (likely yours), ingress is from the contact. Keeping your handler idempotent on the message UUID prevents a send/receive loop. Draft and scheduled messages don't fire it; they fire when actually sent.

Payload shapes

Some payloads are thin references you resolve with an API call; two carry data inline.

JSON// new_message  — reference
{ "thread": "<uuid>", "message": "<uuid>", "profile": "<uuid>" }

// new_lead  — reference, same shape as new_message
{ "thread": "<uuid>", "message": "<uuid>", "profile": "<uuid>" }

// new_leadform_lead  — reference; "leadform_fields" is a message UUID
{ "thread": "<uuid>", "leadform_fields": "<uuid>", "profile": "<uuid>" }

// updated_lead_status  — reference
{ "thread": "<uuid>", "profile": "<uuid>" }

// thread_new_tag  — inline. Note the thread key is "uuid", not "thread".
{
  "uuid": "<thread-uuid>",
  "profile": "<profile-uuid>",
  "tag": { "uuid": "<tag-uuid>", "label": "VIP", "color": "#RRGGBB" }
}

// new_contact  — profile is a reference; the new contact is inline
{
  "profile": "<profile-uuid>",
  "prospect": {
    "uuid": "<uuid>",
    "first_name": "...", "last_name": "...",
    "email": "...", "cellphone": "15551234567",
    "modification": "<ISO-8601>",
    "creation": "<ISO-8601>",
    "metadata": [ { "uuid": "...", "value": "...", "type": "...", "label": "..." } ]
  }
}

Even for the inline payloads, fetch the canonical record (below) rather than mapping the webhook fields directly — the inline shapes differ from the REST responses (raw field names, cellphone without a leading +).

Response codes

You return

Leadferno does

2xx

Nothing further — delivery succeeded

410 Gone

Treats the endpoint as permanently gone and auto-unsubscribes that trigger

Anything else

Logs it. The subscription stays active. No retry.

There is no retry. A failed delivery is a lost event. So:

  1. Acknowledge fast, process asynchronously — return 2xx as soon as you've durably queued the payload, then do resolution and CRM writes.

  2. Monitor your endpoint's error rate, and run a periodic reconciliation poll of recent threads so a delivery gap doesn't become permanent data loss.

Only return 410 when you actually want the subscription cancelled.

Resolve an event into a full record

JavaScriptGET /v0/profiles/{profile_uuid}/threads/{thread_uuid}
Authorization: Bearer <access_token>

Returns the thread with:

  • uuid, state, read, spam, isCallRequest, creation, modification

  • a nested prospectfirst_name, last_name, cellphone (as +<digits> or ""), email (omitted entirely when unset), consent, blocked, metadata

  • assignee and tags when present

  • messagesthe 10 most recent only, newest first

For full conversation history, page through:

JavaScriptGET /v0/profiles/{profile_uuid}/threads/{thread_uuid}/messages?offset=0&limit=50

which also accepts state and type filters.

Determining the channel / lead source

  • There is no source field on a contact — you can't read "web form vs. Facebook vs. SMS" off the prospect.

  • Use the message type (sms, fb, leadform_fields, call_request, …) as the channel, and direction (ingress / egress) for inbound vs. outbound.

  • A message's source field is free text, set only for messages that originate in a Leadbox, lead form, or web widget. It is not set for inbound SMS or Facebook, so don't rely on it as a channel indicator.

Did you find this article helpful?
Previous

Send messages via the API

Next