# No API key: the token in the URL identifies the source.
# Sign the exact bytes you send, with the source's signing secret.
BODY='{"topic": "order/created", "customer": {"phone": "+14155551234", "first_name": "Sam"}}'
T=$(date +%s)
SIG=$(printf '%s' "$T.$BODY" | openssl dgst -sha256 -hmac "$IBUBBL_EVENT_SECRET" | awk '{print $NF}')
curl -X POST "https://api.ibubbl.com/v1/events/$IBUBBL_EVENT_SOURCE_TOKEN" \
-H "Content-Type: application/json" \
-H "X-iBubbl-Signature: t=$T,v1=$SIG" \
--data-raw "$BODY"
import { createHmac } from "node:crypto";
// No API key: the token in the URL identifies the source.
const token = process.env.IBUBBL_EVENT_SOURCE_TOKEN;
const secret = process.env.IBUBBL_EVENT_SECRET ?? "";
// Serialize once, then sign and send that same string.
const body = JSON.stringify({
topic: "order/created",
customer: { phone: "+14155551234", first_name: "Sam" },
});
const t = Math.floor(Date.now() / 1000);
const v1 = createHmac("sha256", secret).update(`${t}.${body}`).digest("hex");
const res = await fetch(`https://api.ibubbl.com/v1/events/${token}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-iBubbl-Signature": `t=${t},v1=${v1}`,
},
body,
signal: AbortSignal.timeout(10_000),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.detail?.error}`);
console.log(data.runs_started);
import hashlib
import hmac
import json
import os
import time
import requests
# No API key: the token in the URL identifies the source.
TOKEN = os.environ["IBUBBL_EVENT_SOURCE_TOKEN"]
SECRET = os.environ["IBUBBL_EVENT_SECRET"]
# Serialize once, then sign and send those same bytes.
body = json.dumps({
"topic": "order/created",
"customer": {"phone": "+14155551234", "first_name": "Sam"},
}).encode()
t = str(int(time.time()))
v1 = hmac.new(SECRET.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
res = requests.post(
f"https://api.ibubbl.com/v1/events/{TOKEN}",
headers={"Content-Type": "application/json", "X-iBubbl-Signature": f"t={t},v1={v1}"},
data=body,
timeout=10,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
print(data["runs_started"])
{
"received": true,
"runs_started": 1
}
{
"detail": {
"error": "invalid_signature",
"message": "Signature verification failed."
}
}
Receive an event
Receive a webhook from a connected third-party app (for example Recharge) and start any workflow whose Webhook received trigger matches it.
The URL you paste into the sending app carries its own credential, so this endpoint takes no Authorization header. Create a source in Settings → Inbound events to get the full URL; the token is shown once.
Any app (secret URL) sources need no signature: the URL is the credential, so keep it private. They accept JSON or application/x-www-form-urlencoded bodies and at most 60 requests a minute. Beyond that the answer is 429 while a workflow is listening on the source; with none listening, the event is accepted and dropped.
Signed sources must sign every request. For Recharge, that is the X-Recharge-Hmac-Sha256 header computed with your API Client Secret — which is not the same value as your API token. An unsigned or mis-signed request returns 403 invalid_signature.
Redeliveries are safe. Providers resend events, and Recharge’s dunning fires one event per charge retry. Each workflow’s event_id_path identifies the underlying object, so repeat deliveries of the same event fold onto the run that already exists rather than messaging the customer again.
A 200 means the event was accepted, not that it matched anything — check runs_started, and see the source’s status in Settings when it is unexpectedly 0.
# No API key: the token in the URL identifies the source.
# Sign the exact bytes you send, with the source's signing secret.
BODY='{"topic": "order/created", "customer": {"phone": "+14155551234", "first_name": "Sam"}}'
T=$(date +%s)
SIG=$(printf '%s' "$T.$BODY" | openssl dgst -sha256 -hmac "$IBUBBL_EVENT_SECRET" | awk '{print $NF}')
curl -X POST "https://api.ibubbl.com/v1/events/$IBUBBL_EVENT_SOURCE_TOKEN" \
-H "Content-Type: application/json" \
-H "X-iBubbl-Signature: t=$T,v1=$SIG" \
--data-raw "$BODY"
import { createHmac } from "node:crypto";
// No API key: the token in the URL identifies the source.
const token = process.env.IBUBBL_EVENT_SOURCE_TOKEN;
const secret = process.env.IBUBBL_EVENT_SECRET ?? "";
// Serialize once, then sign and send that same string.
const body = JSON.stringify({
topic: "order/created",
customer: { phone: "+14155551234", first_name: "Sam" },
});
const t = Math.floor(Date.now() / 1000);
const v1 = createHmac("sha256", secret).update(`${t}.${body}`).digest("hex");
const res = await fetch(`https://api.ibubbl.com/v1/events/${token}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-iBubbl-Signature": `t=${t},v1=${v1}`,
},
body,
signal: AbortSignal.timeout(10_000),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.detail?.error}`);
console.log(data.runs_started);
import hashlib
import hmac
import json
import os
import time
import requests
# No API key: the token in the URL identifies the source.
TOKEN = os.environ["IBUBBL_EVENT_SOURCE_TOKEN"]
SECRET = os.environ["IBUBBL_EVENT_SECRET"]
# Serialize once, then sign and send those same bytes.
body = json.dumps({
"topic": "order/created",
"customer": {"phone": "+14155551234", "first_name": "Sam"},
}).encode()
t = str(int(time.time()))
v1 = hmac.new(SECRET.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
res = requests.post(
f"https://api.ibubbl.com/v1/events/{TOKEN}",
headers={"Content-Type": "application/json", "X-iBubbl-Signature": f"t={t},v1={v1}"},
data=body,
timeout=10,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
print(data["runs_started"])
{
"received": true,
"runs_started": 1
}
{
"detail": {
"error": "invalid_signature",
"message": "Signature verification failed."
}
}
Receive an event
Method:POST | Endpoint: /v1/events/{token}
Description: The URL a connected app (Recharge, your own system, or a no-code tool such as Zapier or Make) posts its events to. An event that matches an active workflow’s “Webhook received” trigger starts a run. See Inbound events for setup.
Authentication: No API key. The token in the path (ibev_ followed by 64 hex characters, copied from the event source in the dashboard) identifies the source. A signed source must also send its signature header: Recharge sends X-Recharge-Hmac-Sha256; a generic signed source sends X-iBubbl-Signature: t=<unix seconds>,v1=<hex>, an HMAC-SHA256 of {t}.{raw body} with the source’s signing secret, no more than 5 minutes old. A secret-URL source sends no signature and is limited to 60 requests a minute.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| (the app’s event) | object | Yes | The connected app’s own payload, sent unchanged as JSON (or form-encoded), at most 256 KB. iBubbl doesn’t require any fields; your workflow maps the ones it needs. The signature is checked against these exact bytes, so don’t re-serialize the body. |
# No API key: the token in the URL identifies the source.
# Sign the exact bytes you send, with the source's signing secret.
BODY='{"topic": "order/created", "customer": {"phone": "+14155551234", "first_name": "Sam"}}'
T=$(date +%s)
SIG=$(printf '%s' "$T.$BODY" | openssl dgst -sha256 -hmac "$IBUBBL_EVENT_SECRET" | awk '{print $NF}')
curl -X POST "https://api.ibubbl.com/v1/events/$IBUBBL_EVENT_SOURCE_TOKEN" \
-H "Content-Type: application/json" \
-H "X-iBubbl-Signature: t=$T,v1=$SIG" \
--data-raw "$BODY"
import { createHmac } from "node:crypto";
// No API key: the token in the URL identifies the source.
const token = process.env.IBUBBL_EVENT_SOURCE_TOKEN;
const secret = process.env.IBUBBL_EVENT_SECRET ?? "";
// Serialize once, then sign and send that same string.
const body = JSON.stringify({
topic: "order/created",
customer: { phone: "+14155551234", first_name: "Sam" },
});
const t = Math.floor(Date.now() / 1000);
const v1 = createHmac("sha256", secret).update(`${t}.${body}`).digest("hex");
const res = await fetch(`https://api.ibubbl.com/v1/events/${token}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-iBubbl-Signature": `t=${t},v1=${v1}`,
},
body,
signal: AbortSignal.timeout(10_000),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.detail?.error}`);
console.log(data.runs_started);
import hashlib
import hmac
import json
import os
import time
import requests
# No API key: the token in the URL identifies the source.
TOKEN = os.environ["IBUBBL_EVENT_SOURCE_TOKEN"]
SECRET = os.environ["IBUBBL_EVENT_SECRET"]
# Serialize once, then sign and send those same bytes.
body = json.dumps({
"topic": "order/created",
"customer": {"phone": "+14155551234", "first_name": "Sam"},
}).encode()
t = str(int(time.time()))
v1 = hmac.new(SECRET.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
res = requests.post(
f"https://api.ibubbl.com/v1/events/{TOKEN}",
headers={"Content-Type": "application/json", "X-iBubbl-Signature": f"t={t},v1={v1}"},
data=body,
timeout=10,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
print(data["runs_started"])
{
"received": true,
"runs_started": 1
}
{
"detail": {
"error": "invalid_signature",
"message": "Signature verification failed."
}
}
Responses
200 OK{
"received": true,
"runs_started": 1
}
runs_started: 0), so the sending app never retries or disables the hook. The reason an event started nothing is recorded on the event source.
403 Forbidden
{
"detail": {
"error": "invalid_signature",
"message": "Signature verification failed."
}
}
{
"detail": {
"error": "not_found",
"message": "No such event source."
}
}
{
"detail": {
"error": "payload_too_large",
"message": "Request body too large."
}
}
{
"detail": {
"error": "rate_limited",
"message": "Too many events for this source; try again in a minute."
}
}
Path Parameters
Response
Event accepted.
Always true once the signature has been verified.
true
How many workflow runs this event started. 0 is a normal answer — it means no active workflow's trigger matched, or this event was a redelivery that de-duplicated onto an existing run.
1

