Skip to main content

HTTP Webhooks

BlueSender's HTTP surface has two directions:

  • CRM → BlueSender: your CRM POSTs outbound messages to /crm/webhook on the BlueSender backend.
  • BlueSender → CRM: BlueSender posts inbound messages and per-message status updates back to your CRM by calling its API endpoints directly (POST /conversations/messages/inbound and PUT /conversations/messages/{messageId}/status).

Both directions are described below.

CRM → BlueSender: POST /crm/webhook​

Endpoints​

Three paths are accepted. They behave identically — pick whichever matches your CRM's terminology.

PathNotes
POST /crm/webhookRecommended primary path.
POST /crm/webhook/imessageAlternate.
POST /crm/webhook/smsAlternate.

All three forward the message to the Mac client bound to the locationId carried in the payload. Routing and validation are identical; the only observable difference is the message field on the 200 OK body — /crm/webhook/sms returns "Message sent to SMS" while the other two return "Message sent to iMessage".

Request body​

The body is JSON. The shape matches the CRM's Conversation Provider — Outbound Message webhook schema, with all fields at the top level. No field aliases are accepted — send the exact field names below. Earlier releases tolerated a wider alias set; that compatibility shim was removed in 1.19.13 (see Deprecations).

FieldTypeRequiredNotes
locationIdstringyesRouting key — identifies the CRM tenant / Mac.
contactIdstringyesRecipient contact in the CRM.
messagestringyes (or attachments)SMS / iMessage body. Send as a string, not an object.
attachmentsarray of URL stringsyes (or message)Pre-uploaded asset URLs to send along with or instead of message.
messageIdstringrecommendedPer-message correlation ID. Required if you want status callbacks.
phonestringconditionalRecipient's phone. Optional when contactId is supplied and BlueSender's contact lookup against the CRM succeeds. If the lookup fails (CRM auth issue, contact not found, missing phone on the contact, etc.) the request is rejected with HTTP 400 Missing required fields. Send phone explicitly to avoid relying on the lookup.
typestringoptionalWhen present, must equal "SMS". BlueSender routes everything through iMessage / SMS at the Mac, so "Email" (and any other value) is rejected with HTTP 400 UNSUPPORTED_MESSAGE_TYPE. Omit the field if you want default behavior.
userIdstringoptionalThe CRM user who triggered the send. Logged but not interpreted.

Body length limit: requests with a message body exceeding 20,000 characters are rejected with HTTP 400. This matches the practical iMessage character limit.

Responses​

StatusWhenBody
200 OKForwarded to the Mac.{ "status": "success", "requestId", "message", "messageId", "locationId" }
400 Bad RequestValidation failure. The body shape varies by failure type — labels include Invalid content type, Invalid request body, Missing required fields, Message body exceeds maximum length of 20000 characters, Invalid phone number format, and Unsupported message type. See Error codes.varies by label
401 UnauthorizedSignature invalid or absent — webhook signature verification is enforced. See Signature verification.(varies)
404 Not FoundNo Mac client connected for that locationId.{ "error": "Location not connected", "locationId", "message" }
429 Too Many RequestsPer-locationId HTTP rate limit exceeded — or per-source-IP when locationId is missing/malformed. See Rate limiting.{ "error": "TOO_MANY_REQUESTS", "message": "Too many webhook requests for this location, please try again later." }
5xxInternal error during processing.(varies)

The 404 body does not include a code field. The structured error code CLIENT_NOT_CONNECTED is delivered separately via the status-update callback (see below) when a messageId is present.

Routing​

locationId is the routing key. BlueSender forwards the webhook to the Mac client currently bound to that locationId. If no Mac is online for that locationId at the moment the webhook arrives, the request fails immediately with HTTP 404 — see No backend queue for the rationale and the expected CRM-side retry pattern.

Example​

This illustrates the request body shape only — it is not a runnable request. Signature verification is enforced (see Signature verification), so an unsigned curl like this is rejected with HTTP 401. In practice the CRM generates the X-GHL-Signature header automatically on real deliveries; signatures cannot be produced by hand here.

