Partner Reseller API
Browse the eSIM catalog, place orders on behalf of your own customers, and receive delivery confirmation from your own backend — no browser session required.
https://triptel.co/api/v1/partner1. Authentication
All endpoints except the token endpoint itself require a Bearer access token in the Authorization header. The long-lived API key you were issued (esim_live_...) is never sent on these calls directly — it is only ever used once, to obtain a token.
1.1 Exchange your API key for an access token
/auth/tokenPOST /api/v1/partner/auth/token
Content-Type: application/json
{
"api_key": "esim_live_1a2b3c4d5e6f7g8h..."
}{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer",
"expires_in": 3600
}- Tokens are valid for 1 hour. There is no refresh token — when a token expires, call this endpoint again with your API key to get a new one (standard OAuth2 Client Credentials style flow).
401if the key is invalid, malformed, or has been revoked.- Rate limited to 10 requests/minute per IP.
1.2 Use the token on every other call
GET /api/v1/partner/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...401if the token is missing, malformed, expired, or was issued for a different purpose — a regular customer login token cannot be used here, and this token cannot be used against the regular customer-facing API either.- If your key is revoked by an admin, tokens already issued for it stop working on their very next use — you don't have to wait out the full hour.
Full flow example
TOKEN=$(curl -s -X POST https://triptel.co/api/v1/partner/auth/token \
-H "Content-Type: application/json" \
-d '{"api_key": "esim_live_..."}' | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
curl -s https://triptel.co/api/v1/partner/me \
-H "Authorization: Bearer $TOKEN"2. Endpoints
All request/response bodies are JSON. Every endpoint below requires the Bearer token from §1.2.
/countriesLists all active destination countries. Optional query param search filters by name or ISO2 code.
[
{
"id": 1,
"iso2": "US",
"iso3": "USA",
"name_en": "United States",
"name_bn": null,
"flag_url": "https://...",
"region": null,
"is_popular": true,
"is_active": true
}
]Rate limit: 120/minute.
/countries/{iso2}/productsLists active eSIM data products for a country. iso2 is case-insensitive.
[
{
"id": "a1b2c3d4-...",
"country_id": 1,
"title_en": "1GB / 7 Days",
"title_bn": null,
"data_amount_gb": 1.0,
"validity_days": 7,
"retail_price": 4.50,
"currency": "EUR",
"is_unlimited": false,
"is_active": true,
"coverage_networks": null
}
]404 if the country ISO2 doesn't exist. Rate limit: 120/minute.
/meYour merchant profile, effective commission rate, and current wallet balance.
{
"business_name": "Acme Travel Co",
"commission_rate_pct": 5.00,
"wallet_balance": 142.5000,
"wallet_currency": "EUR",
"commission_balance": 3.2500,
"webhook_configured": true
}wallet_balance is the prepaid balance orders are paid from — check it before charging your own customer. commission_balance is commission earned on completed orders, held separately.
Reflects any active volume-based commission tier, falling back to your flat rate. Rate limit: 120/minute.
/ordersPlaces an order and provisions an eSIM asynchronously. Your merchant wallet is debited the product's retail_price immediately; the order returns in PROCESSING status right away — poll GET /orders/{id} or wait for the webhook (§3) to find out whether it completed.
{
"product_id": "a1b2c3d4-...",
"customer_reference": "your-internal-order-id-123"
}The response is the full order object (see GET /orders/{order_id} below) plus idempotent_replay.
Always send customer_reference — it makes retries safe
- The same
customer_referencewith the sameproduct_idreturns the original order ("idempotent_replay": true) and never charges your wallet again. - The same
customer_referencewith a differentproduct_idreturns409. - Without one, every call is a new purchase — a retried timeout can buy two eSIMs. Use one unique value per eSIM you sell, e.g. your own order ID.
What happens after PROCESSING (usually 10–30 seconds):
- Success — order becomes
COMPLETED, the eSIM fields and install links are filled in, and commission is credited to your account. - Failure — order becomes
FAILED, no eSIM is issued, and the full amount is refunded to your wallet automatically. No commission is paid on a failed order. - If our server restarts mid-order, a recovery job still settles it within minutes — a
PROCESSINGorder always ends asCOMPLETEDorFAILED.
400 insufficient wallet balance (nothing charged) · 404 product not found/inactive · 409 customer_reference reused for another product · rate limit 30/minute.
/orders/{order_id}The current state of one of your orders. Poll every 3–5 seconds while PROCESSING. The webhook sends this same object.
{
"order_id": "e5f6a7b8-...",
"order_no": "PTR-20260913-A1B2C3",
"customer_reference": "your-order-id-123",
"status": "COMPLETED",
"amount": "4.5000",
"currency": "EUR",
"product": {
"id": "a1b2c3d4-...",
"title": "Singapore 1GB 7 Days",
"country_iso2": "SG",
"data_amount_gb": 1.0,
"is_unlimited": false,
"validity_days": 7
},
"iccid": "8965012345678901234",
"lpa_string": "LPA:1$smdp.example.com$ABC123",
"qr_code_data": "LPA:1$smdp.example.com$ABC123",
"smdp_address": "smdp.example.com",
"matching_id": "ABC123",
"install_links": {
"ios": "https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA%3A1%24...",
"android": "https://esimsetup.android.com/esim_qrcode_provisioning?carddata=LPA%3A1%24..."
},
"is_mock_fallback": false,
"created_at": "2026-09-13T10:00:00+00:00",
"updated_at": "2026-09-13T10:00:14+00:00"
}- Encode
qr_code_datainto the QR code your customer scans; showsmdp_addressandmatching_idfor manual entry. install_links.iosworks on iOS 17.4+; Android support is still uneven — always show the QR code too.- For unlimited plans
is_unlimitedis true anddata_amount_gbis 0. - Fields are only ever added, never removed or renamed — ignore ones you don't recognise.
is_mock_fallbackis legacy and always false.
404 if the order doesn't exist or isn't yours. Rate limit: 120/minute.
/ordersOrders you placed through this API, newest first — for reconciliation. Query params: customer_reference, status (PROCESSING, COMPLETED, FAILED), limit (1–100, default 50), offset.
{
"orders": [ { "order_id": "e5f6a7b8-...", "status": "COMPLETED", "...": "..." } ],
"total": 1,
"limit": 50,
"offset": 0
}Rate limit: 120/minute.
/webhook-configSets, rotates, or clears your webhook endpoint. Self-service — no admin involvement needed.
{ "webhook_url": "https://yourapp.example.com/webhooks/esim" }{
"webhook_url": "https://yourapp.example.com/webhooks/esim",
"webhook_secret": "whsec_...",
"warning": "Store this secret securely - it will not be shown again in full."
}A new webhook_secret is generated every time you set or change the URL — save it immediately, it verifies webhook signatures (§3) and is never shown again.
Send {"webhook_url": null} to clear your webhook entirely (you'll only be able to poll after that):
{ "webhook_url": null, "webhook_secret": null }Rate limit: 10/minute.
3. Webhooks
If you have a webhook_url configured, we POST to it once your order reaches a terminal state (COMPLETED or FAILED).
Delivery attempts: one immediately when the order settles; if that fails (non-2xx response, 10-second timeout, connection error), a sweep every 5 minutes retries it, up to 3 attempts total within 24 hours — for both COMPLETED and FAILED orders. Poll GET /orders/{order_id} if you suspect a delivery was missed.
Payload
The body is exactly the order object returned by GET /orders/{order_id} (§2). On a FAILED order the eSIM fields are null — check status first.
Your receiver must
- Verify the signature on the raw body (below) and reject mismatches with
401. - Be idempotent: the same order can arrive more than once, or race your own polling — key on
order_id. - Respond
2xxquickly and do slow work afterwards. - Check the
X-Webhook-Eventheader:order.updatedfor orders,webhook.testfor a signed test ping sent from your Partner Dashboard (body{"event": "webhook.test", ...}) — verify it and return2xxwithout processing.
You can set, rotate, remove and test your webhook, and see every delivery attempt, from your Partner Dashboard.
Signature verification
Every delivery includes an X-Webhook-Signature header: the HMAC-SHA256 of the exact raw request body, hex-encoded, using your webhook_secret.
import hmac, hashlib
def verify_webhook(raw_body: bytes, signature_header: str, webhook_secret: str) -> bool:
expected = hmac.new(webhook_secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header)Always verify against the raw bytes of the request body, before any JSON parsing — re-serializing and re-hashing a parsed-then-reserialized payload is not guaranteed to match byte-for-byte.
4. Rate Limits
| Endpoint | Limit |
|---|---|
POST /auth/token | 10/minute (per IP) |
POST /orders | 30/minute (per API key) |
PUT /webhook-config | 10/minute (per API key) |
| Everything else | 120/minute (per API key) |
A 429 response means you've hit the limit — back off and retry after a short delay. Limits are keyed to your specific API key (via the token), not your IP, except for the token endpoint itself (which has no partner identity yet).
5. Errors
Standard HTTP status codes; error detail is always in the JSON body's detail field:
{ "detail": "Insufficient wallet balance. Please top up." }| Status | Meaning |
|---|---|
| 400 | Bad request — invalid input or a business rule violation |
| 401 | Missing/invalid/expired token, or invalid/revoked API key at the token endpoint |
| 404 | Resource not found (deliberately indistinguishable from "not yours") |
| 429 | Rate limit exceeded |
| 502 | Upstream provisioning failure (rare from your side — provisioning happens after POST /orders already returned) |
6. Getting API Access
Key issuance and revocation are Super-Admin-only actions performed from the internal admin console — there is no self-service signup. If you're a merchant looking to integrate programmatically, contact your account admin once your partner application is approved.
If you suspect your key has leaked, ask for immediate revocation — any access tokens already issued from it stop working on their next use, not just after they naturally expire.
Not a partner yet? Apply to the B2B Reseller Program