Webhooks

Consio can POST JSON events to your HTTPS endpoint when SMS messages are received, delivered, or fail. You manage endpoints over the Public API; deliveries are signed so you can verify authenticity.

Create body

{
  "url": "https://example.com/webhooks/consio",
  "description": "Production SMS events",
  "events": ["message.received", "message.delivered", "message.failed"]
}

Constraints:

  • URL must be absolute HTTPS
  • Max 20 endpoints per workspace
  • Create response includes secret (whsec_…) once; later responses expose only secret_suffix

Events

EventWhen it fires
message.receivedAn inbound SMS is persisted
message.deliveredAn outbound SMS is marked delivered
message.failedAn outbound SMS fails or is rejected at send time

Only enabled endpoints subscribed to the event receive the delivery.

Payload

{
  "event_id": "550e8400-e29b-41d4-a716-446655440000",
  "event": "message.received",
  "workspace_rid": "…",
  "message": {
    "rid": "…",
    "type": "SMS",
    "content": "Hello",
    "trigger_type": "INBOUND",
    "customer_rid": "…",
    "phone_number_rid": "…",
    "user_rid": null,
    "ai_agent_rid": null,
    "customer_phone_number": "+15551234567",
    "external_status": null,
    "sent_at": null,
    "delivered_at": null,
    "failed_at": null,
    "created_at": "2026-07-24T12:00:00Z",
    "updated_at": "2026-07-24T12:00:00Z"
  }
}

event_id is a stable unique identifier. Retries reuse the same event_id — use it for deduplication.

Delivery is at-least-once. There is no ordering guarantee across endpoints or events.

Request format

Each delivery is:

  • POST to your URL
  • Content-Type: application/json
  • User-Agent: Consio-Webhooks/1.0
  • X-Consio-Signature: t=<unix_seconds>,v1=<hmac_sha256_hex>
  • No redirects followed
  • 5 second timeout

Verifying signatures

Signed bytes: "{t}." + raw_body (HMAC-SHA256 with your whsec_… secret).

  1. Read the raw request body before JSON parsing
  2. Parse t and v1 from X-Consio-Signature
  3. Reject if |now - t| > 300 seconds (replay protection)
  4. Recompute the HMAC and compare with a constant-time equality check
def verify_signature(*, secret: str, body: bytes, header_value: str, tolerance_seconds: int = 300) -> bool:
    """Return whether ``header_value`` is a valid signature for ``body``.

    Rejects signatures whose timestamp is older or newer than
    ``tolerance_seconds`` relative to now (replay protection).

    Args:
        secret: str
            Endpoint signing secret (plaintext).
        body: bytes
            Raw HTTP request body that was signed.
        header_value: str
            Value of ``X-Consio-Signature``.
        tolerance_seconds: int
            Max absolute age of the signature timestamp. Default 300s.

    Returns:
        bool: True when the signature matches and the timestamp is fresh.
    """
    parts = dict(part.strip().split("=", 1) for part in header_value.split(",") if "=" in part)
    try:
        timestamp = int(parts["t"])
        expected_v1 = parts["v1"]
    except (KeyError, ValueError):
        return False

    if abs(int(time.time()) - timestamp) > tolerance_seconds:
        return False

    expected_header = build_signature_header(secret=secret, body=body, timestamp=timestamp)
    expected_digest = dict(part.strip().split("=", 1) for part in expected_header.split(",") if "=" in part)["v1"]
    return hmac.compare_digest(expected_digest, expected_v1)

Delivery policy

OutcomeBehavior
HTTP 2xxSuccess
408, 425, 5xxRetried
429Retried using Retry-After (default 60s if missing/invalid) (max 10mn)
Other 4xx, redirects (3xx)Permanent failure — not retried
Network timeout / connection errorsRetried

Retry settings:

  • Up to 6 attempts total (1 initial + 5 retries)
  • Exponential backoff with jitter (roughly 1s → 2s → 4s → 8s → 16s, capped at 10 minutes)
  • Payload snapshot is frozen at dispatch; retries resend that snapshot with a new signature timestamp
  • Same event_id across retries

If an endpoint is disabled, deleted, or unsubscribed while a delivery is queued, Consio skips the HTTP call.


Did this page help you?