Lead sources quick-start
An inbound source (a lead source in the dashboard) is a place leads come from: a form on your website, a Lead URL your own server, CRM automations, Zapier or Make post leads to, or a Google Ads lead form. Each lead becomes (or updates) a contact, with its UTM parameters, click ids and the consent wording it was shown. A source can add labels, set an owner and enrol each lead in a workflow.
Create sources with POST /v1/inbound-sources. Creating or changing a source needs a live API
key or a signed-in member; test keys can only read. Creating one also needs
"consent_attested": true: you confirm that everyone you send to bubbl3 has agreed to receive
texts from you. bubbl3 records who confirmed it and when; a create without it is refused (400).
| Kind | How bubbl3 knows the lead is yours |
|---|---|
form | The source's public token, posted from one of its allowed origins |
webhook (Lead URL in the dashboard) | An HMAC signature with the source's secret (or a public token, like a form) |
google_ads_lead_form | The key you set in Google Ads |
Every lead is posted to POST https://api.bubbl3.com/v1/inbound/leads/<source id>. A lead needs
a phone number. Anything bubbl3 can't accept for that source (an unknown source, a bad signature,
token or origin, a replay, a paused source) gets the same 403 with the message
Invalid lead request, so the answer never says which part was wrong.
1. A form on your website
Create a form source and list the sites it's on in allowed_origins, for example
https://www.example.com. The source's public_token goes in the snippet you paste:
<script src="https://api.bubbl3.com/embed/v1/lead.js" async></script>
<form data-bubbl3-source="src_..." data-bubbl3-token="b3p_..." data-bubbl3-api="https://api.bubbl3.com"
data-bubbl3-consent="By submitting, you agree to receive texts from Example Co. Reply STOP to opt out.">
<input name="first_name" autocomplete="given-name">
<input name="phone" type="tel" autocomplete="tel" required>
<input name="email" type="email" autocomplete="email">
<button type="submit">Send</button>
</form>
The public token isn't a secret. Anyone can read it in your page. The allowed-origins check stops browsers on other sites from posting with it, but a script can send any origin it likes. So bubbl3 limits what a form lead can do:
- a form lead for someone already in your contacts only records the lead and adds the source's labels. It doesn't change their consent or any opt-out, doesn't fill in or change their details, and doesn't start the source's workflow again;
- only the source's own labels are added.
tagsorlabelsfields in a form post are ignored; - a form source adds at most 500 new contacts a UTC day. Once it has, every lead from it gets the
same
403until the next day, including leads for people already in your contacts, and bubbl3's team is alerted.
So if someone already in your contacts fills in the form again, you see the new lead on their contact, but no workflow restarts for them; follow up by hand.
If a form starts getting junk leads, rotate its token (Rotate token on the source in
Settings, Integrations, Lead sources, or POST /v1/inbound-sources/<id>/rotate-secret). The old token
stops working at once, so paste the new snippet on your site straight away. For leads from your
own server, use a signed Lead URL (webhook) source instead: it has no cap.
The script:
- keeps UTM parameters and click ids (
gclid,fbclidand others) from the page address in a first-party cookie for 90 days, so a visitor who browses first is still attributed; - shows the consent wording above the button, and sends it with the lead as evidence;
- sets
data-bubbl3-stateon the form tosending,sentorfailed, and firesbubbl3:sentorbubbl3:failedon it, so your page can show its own message.
Field names are matched to contact fields (first_name, last_name, phone, email and common
variants such as phone_number or mobile_phone). Set field_aliases on the source for others,
for example { "Mobile Number": "phone" }.
Content security policy
If your site sends a Content-Security-Policy header, allow the script and the post to the bubbl3
API. Add the API origin to these two directives (keep what you already have in them):
script-src 'self' https://api.bubbl3.com;
connect-src 'self' https://api.bubbl3.com;
The script is served from a versioned path and cached for a day, so fixes reach your page without
any change on your side. A change that would break your page gets a new path
(/embed/v2/lead.js); /embed/v1/lead.js keeps working.
2. A Lead URL for your own server
Create a webhook source (a Lead URL in the dashboard): a signed address your server, CRM
automations, Zapier or Make post leads to. The answer includes secret once: store it with your other secrets.
Sign each request with HMAC-SHA256 over <unix seconds>.<raw body> and send it in the
bubbl3-signature header as t=<unix seconds>,v1=<hex digest>. The timestamp must be within five
minutes of the current time.
import { createHmac } from 'node:crypto';
const body = JSON.stringify({ event_id: 'order-1042', phone: '+19175550142', first_name: 'Ada' });
const t = Math.floor(Date.now() / 1000);
const v1 = createHmac('sha256', process.env.BUBBL3_SOURCE_SECRET).update(`${t}.${body}`).digest('hex');
await fetch('https://api.bubbl3.com/v1/inbound/leads/src_...', {
method: 'POST',
headers: { 'content-type': 'application/json', 'bubbl3-signature': `t=${t},v1=${v1}` },
body,
});
A lead bubbl3 already has gets 200 {"received": true} again, so retries are safe. Send a stable
event_id in the body (or an Idempotency-Key header) to make that explicit; without one, the
same body twice counts as one lead. Bodies can be JSON or form-encoded, up to 64 KB.
3. Google Ads lead forms
Create a google_ads_lead_form source. In Google Ads, open the lead form's Lead delivery
settings, paste the source's intake URL as the webhook URL, and paste the source's secret as the
key. Use Send test data to check it: bubbl3 accepts test leads but doesn't create contacts
from them.
Consent
A source's consent_mode records what its leads agreed to. Until it is set (by a member with full
access to compliance), leads arrive with consent recorded as unknown. Because you confirmed when
you created the source that everyone you send has agreed to receive texts, they are saved,
enrolled in the source's workflow and texted, unless your workspace only messages contacts who
opted in.
Opt-outs always win: a lead whose number has opted out is never texted, whatever the source says.
With consent_mode: "crm_field", a no in the consent field (no, declined, unsubscribed,
opted_out, or a false checkbox) opts the lead out; a missing or unrecognised answer leaves
consent unknown. On a website form (a public-token source), a lead whose consent field says no is
simply not created: nothing is changed and nobody is opted out, and the form answers as usual.