For developers

Every way a lead gets in.

Someone has asked you to put Bludge on their website. That is one line of HTML and you are done. The rest of this page is the detail underneath it: the endpoints, the payloads they answer with, and what happens in the seconds after an enquiry lands.

Three ways a lead gets in

Whichever one you pick, it ends the same way: a lead on the account, and a call scheduled for right now.

A form on the websiteTwo lines of code, no stylesheet and no library. The embed.
The CRM they already useActiveCampaign, HubSpot or Zoho, connected from the admin. What each one sends.
A spreadsheetA CSV upload in the admin. Anything older than 90 days is skipped, and it says how many.

There is a fourth way that needs you rather than them: post the lead yourself, as JSON, with a key. That is the webhook.

And a fifth that needs no code at all: somebody phones. When the owner buys a number in the admin, that number's voice, status and messaging webhooks are set to point at Bludge as part of the purchase — there is no console to visit and nothing to paste, and the same settings can be re-applied from the list of numbers in the admin if they ever drift. Calls to it are answered by the same assistant, which says it is an AI in its first sentence, and the call becomes a lead: joined to an existing one if the number matches, created as a new one if it does not. To use a number that is already published, forward it to the Bludge number at your own carrier; the forwarding number arrives with the call and is stored on it.

The form on the website

Paste this where the form should appear, just before the closing body tag. The div is where it draws; the script is loaded async and needs nothing else.

<div id="bludge-form-YOUR-FORM-ID"></div>
<script src="https://bludge.ai/forms/embed.js" data-form-id="YOUR-FORM-ID" async></script>

Where the form id comes from. The owner finds it in the admin under Lead sources, on the form's Embed code button. That page has the snippet with the real id already in it, a copy button, and a field that emails the whole thing to you.

If you would rather not touch the site at all, every form is also a page we host, at https://bludge.ai/forms/:uuid. Link it from a button and nothing needs installing.

The script does four things, in this order:

  1. POST /forms/:uuid/track_view, with a session id it keeps in localStorage.
  2. GET /forms/:uuid/config, for the fields and the styling.
  3. Draws the fields into your div.
  4. POST /forms/:uuid/submit when someone sends it.

You can skip the script and call those three endpoints from your own form. They are below.

The form endpoints

JSON in, JSON out. No key: the form's uuid is the address. They answer Access-Control-Allow-Origin: *, so you can call them from the browser on any domain. A uuid that is unknown, or a form that has been switched off, gets 404.

GET/forms/:uuid/config

The fields to draw, the styling set in the admin, and the message to show after a send.

{
  "success": true,
  "data": {
    "id": "3f8c1a7e-…",
    "fields": [
      { "name": "name",  "label": "Name",  "type": "text",  "required": true },
      { "name": "email", "label": "Email", "type": "email", "required": true },
      { "name": "phone", "label": "Phone", "type": "tel",   "required": true }
    ],
    "styling": { "…": "colours and labels, set in the admin" },
    "success_message": "Thanks! We'll call you shortly."
  }
}

fields is name, email and phone, plus company and message when the owner has switched them on, plus any custom fields they have added. Custom fields carry their own type, and a checkbox is sent back as an array.

POST/forms/:uuid/track_view

Counts one view. Send your own session_id to tie a view to a later submission, or leave it out and one is made for you and returned.

{ "session_id": "9b1c…" }

{
  "success": true,
  "data": { "session_id": "9b1c…" },
  "message": "View tracked successfully"
}

POST/forms/:uuid/submit

Creates the lead. name and phone are required; everything else is whatever the form asks for. The five utm_* values and landing_page are optional and stored with the submission. The referrer, the IP address and the user agent are taken from the request itself.

{
  "name": "Jess Turner",
  "email": "jess@example.com",
  "phone": "0412 345 678",
  "message": "After a quote for a battery.",
  "utm_source": "google",
  "utm_medium": "cpc",
  "utm_campaign": "batteries-spring",
  "landing_page": "https://example.com/batteries"
}

A lead was made, and it is already being called:

{
  "success": true,
  "data": {
    "lead_id": 4821,
    "message": "Thanks! We'll call you shortly."
  },
  "message": "Form submitted successfully"
}

Something was missing, 422:

{
  "success": false,
  "error": {
    "message": "Validation failed",
    "code": "unprocessable_entity",
    "details": { "validation_errors": ["Phone is required"] }
  }
}

Show data.message on success and the strings in validation_errors on failure, and you have covered every case the endpoint has.

Posting a lead in yourself

