Webhooks
Instead of polling, register a webhook endpoint (POST /api/v1/webhook-endpoints, or in the console) and receive events as signed HTTPS POST requests. An operation create may also name its own callback_url, which overrides the endpoints for that operation's events.
Events
| Event | When |
|---|---|
operation.completed |
an operation ended SEND_SUCCESS |
operation.failed |
an operation ended SEND_FAILURE |
operation.expired |
an operation expired before delivery |
operation.cancelled |
an operation was cancelled, superseded by a retry, or its eUICC deleted |
operation.awaiting_confirmation |
an operation waits for the card's notification |
operation.delivered |
the card collected an operation (opt-in: endpoints that list it only) |
euicc.registered |
a card's certificates were stored for the first time |
euicc.association_changed, euicc.association_broken, euicc.last_eim_deleted |
changes of a card's eIM association |
euicc.deleted, euicc.state_change_reported |
a card was deleted / reported a state change |
import.finished |
an import job finished |
notification.received, notification.relay_failed |
a GSMA notification from a card was stored / could not be relayed |
webhook.test |
the endpoint's test event |
Payload
{
"id": "0192…", "type": "operation.completed", "created_at": "2026-10-24T08:00:00.000Z",
"tenant_id": "…", "sub_tenant_path": "acme.eu",
"data": { "operation_id": "…", "eid": "89049032123451234512345678901234", "action": "ENABLE",
"operation_status": "COMPLETED", "command_status": "SEND_SUCCESS", "outcome": [] },
"links": { "self": "/api/v1/operations/…" }
}
Signature
Deliveries follow Standard Webhooks. Each carries three headers:
webhook-id— the event's id, the same on every retry and replay;webhook-timestamp— seconds since the Unix epoch;webhook-signature—v1,<base64 HMAC-SHA256>overwebhook-id.webhook-timestamp.bodywith the endpoint's secret; during a secret rotation two signatures, separated by a space.
import base64, hashlib, hmac, time
def verify(secret: bytes, headers, body: bytes) -> bool:
msg_id, ts = headers["webhook-id"], headers["webhook-timestamp"]
if abs(time.time() - int(ts)) > 300:
return False # older than 5 minutes: refuse
mac = hmac.new(secret, f"{msg_id}.{ts}.".encode() + body, hashlib.sha256).digest()
expected = base64.b64encode(mac).decode()
return any(
hmac.compare_digest(sig.removeprefix("v1,"), expected)
for sig in headers["webhook-signature"].split()
)
Receiver checklist
- Verify
webhook-signaturewith the endpoint secret overwebhook-id.webhook-timestamp.body; reject timestamps older than 5 minutes. - Respond 2xx within 10 s; do the real work asynchronously.
- De-duplicate on
webhook-id. - Do not infer ordering from arrival; read
operation_id+command_status(+attempt) fromdata. - Fetch the full resource from
links.selfwhen the summary is not enough; the payload is a snapshot at emission time. - Rotate the secret from the UI and keep accepting both signatures for 24 h.
Retries
A delivery that does not answer 2xx within 10 seconds is retried after 1 min, 5 min, 30 min, 2 h and 12 h — six attempts in all — when it failed with a 5xx, 408 or 429, a connection error or a timeout. Any other 4xx is not retried. A delivery that runs out of attempts becomes a dead letter, kept for 30 days and replayable from the console or with POST /api/v1/webhook-deliveries/{delivery_id}:replay; a replay keeps the original webhook-id.