Skip to main content

Webhooks

This page documents customer-facing webhooks for API integrations.

Webhooks are the lifecycle source of truth after a send request is accepted. A synchronous send response tells you the request was queued. Webhooks tell you what happened after that.

What webhooks are for

Use customer-facing webhooks to:

  • track outbound status progression after QUEUED
  • detect final delivery or failure outcomes
  • receive inbound customer messages
  • correlate platform events back to your own business records

If you send messages but do not consume webhooks, your integration is missing the final delivery and failure lifecycle.

Customer-facing webhook types

Outbound lifecycle

  • message.queued
  • message.sent
  • message.delivery
  • message.failed

Inbound message

  • message.inbound

Typical outbound lifecycle

For a normal successful outbound send, you should expect a sequence similar to:

  1. message.queued
  2. message.sent
  3. message.delivery

For a failed outbound send, you should expect the sequence to terminate with message.failed.

Treat sequence as the ordering field for outbound lifecycle handling.

Signing headers

When signing is enabled, Red Cloud sends:

  • X-RedCloud-Timestamp
  • X-RedCloud-Signature

Signing formula:

HMAC_SHA256(secret, "${timestamp}.${rawBody}")

Where:

  • timestamp is the value of X-RedCloud-Timestamp
  • rawBody is the exact JSON body delivered to your endpoint

Signature verification requirements

  • verify against the exact raw request body, not a re-serialized object
  • reject requests missing signing headers
  • reject requests with invalid signatures
  • return a non-2xx response when signature verification fails

If your framework parses JSON automatically, make sure you still preserve the raw body used in the HMAC calculation.

Retry/backoff behavior

Webhook delivery is queue-based.

Current retry schedule:

  • 1 minute
  • 5 minutes
  • 15 minutes
  • 60 minutes
  • then terminal failure

Operational implication

Your webhook endpoint should:

  • return 2xx only after the event is durably accepted by your system
  • return non-2xx when processing failed and a retry is appropriate
  • avoid long synchronous work before acknowledging receipt

If you need heavier downstream processing, acknowledge quickly and continue inside your own system.

Canonical payload snapshots

message.queued

{
"eventType": "message.queued",
"direction": "outbound",
"occurredAt": 1775002218000,
"apiClientId": "rc_test_client_123",
"messageId": "msg_abc123",
"sequence": 1,
"data": {
"messageId": "msg_abc123",
"externalReference": "order_789",
"sendFrom": "+15551234567",
"to": "+15551234567",
"status": "QUEUED",
"error": null
}
}

message.sent

{
"eventType": "message.sent",
"direction": "outbound",
"occurredAt": 1775002218200,
"apiClientId": "rc_test_client_123",
"messageId": "msg_abc123",
"sequence": 2,
"data": {
"messageId": "msg_abc123",
"externalReference": "order_789",
"sendFrom": "+15551234567",
"to": "+15551234567",
"status": "sent",
"error": null
}
}

message.delivery

{
"eventType": "message.delivery",
"direction": "outbound",
"occurredAt": 1775002218400,
"apiClientId": "rc_test_client_123",
"messageId": "msg_abc123",
"sequence": 3,
"data": {
"messageId": "msg_abc123",
"externalReference": "order_789",
"sendFrom": "+15551234567",
"to": "+15551234567",
"status": "delivered",
"error": null,
"providerMessageRef": "provider_msg_123"
}
}

message.failed

{
"eventType": "message.failed",
"direction": "outbound",
"occurredAt": 1775002218600,
"apiClientId": "rc_test_client_123",
"messageId": "msg_abc123",
"sequence": 4,
"data": {
"messageId": "msg_abc123",
"externalReference": "order_789",
"sendFrom": "+15551234567",
"to": "+15551234567",
"status": "failed",
"error": {
"code": "PROVIDER_SEND_FAILED",
"message": "Provider send failed",
"details": {}
}
}
}

message.inbound

{
"eventType": "message.inbound",
"direction": "inbound",
"occurredAt": 1775002218800,
"apiClientId": "rc_test_client_123",
"data": {
"messageId": "msg_abc123",
"conversationId": "conv_abc123",
"numberId": "num_abc123",
"from": "+15551234567",
"to": "+15551234567",
"body": "Reply from customer",
"media": [],
"timestamp": 1775002218800
}
}

Field explanation

Common fields you should expect:

  • eventType
    • lifecycle event name
  • direction
    • outbound or inbound
  • occurredAt
    • event timestamp in epoch milliseconds
  • apiClientId
    • Red Cloud API client identifier
  • messageId
    • Red Cloud message identifier
  • sequence
    • outbound event ordering field

For outbound events, persist and use:

  • messageId
  • externalReference
  • sequence

For inbound events, persist and use:

  • messageId
  • conversationId
  • numberId
  • sender and recipient values

For every webhook request:

  1. capture the raw body
  2. verify signature headers
  3. parse the payload
  4. log or persist eventType, messageId, and occurredAt
  5. apply idempotent event handling in your own system
  6. return 200 only after the event is accepted internally

At minimum, your integration should avoid duplicate downstream effects when a webhook delivery is retried.

Common webhook mistakes

  • treating the synchronous send response as final delivery
  • verifying a re-serialized JSON body instead of the raw body
  • returning 200 before the event is actually accepted internally
  • ignoring sequence for outbound lifecycle handling
  • not persisting messageId and externalReference
  • doing heavy downstream work before sending the HTTP response

Webhook boundary

Customer-facing webhook endpoints are configured by the customer and receive signed Red Cloud deliveries.

Internal platform ingestion paths are not part of the public API surface and are intentionally undocumented here.