# eSIMfly Business API — Webhooks (PUT/GET /webhooks) — prompt for AI coding assistants TASK: configure the eSIMfly webhook once and implement a secure receiver, so our system learns in real time when an eSIM is installed, changes status or runs low on data — instead of polling. COMMON RULES (apply to every eSIMfly request) - Base URL: https://esimfly.net/api/v1/business - Headers on every call: RT-AccessCode (esf_...), RT-RequestID (fresh UUID v4 per request; reuse -> 400 DUPLICATE_REQUEST), RT-Timestamp (ms since epoch; >5 min old -> 401 INVALID_TIMESTAMP), RT-Signature = UPPERCASE hex HMAC-SHA256(secretKey, timestamp + requestId + accessCode + rawBody). rawBody = "" for GET; for POST/PUT sign the exact body string you send, with Content-Type: application/json. - Keep access code + secret key server-side in env vars. Never ship them to a browser or mobile app. - Responses: { success: true, ... } or { success: false, error|message, code }. Branch on `success` and `code`. - Rate limits are per API key, shown in the business dashboard (typically 100/minute, 1,000/hour, 10,000/day) -> RATE_LIMIT_EXCEEDED. Pace bulk work at <= 1 req/s. - Read `currency` from responses (USD | IQD | EUR for enterprise). Never hard-code it. IQD amounts are integers. - Package codes are opaque strings: store and send back verbatim, never parse or prefix them. - Node.js/TypeScript: use the official SDK instead of raw HTTP — `npm install @esimfly/sdk` (https://github.com/eSimfly-Official/esimfly-sdk-nodejs); it implements these rules. Other languages: implement the contract below. - Full multi-endpoint prompt: https://docs.esimfly.net/llm/esimfly-api-full-prompt.txt EVENTS (subscribe only to what you use) esim.installed profile enabled on a device for the first time (seconds; eSIMfly-network eSIMs) esim.profile.updated SM-DP+ state: BPP installation -> Enable -> Disable/Delete (seconds; chatty — usually skip) esim.usage.threshold remaining data crossed 500 / 200 / 100 / 50 MB (seconds; not for unlimited plans) esim.status.changed NEW -> ACTIVE -> DEPLETED / EXPIRED / CANCELLED (<= 30 min; all providers) esim.provisioned async-provisioned order has its QR code (seconds; async packages only) order.completed reserved, not emitted yet Each transition is delivered once per eSIM even when we learn of it from two sources. CONFIGURE (one-time, or from an admin screen) PUT https://esimfly.net/api/v1/business/webhooks Body: { "webhook_url": "https://our-server.com/api/esimfly-webhook", "events": ["esim.installed", "esim.status.changed", "esim.usage.threshold"] } -> { success: true, webhook: { url, secret: "whsec_...", events }, note } SAVE `secret` in a secure secret store — it is shown once. Re-run PUT to rotate it. GET https://esimfly.net/api/v1/business/webhooks -> { success, webhook: { url, events, api_key_name }, available_events: [{ event, description }], recent_deliveries: [{ id, event, order_reference, status, attempts, last_attempt, delivered_at }] } (Also configurable in Business Dashboard > Settings > API Keys > Set URL.) DELIVERY (eSIMfly -> us) POST , Content-Type: application/json, User-Agent: eSIMfly-Webhook/1.0 Headers: X-Webhook-Event, X-Webhook-Signature ("sha256="), X-Webhook-Timestamp (ISO), X-Webhook-Id (unique delivery id) Body: { "event": "", "timestamp": "", "data": { iccid, esim_id, package_code, order_reference, source, ...event fields } } Event fields: esim.installed { eid, imsi, installed_at } esim.profile.updated { profile_status: "BPP installation"|"Enable"|"Disable"|"Delete", profile: "Enabled"|"Disabled"|"Not Installed", previous_profile_status, eid, imsi, changed_at } esim.status.changed { old_status, new_status, changed_at, expiry_date } esim.usage.threshold { threshold_remaining_mb: 500|200|100|50, used_percent, used_gb, total_gb, remaining_gb, package_id, reported_at } esim.provisioned { iccid, qr_code_url, lpa_string, direct_apple_install_url, direct_android_install_url, package_name } Retries if we do not answer 2xx within 10 s: 10 s, 30 s, 2 min, 10 min (5 attempts total), then marked failed. RECEIVER — implement exactly 1. Read the RAW request body bytes (disable JSON body parsing on this route). 2. expected = "sha256=" + hex(HMAC-SHA256(secret, rawBody)); compare with X-Webhook-Signature using a constant-time comparison. Mismatch or missing header -> 401, do nothing else. 3. Dedupe: store X-Webhook-Id with a unique constraint; if already seen -> respond 200 immediately. 4. Persist the payload, respond 200 within 10 seconds, process in a background job. 5. Job: find our esims row by data.iccid (fallback data.esim_id / order_reference) and apply: - esim.installed -> mark installed_at, notify the customer their eSIM is on the phone - esim.status.changed -> update status/expiry; on ACTIVE start the validity countdown, on DEPLETED/EXPIRED offer a top-up - esim.usage.threshold -> update used/remaining GB; at 200 MB or below send a low-data message / top-up offer - esim.profile.updated -> update profile state if we display it 6. Unknown ICCID (race with our own order insert) -> retry the job a few times before alerting. 7. Treat webhooks as the primary signal; keep GET /esims/usage/query and POST /esims/status for reconciliation only. 8. HTTPS only; never log the secret or put it in client code. DELIVERABLE: setWebhook()/getWebhook() in the shared client, the raw-body receiver with signature check and dedupe table, the background event applier, and an admin page showing recent_deliveries.