curl -X POST https://api.bluesendr.com/crm/webhook/imessage \
-H "Content-Type: application/json" \
-d '{
"type": "SMS",
"locationId": "loc123",
"contactId": "ct123",
"messageId": "msg123",
"message": "hi",
"phone": "+15555555555"
}'

Signature verification​

BlueSender verifies webhook signatures against the CRM's published public keys. Two signature headers are supported during the CRM's header migration:

HeaderAlgorithmStatus
X-GHL-SignatureEd25519Current standard — preferred when present.
X-WH-SignatureRSA-SHA256Legacy — deprecated by the CRM on 2026-07-01. Accepted only as a fallback while it is still sent.

When both headers are present, X-GHL-Signature (Ed25519) is verified and the legacy header is ignored. The verifier is pinned to the CRM's published keys, so signatures produced with any other key do not validate.

Signature verification is mandatory — enforced

Webhook signature verification is enforced in production. Webhooks to /crm/webhook must carry a valid signature BlueSender can verify, or they are rejected with HTTP 401. Signing is a requirement for every CRM integration, not an optional feature.

There is no unsigned passthrough: an absent signature is rejected exactly like an invalid one.

Header migration status

The CRM signs Conversation Provider webhook deliveries (the channel BlueSender receives on) with the Ed25519 X-GHL-Signature header; the legacy RSA X-WH-Signature header is not sent on this channel. BlueSender's verifier reads both, prefers X-GHL-Signature, and keeps the RSA path only as a transitional fallback. The legacy X-WH-Signature fallback will be removed after the CRM completes its 2026-07-01 deprecation.

Behavior (enforced):

SignatureResult
Present and validWebhook processed.
Present but invalidRejected with HTTP 401.
AbsentRejected with HTTP 401.

There is no unsigned passthrough — an absent signature is rejected exactly like an invalid one.

Integrating a CRM other than the primary one?

Signing is mandatory for all CRM integrations — verification is enforced and there is no unsigned path. BlueSender verifies against an allowlist of trusted public keys; a CRM whose key is not on the allowlist cannot deliver webhooks (every request is rejected with HTTP 401). Access is gated by design — a new CRM is onboarded deliberately, not by simply pointing webhooks at the endpoint.

To integrate a new CRM, contact BlueSender support and provide:

  • Your public key (PEM), used to verify your webhook signatures.
  • The signature scheme: Ed25519 or RSA-SHA256, computed over the exact raw request body, signature base64-encoded.
  • The request header your CRM sends the signature in (e.g. your own X-…-Signature). It does not have to match the primary CRM's header — your header name is registered alongside your key.

BlueSender adds your key, scheme, and header to the trusted allowlist and notifies you when your integration is live. Until then your webhooks are rejected. A CRM that cannot produce Ed25519 or RSA-SHA256 signatures over the raw body cannot be onboarded.

Backend → CRM delivery​

BlueSender posts back to the CRM in two situations:

  1. An iMessage / SMS arrives on the Mac and needs to be forwarded to the CRM as an inbound message.
  2. The status of an outbound message changes (pending, delivered, failed) and needs to be reflected in the CRM.

Both calls go to the CRM API host configured on the BlueSender deployment (CRM_HOSTNAME, default services.leadconnectorhq.com). A CRM that adopts these two endpoints can receive BlueSender traffic.

POST /conversations/messages/inbound​

Used to deliver inbound iMessage / SMS messages from the Mac to the CRM. Request shape:

{
"type": "SMS",
"contactId": "<crm contact id>",
"locationId": "<crm location id>",
"message": "Inbound message text, possibly with bluesendr-metadata:v1 footer",
"attachments": ["https://..."]
}
FieldNotes
typeThe CRM's Conversation Provider channel discriminator (the documented enum is [SMS / Email / Call]). BlueSender only emits "SMS" today; iMessage and SMS messages from the Mac are both reported as "SMS" because the CRM treats them under the same channel.
contactIdResolved from the inbound sender's phone via /contacts/search against the CRM.
locationIdThe CRM tenant the Mac is bound to.
messageThe inbound body. For groups, voice messages, reactions, and stickers this includes a bluesendr-metadata:v1 footer the CRM should parse.
attachmentsOptional array of pre-uploaded asset URLs (flat string array). Omitted when there are no attachments. The CRM's newer Conversations help-portal docs describe a richer per-attachment object (with type and nested content); BlueSender currently emits the older flat-URL form, which the CRM's Conversation Provider endpoint still accepts.

