TitanSim / Developers
Webhooks and events
Receive signed lifecycle events or poll the event feed.
Event types
Register up to three HTTPS webhook endpoints per mode. A create response shows its signing secret once. Subscribe to selected event types or use the default selection. Each event carries the resource as the API returns it: order.completed carries the Order, esim.* events carry the Esim (esim.usage adds threshold_percent 50 or 80; esim.depleted means 100% used), topup.completed carries the Topup including balance_after_usd, and balance.low carries balance_usd and low_balance_threshold_usd.
- order.completed
- esim.activated
- esim.usage
- esim.depleted
- esim.expiring
- esim.expired
- topup.completed
- balance.low
- webhook.test
Verify signatures
Use the raw request body. TitanSim-Signature has t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>. Reject timestamps outside five minutes. Compare digests in constant time.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyTitanSimWebhook(secret, rawBody, header) {
const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? '');
if (!match) return false;
const timestamp = Number(match[1]);
if (!Number.isInteger(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`, 'utf8').digest();
return timingSafeEqual(expected, Buffer.from(match[2], 'hex'));
}Python verifier
Use the same raw bytes before JSON parsing.
import hmac
import hashlib
import time
import re
def verify_titansim_webhook(secret: str, raw_body: bytes, header: str) -> bool:
match = re.fullmatch(r't=(\d+),v1=([0-9a-f]{64})', header or '')
if not match:
return False
timestamp = int(match.group(1))
if abs(time.time() - timestamp) > 300:
return False
signed = str(timestamp).encode() + b'.' + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, match.group(2))Delivery and polling
Any 2xx response acknowledges delivery. Retries occur after 1 minute, 5 minutes, 30 minutes, 2 hours, and 12 hours, then the delivery is exhausted. GET /events?cursor=&type= is a polling alternative, oldest first from the cursor, with 30-day retention.
Timing: order.completed and topup.completed are sent within seconds. Activation, usage, depletion and expiry events follow carrier reporting and our sync cadence — expect them within about 15 minutes of the change being observed; esim.expiring arrives between 24 and 20 hours before expires_at.
- TitanSim-Event-Id identifies an event.
- TitanSim-Event-Type names its type.
- POST /webhook-endpoints/{id}/test sends webhook.test.