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 onlysecret_suffix
Events
| Event | When it fires |
|---|---|
message.received | An inbound SMS is persisted |
message.delivered | An outbound SMS is marked delivered |
message.failed | An 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:
POSTto your URLContent-Type: application/jsonUser-Agent: Consio-Webhooks/1.0X-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).
- Read the raw request body before JSON parsing
- Parse
tandv1fromX-Consio-Signature - Reject if
|now - t| > 300seconds (replay protection) - 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
| Outcome | Behavior |
|---|---|
HTTP 2xx | Success |
408, 425, 5xx | Retried |
429 | Retried using Retry-After (default 60s if missing/invalid) (max 10mn) |
Other 4xx, redirects (3xx) | Permanent failure — not retried |
| Network timeout / connection errors | Retried |
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_idacross retries
If an endpoint is disabled, deleted, or unsubscribed while a delivery is queued, Consio skips the HTTP call.
Updated 11 days ago
Did this page help you?
