HTTP Webhooks
BlueSender's HTTP surface has two directions:
- CRM → BlueSender: your CRM
POSTs outbound messages to/crm/webhookon 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/inboundandPUT /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.
| Path | Notes |
|---|---|
POST /crm/webhook | Recommended primary path. |
POST /crm/webhook/imessage | Alternate. |
POST /crm/webhook/sms | Alternate. |
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).
| Field | Type | Required | Notes |
|---|---|---|---|
locationId | string | yes | Routing key — identifies the CRM tenant / Mac. |
contactId | string | yes | Recipient contact in the CRM. |
message | string | yes (or attachments) | SMS / iMessage body. Send as a string, not an object. |
attachments | array of URL strings | yes (or message) | Pre-uploaded asset URLs to send along with or instead of message. |
messageId | string | recommended | Per-message correlation ID. Required if you want status callbacks. |
phone | string | conditional | Recipient'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. |
type | string | optional | When 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. |
userId | string | optional | The 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
| Status | When | Body |
|---|---|---|
200 OK | Forwarded to the Mac. | { "status": "success", "requestId", "message", "messageId", "locationId" } |
400 Bad Request | Validation 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 Unauthorized | Signature invalid or absent — webhook signature verification is enforced. See Signature verification. | (varies) |
404 Not Found | No Mac client connected for that locationId. | { "error": "Location not connected", "locationId", "message" } |
429 Too Many Requests | Per-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." } |
5xx | Internal 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:
| Header | Algorithm | Status |
|---|---|---|
X-GHL-Signature | Ed25519 | Current standard — preferred when present. |
X-WH-Signature | RSA-SHA256 | Legacy — 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.
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.
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):
| Signature | Result |
|---|---|
| Present and valid | Webhook processed. |
| Present but invalid | Rejected with HTTP 401. |
| Absent | Rejected with HTTP 401. |
There is no unsigned passthrough — an absent signature is rejected exactly like an invalid 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:
Ed25519orRSA-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:
- An iMessage / SMS arrives on the Mac and needs to be forwarded to the CRM as an inbound message.
- 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://..."]
}
| Field | Notes |
|---|---|
type | The 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. |
contactId | Resolved from the inbound sender's phone via /contacts/search against the CRM. |
locationId | The CRM tenant the Mac is bound to. |
message | The inbound body. For groups, voice messages, reactions, and stickers this includes a bluesendr-metadata:v1 footer the CRM should parse. |
attachments | Optional 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.
messageIdThe 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:
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.status: "delivered"orstatus: "failed"— fired once the Mac sends amessage_statusframe 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"
}
}
| Field | Notes |
|---|---|
status | The 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. |
error | Present 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:
| Endpoint | OAuth (CRM_OAUTH_ENABLED=true) | PIT fallback |
|---|---|---|
POST /conversations/messages/inbound | Used when configured. | Yes — falls back to a Private Integration Token (PIT) when OAuth is unavailable. |
PUT /conversations/messages/{messageId}/status | Required. | 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.