Webhooks
We push an event to your registered URLs whenever anything meaningful happens. Webhooks are the fast path; the events API is the queryable history behind them.
Registering endpoints
curl -X POST https://api.example.com/v2/webhook_endpoints \
-H "Authorization: Bearer pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://yourapp.com/hooks/porting",
"enabled_events": ["porting_order.status_changed", "number.activated"]}'
- Leave
enabled_eventsempty to receive all event types. - The response contains the endpoint's signing
secret— shown once. - You can register multiple endpoints, pause them (
active: false), rotate secrets (POST …/rotate_secret), and set a per-order override with thewebhook_urlfield on a porting order. - URLs must be HTTPS.
- The full list of event types:
GET /v2/webhook_deliveries/meta/event_types.
Delivery format
POST https://yourapp.com/hooks/porting
Content-Type: application/json
Porting-Event-Id: 55f2…
Porting-Event-Type: porting_order.status_changed
Porting-Signature: t=1765432100,v1=6c3f0e…
{"id": "55f2…", "type": "porting_order.status_changed",
"created_at": "2026-08-12T10:00:00Z",
"payload": {"porting_order": {…}, "previous_status": "in_process",
"new_status": "foc_scheduled"}}
Respond with any 2xx within 15 seconds. Anything else counts as a failure and triggers retries.
Verifying signatures
The signature is HMAC-SHA256 over "<timestamp>.<raw body>" with your
endpoint's secret. Always verify it, and reject stale timestamps (replay
protection):
import hashlib, hmac, time
def verify(secret: str, body: bytes, header: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
Retries
Failed deliveries are retried with backoff: 1m, 5m, 30m, 2h, 6h. After the
last failure the delivery is marked failed (the event itself is never lost —
you can always republish it).
Design your handler to be idempotent: use Porting-Event-Id to deduplicate,
because retries can occasionally deliver the same event twice.
The audit log
Every webhook we ever sent you (or tried to) is recorded and queryable:
# everything, newest first — filter by status, event_type, endpoint, order, date
curl "https://api.example.com/v2/webhook_deliveries?status=failed" \
-H "Authorization: Bearer pk_live_YOUR_KEY"
# one delivery in full: per-attempt history + the exact payload sent
curl https://api.example.com/v2/webhook_deliveries/{delivery_id} \
-H "Authorization: Bearer pk_live_YOUR_KEY"
# fixed your endpoint? retry a failed delivery immediately
curl -X POST https://api.example.com/v2/webhook_deliveries/{delivery_id}/retry \
-H "Authorization: Bearer pk_live_YOUR_KEY"
Each delivery's attempt_log lists every HTTP attempt with its response code,
error, and duration — enough to debug any delivery problem without contacting
support.
Missed events
If your endpoint was down past the retry window, or you need an event again:
curl -X POST https://api.example.com/v2/porting/events/{event_id}/republish \
-H "Authorization: Bearer pk_live_YOUR_KEY"
Event types
| Type | Fired when |
|---|---|
porting_order.created |
A draft order was created |
porting_order.split |
One request became several orders |
porting_order.status_changed |
An order moved through its lifecycle |
porting_order.requirement_updated |
A requirement was approved or rejected |
porting_order.new_comment |
Ops/provider commented on an order |
porting_order.foc_date_changed |
The confirmed port date changed |
number.activated |
A number went live on the platform |
number.ported_out |
A number left the platform |
port_out.created |
Another carrier requested one of your numbers |
port_out.status_changed |
A port-out was approved/rejected/completed/expired |