If the leads are somewhere else — a custom site, an internal tool, a job board — post them as JSON with a key in the X-API-Key header.

POST/webhooks/active_campaign

The path is named after the first system that used it. It takes the same body from anything.

{
  "contact": {
    "name": "Jess Turner",
    "email": "jess@example.com",
    "phone": "+61412345678",
    "state": "NSW"
  },
  "source": "Website Form"
}

name can be sent instead as firstName and lastName. state is worth sending: it decides which time zone the calling hours are read in. source is free text and shows up on the lead in the admin.

curl -X POST https://bludge.ai/webhooks/active_campaign \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{"contact":{"name":"Test Lead","phone":"+61412345678","state":"NSW"},"source":"Test"}'
  • 200 OK — the lead was taken and queued for a call.
  • 401 Unauthorized — the X-API-Key header is missing, wrong, or belongs to an account that is switched off.
  • 422 Unprocessable Entity — the JSON is malformed or the contact object is not there.

Where the URL and the key come from. In the admin: Assistants, the assistant, the For developers tab, then Show the technical details. Both have copy buttons. The switch above them, "Accept leads posted to our webhook", has to be on — with it off the request is still answered 200 and nothing is called. Making a new key stops the old one working immediately.

Three things happen quietly and are worth knowing about:

  • A lead we have already seen recently — the same phone number or the same email address, inside the account's duplicate window — is merged into the original rather than called twice.
  • Over the account's daily call limit, the lead is kept and no call is scheduled.
  • An enquiry older than 90 days is not called automatically. That is a calling rule, not a setting.

If the leads are already in a CRM

Three are connected from the admin, on the assistant's Lead sources tab. HubSpot is a Connect button; ActiveCampaign takes an account address and a key; Zoho still takes its credentials by hand. You do not have to build any of that. What follows is only what each one sends us, in case you are debugging why a contact did not turn into a call.

  • HubSpot posts to /webhooks/hubspot on contact.creation, contact.propertyChange and form.submission. We read firstname, lastname, email, phone or mobilephone, state, and the object id.
  • Zoho posts to /webhooks/zoho for the Leads module. We read First_Name, Last_Name, Email, Phone or Mobile, Lead_Source, State and the record id.
  • ActiveCampaign uses the same endpoint and the same body as the webhook above.

Both CRM endpoints are keyed the same way, with X-API-Key, and the key has to belong to an assistant whose CRM is that system. Contacts are matched on email address, so an existing contact is updated rather than duplicated. A contact with no phone number is stored and not called.

Getting the result back out

Switch on "POST the result of a qualified call to a URL" on the same For developers tab, give it an HTTPS endpoint, and every qualified call arrives at it.

{
  "event": "lead_qualified",
  "timestamp": "2026-02-11T09:14:07+11:00",
  "assistant": { "id": 123, "name": "Front desk" },
  "lead": {
    "id": 4821,
    "name": "Jess Turner",
    "email": "jess@example.com",
    "phone": "+61412345678",
    "status": "contacted",
    "outcome": "hot",
    "source": "Website Form",
    "state": "NSW"
  },
  "call": { "duration_seconds": 120, "outcome": "qualified", "summary": "…" }
}

Your endpoint has to answer 2xx, answer inside ten seconds, and expect up to three retries with a widening gap if it does not.

What happens in the next five seconds

  1. The lead is created and a call is scheduled for now. Nothing waits on a cron tick.
  2. The queue checks the rules: the calling window in the lead's own time zone, the 90-day consent age, and the do-not-call list.
  3. If the window is shut, the call waits until it opens. It is not dropped and it is not made early.
  4. The call goes out. It says it is an AI in the first sentence, every time, in every voice.
  5. Nobody home: one more try about an hour later, inside the window, then a text from the business's own number.
  6. The outcome, the transcript and the booking are written back — to the admin, to the CRM if one is connected, and to your endpoint if you have set one.

A "stop" by text opts that number out of calls and messages across the whole account, whatever posted it in.

What gets stored

A form submission keeps what you would expect and nothing you would not: the field values, the utm_* values and landing page if you sent them, the referrer, the IP address and the user agent. Calls keep a transcript, and no audio: the recording is not stored. Webhook bodies are kept as they arrived, so a failed one can be read back.

The two pages that answer the rest of it are Privacy and Calling rules. If you are the one signing off on this, those are the two to read.

Is there a proper API?

Not yet, and we would rather say so than publish a stub. A public API is planned, after the first pilots are running; what is on this page is the whole of it today. If you need something it does not cover, tell us what — that is how the list gets written.