KKeepMyNumber
Get started

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_events empty 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 the webhook_url field 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