Receive events from your app

Two doors, different authorization and contact behavior:

  • POST /api/integrations/inbound/{id}/{token} is not an API-key endpoint. Enable incoming events on a Connected app and copy its private callback URL into BLUETICKED_CALLBACK_URL. The connection fixes organization and business scope; never send those to select another workspace.
  • POST /api/v1/events/send uses an API key or OAuth access with events:send. events/send never creates a contact. Create the contact first using POST /api/v1/contacts with contacts:write, then send its contact_id or matching phone/email in the same business.
  • Callback ingestion can create or update a contact from the mapped payload. Receiving an event does not grant consent. Never invent opt-ins; suppression and channel send rules still apply.

Send a callback

The URL contains a credential. Do not print it, include it in screenshots, or commit it. Rotating the callback token requires updating the sender. The request body limit is 128,000 bytes; larger bodies return 413. Use application/json for nested source data or application/x-www-form-urlencoded for flat string fields, with the owner's field mapping. The callback reads the first nonempty event-type value from event, event_type, type, status, in that order. Without one it uses callback.received. Example body (use your connection and flow's event/field names):

json
{
  "event": "appointment.created",
  "id": "booking-1234",
  "customer": {
    "phone": "+27820000001",
    "name": "Carol"
  },
  "appointment": {
    "service": "Oxygen assessment",
    "slot": "Tuesday 09:30",
    "branch": "Hillcrest"
  }
}

The same event as a form post, its fields flat strings (sign the raw form body the same way when signing is on):

http
Content-Type: application/x-www-form-urlencoded

event=appointment.created&id=booking-1235&phone=%2B27820000001&name=Carol

Turn on "Require signed requests" on the connection. Blueticked shows the signing secret once and creates a new callback URL; toggling signed requests or rotating the signing secret also rotates the URL. Update the sender with that URL and keep the signing secret in BLUETICKED_CALLBACK_SIGNING_SECRET. For generic signing, send x-signature as the hex HMAC-SHA256 of the EXACT raw body bytes (sha256= is accepted). Do not reserialize after signing. Vendor signing schemes are different; match the configured scheme rather than substituting this example.

js
import { createHmac } from "node:crypto";
const url = process.env.BLUETICKED_CALLBACK_URL;
const secret = process.env.BLUETICKED_CALLBACK_SIGNING_SECRET;
if (!url || !secret) throw new Error("Callback configuration missing");
const rawBody = JSON.stringify({"event":"appointment.created","id":"booking-1234","customer":{"phone":"+27820000001","name":"Carol"},"appointment":{"service":"Oxygen assessment","slot":"Tuesday 09:30","branch":"Hillcrest"}});
const signature = createHmac("sha256", secret).update(rawBody).digest("hex");
const response = await fetch(url, { method: "POST", headers: {
  "Content-Type": "application/json", "x-signature": signature,
  "Idempotency-Key": "booking-1234:appointment.created:revision-1",
}, body: rawBody });
const result = await response.json();
// Inspect response.status and result.outcome; do not assume a send.

Read the response and retry safely

202 means accepted, not necessarily queued; outcome is absent on duplicates and on connections with no flow. When present, inspect outcome.kind and each outcome.outcomes line: queued means reserved for asynchronous delivery, ran means non-send actions ran, not_sent means a refusal or failure, no_flow means no matching listener, no_contact means no usable contact, withheld means contact suppression, already_sent means the line was previously completed. A line that queued a message and then had a later step refused still reads queued, with the ids of what was queued; the refusal is kept in the automation's run history. A line whose step failed for any other reason (a lost queue publication, an unexpected error) reads not_sent and still lists every message row it made in messageIds; some of those may have gone, so reconcile them before any retry. A queued result is not provider delivery; reconcile messageIds through delivery events or the message status endpoint. A not_sent line the automation refused also carries code, from a closed set, beside its reason: branch on the code, never the sentence; template_value_missing means the event itself lacked a value the message needs, so sending the same event again will not send that line. Send one stable Idempotency-Key for the same delivery and a different delivery key for a later revision, even on the same record. Without that header, the callback falls back to event plus the record reference; two same-event revisions can otherwise look identical. Replaying a completed receipt returns duplicate: true without repeating effects. HTTP 503 receipt_busy or 503 receipt_unavailable returns Retry-After: 10; a paused connection returns 503 integration_paused with Retry-After: 300. Honor Retry-After with bounded backoff, not a new key. Failed or uncertain effects are held for review instead of being repeated blindly: processing_failed can return 500, and a later held receipt returns 202 with outcome.kind not_sent. Changing the delivery key to force another attempt can double a send. Read the connection's delivery log and reconcile first. A 401 signature_invalid or a 404 not_found (Callback not found) needs configuration repair; a 400 invalid_json or a 413 body limit needs payload repair, not blind retries. HTTP 429 has the flat body {"ok": false, "error": "rate_limited"} and Retry-After: 60. Authenticated requests share a 1,000-per-minute budget per connection across sender addresses; invalid credentials are limited to 30 per address per minute. Honor Retry-After without changing the delivery key. WooCommerce setup is not a live event: an unsigned application/x-www-form-urlencoded body containing only one positive-integer webhook_id, with neither x-wc-webhook-topic nor x-wc-webhook-signature, gets HTTP 200 and {"ok": true, "setup_ping": true} on a WooCommerce connection. It creates only a test log, never a contact, flow action or connected status. If that test log cannot be written, the ping gets HTTP 503 setup_ping_unavailable with Retry-After: 10; save the webhook in WooCommerce again. Signed WooCommerce deliveries still require their configured vendor signature.

Events API: create the contact first

These are example UUIDs, not workspace IDs you may reuse. Discover your business ID and substitute it in BOTH requests. All resource IDs are UUIDs, never prefixed strings. The event endpoint accepts international and South African national phone forms, including spaces and punctuation, using the callback's normalizer. Contact creation still requires canonical E.164 phone values. Do not pass a nested contact object to events/send: its identifiers are flat contact_id, email or phone. POST /api/v1/contacts (contacts:write):

json
{
  "business_id": "71000000-0000-4000-9000-000000000002",
  "phone": "+27821234567",
  "first_name": "Sam"
}

POST /api/v1/events/send (events:send), only after the contact request succeeds:

json
{
  "business_id": "71000000-0000-4000-9000-000000000002",
  "channel": "whatsapp",
  "event": "rental.booked",
  "phone": "+27821234567",
  "data": {
    "product": "CPAP",
    "reference": "R-1842",
    "date": "2026-10-06",
    "customer": {
      "department": "Home care"
    }
  }
}

Do not add consent:true just to make an example send. Preserve the customer's actual consent evidence. Free-text WhatsApp still needs the customer's open 24-hour reply window. When it is closed, configure an approved template in the flow; not_sent is not permission to bypass the window. Use a test key/test_mode for events evaluation before live access; its 200 response has outcome: null and queues nothing. Callback URLs do not inherit an API test key's sandbox: a callback can run live flow actions. Use an isolated test connection and explicit human approval before any callback that could contact a recipient or spend credit.

Find the connection and its history

Open Connected apps → Integrations, then the connection. Its record contains the receiving address, setup, Deliveries and What happened. Incoming callbacks are separate fromwebhooks Blueticked sends to your server.