Triptel
For Developers

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.

Base URLhttps://triptel.co/api/v1/partner
Getting set up is an admin-only, manual step: a Super Admin issues your API key once your merchant application is approved. There is no self-service signup for API access — see §6 below.

1. 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

POST/auth/token
Request
POST /api/v1/partner/auth/token
Content-Type: application/json

{
  "api_key": "esim_live_1a2b3c4d5e6f7g8h..."
}
Response 200
{
  "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).
  • 401 if 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...
  • 401 if 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

bash
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.

GET/countries

Lists all active destination countries. Optional query param search filters by name or ISO2 code.

Response 200
[
  {
    "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.

GET/countries/{iso2}/products

Lists active eSIM data products for a country. iso2 is case-insensitive.

Response 200
[
  {
    "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.

GET/me

Your merchant profile, effective commission rate, and current wallet balance.

Response 200
{
  "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.

POST/orders

Places 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.

Request
{
  "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_reference with the same product_id returns the original order ("idempotent_replay": true) and never charges your wallet again.
  • The same customer_reference with a different product_id returns 409.
  • 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 PROCESSING order always ends as COMPLETED or FAILED.

400 insufficient wallet balance (nothing charged) · 404 product not found/inactive · 409 customer_reference reused for another product · rate limit 30/minute.

GET/orders/{order_id}

The current state of one of your orders. Poll every 3–5 seconds while PROCESSING. The webhook sends this same object.

Response 200
{
  "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_data into the QR code your customer scans; show smdp_address and matching_id for manual entry.
  • install_links.ios works on iOS 17.4+; Android support is still uneven — always show the QR code too.
  • For unlimited plans is_unlimited is true and data_amount_gb is 0.
  • Fields are only ever added, never removed or renamed — ignore ones you don't recognise. is_mock_fallback is legacy and always false.

404 if the order doesn't exist or isn't yours. Rate limit: 120/minute.

GET/orders

Orders you placed through this API, newest first — for reconciliation. Query params: customer_reference, status (PROCESSING, COMPLETED, FAILED), limit (1–100, default 50), offset.

Response 200
{
  "orders": [ { "order_id": "e5f6a7b8-...", "status": "COMPLETED", "...": "..." } ],
  "total": 1,
  "limit": 50,
  "offset": 0
}

Rate limit: 120/minute.

PUT/webhook-config

Sets, rotates, or clears your webhook endpoint. Self-service — no admin involvement needed.

Request (set/rotate)
{ "webhook_url": "https://yourapp.example.com/webhooks/esim" }
Response 200
{
  "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):

Response (clear)
{ "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 2xx quickly and do slow work afterwards.
  • Check the X-Webhook-Event header: order.updated for orders, webhook.test for a signed test ping sent from your Partner Dashboard (body {"event": "webhook.test", ...}) — verify it and return 2xx without 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.

python
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

EndpointLimit
POST /auth/token10/minute (per IP)
POST /orders30/minute (per API key)
PUT /webhook-config10/minute (per API key)
Everything else120/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." }
StatusMeaning
400Bad request — invalid input or a business rule violation
401Missing/invalid/expired token, or invalid/revoked API key at the token endpoint
404Resource not found (deliberately indistinguishable from "not yours")
429Rate limit exceeded
502Upstream 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