PUT /conversations/messages/{messageId}/status​

Used to update the per-message delivery state of an outbound message the CRM previously POSTed to /crm/webhook. The {messageId} in the URL is the top-level messageId field from the original /crm/webhook request body.

Send a real per-message messageId

The value BlueSender substitutes into the URL must be a real per-message identifier. The CRM's status endpoint resolves {messageId} against its own message store — a conversationId or conversationProviderId is not a message ID and is not accepted.

The status callback chain runs only when the inbound payload carries a per-message messageId at the top level of the body. If your CRM sends only a conversation-level identifier, BlueSender will still route the message to iMessage and return 200, but no status callback will be issued to the CRM and the message will not transition out of its initial state. Pass a per-message messageId on every POST /crm/webhook to enable status reporting.

Lifecycle. When all three preconditions hold — a resolvable message ID is present on the inbound request, OAuth status callbacks are configured on the deployment (see Authentication below), and the Mac eventually sends a per-message status frame back — BlueSender posts up to two status callbacks per outbound message:

  1. status: "pending" — fired immediately after the backend accepts the request and forwards it toward the Mac (i.e. published onto the internal routing channel for the owning instance). This is a backend-level ack, not a Mac-receipt ack.
  2. status: "delivered" or status: "failed" — fired once the Mac sends a message_status frame back. If the Mac is offline or never reports a terminal state, no second callback is made.

If any precondition fails (no resolvable ID on the inbound request, OAuth not configured, or no Mac status frame), the corresponding callback is skipped silently. Operators can confirm by tailing backend logs for oauth_not_configured (no OAuth) or absence of status-update entries (no Mac frame).

Request shape:

{
"status": "delivered",
"error": {
"code": "<error.code, e.g. CLIENT_NOT_CONNECTED>",
"type": "error",
"message": "Human-readable detail"
}
}
FieldNotes
statusThe values BlueSender emits today are pending (forwarded to Mac), delivered (Mac sent successfully), and failed (Mac could not send). The CRM's underlying status endpoint may accept additional values (e.g. read, sent, undelivered) — BlueSender does not produce them.
errorPresent only when status === "failed". See Error codes for the catalog of error.code values.

Authentication​

BlueSender authenticates outbound calls to the CRM with a bearer token on the Authorization header, plus a Version header (currently Version: 2021-07-28) carrying the CRM's API-version discriminator. The token is obtained per-locationId and refreshed automatically.

The two BE → CRM endpoints have different auth fallback behavior:

EndpointOAuth (CRM_OAUTH_ENABLED=true)PIT fallback
POST /conversations/messages/inboundUsed when configured.Yes — falls back to a Private Integration Token (PIT) when OAuth is unavailable.
PUT /conversations/messages/{messageId}/statusRequired.No — there is no PIT fallback for status updates.

If a deployment is not OAuth-configured, all status callbacks are skipped and logged with reason oauth_not_configured; inbound message delivery still works via PIT. Operators planning to enable status callbacks must complete the OAuth setup (CRM_OAUTH_CLIENT_ID, CRM_OAUTH_CLIENT_SECRET, CRM_OAUTH_TOKEN_URL, CRM_OAUTH_ENCRYPTION_KEY, CRM_OAUTH_DATABASE_URL — or the generic DATABASE_URL fallback — and a numeric CRM_OAUTH_REFRESH_THRESHOLD_SEC) — see the OAuth callback codes in the error catalog for the failure modes during setup.

See Errors & Codes for the full error.code catalog and License vs Location for the relationship between license seats and locations.