---
name: terudo-api
description: Integrate the Terudo partner API server-side to sell travel eSIMs, with trip quotes, commission or net orders, eSIM follow-up, top-ups, webhooks with signature checks and statements. Use when the user works with Terudo's API, a sk_live_/sk_test_ Terudo key, Terudo webhooks (Terudo-Signature), or wants to send eSIM offers by email, SMS, chat or from a back office or mobile app.
---

# Terudo partner API

Base URL `https://terudo.com/api/partner/v1`, JSON, `Authorization: Bearer <key>`. Reference: https://terudo.com/developers/docs/api.md and https://terudo.com/developers/docs/api-reference.md (or the `terudo-docs` MCP server: `api_reference`, `read_doc api`). OpenAPI: https://terudo.com/developers/openapi.json.

## Rules

- Secret keys (`sk_live_…`, `sk_test_…`) only in server code, read from environment secrets (`TERUDO_SECRET_KEY`). Never in a client bundle, a mobile app, logs or the repository.
- Develop with `sk_test_…`: same API, sandbox sales, simulated payment, nothing billed. Do not switch code paths between test and live; only the key changes.
- Net prices (`net_cents`), commissions and credit are the partner's business data: never send them to a traveller-facing page, email or app.
- Every `POST /orders` carries `reference` = the partner's own booking/file id. Same reference → same order (HTTP 200 instead of 201), so retries and double clicks never create a second sale. Without a reference, send an `Idempotency-Key` header.
- Test error `code`, not `message`. Show `message` to staff if useful (it is human-readable, often French).

## Sell for a trip

1. `POST /quotes` `{ destination, start_date, end_date, profile? }` → `options[]`, pick `recommended: true`.
2. Optional honesty checks before selling: `GET /coverage/{destination}` (traveller's French plan may already include it), `GET /compatibility?device=<model>`.
3. `POST /orders`:
   - Commission (default): `{ plan, reference, client: { email, locale }, trip, return_url, metadata }` → `checkout_url` for the traveller (or `notify_client: true` to let Terudo email it). Optional `discount_percent` 0–15 (default 5, from the partner's commission), `currency` for the traveller's currency.
   - Net: add `"kind": "net"`; paid from prepaid credit, delivered now or at `deliver_at`. Handle `402 insufficient_credit`.
4. Follow with webhooks, not polling. `GET /orders/{id_or_reference}` when needed.

Order statuses: `pending`, `paid`, `ordered`, `delivered`, `failed`, `refunded`, `cancelled`, `expired`. Delivered orders have `esim` (`id`, `page_url`, install links; activation code with a secret key).

## Webhooks

- Create with `POST /webhooks` `{ url, events }` (or in the portal); store the returned `secret` (`whsec_…`). Test keys create test endpoints.
- Verify `Terudo-Signature: t=<unix>,v1=<hex>`: HMAC-SHA256 of `"<t>.<raw body>"` with the secret, constant-time compare, reject `t` older than 300 s. Use the RAW body (configure the framework: `express.raw`, `request.get_data()`, `request.raw_post`, `php://input`). Get the exact code: MCP `webhook_verifier`, or `terudo snippet webhook --language <node|python|php|ruby>`.
- Answer 2xx within 10 s, process after. Deduplicate on the event `id` (retries keep it). Body: `{ id, type, created_at, mode, data: { order } }`.
- Useful events: `order.paid`, `order.delivered` (store `esim.id` with the booking), `esim.installed`, `esim.low_data` (offer a top-up), `order.refunded`.
- Local test: `terudo webhooks trigger order.delivered --url http://localhost:3000/<path> --secret whsec_…`, or `POST /webhooks/{id}/test`.

## After the sale

`GET /esims/{id}`, `GET /esims/{id}/usage`, `GET|POST /esims/{id}/topups`, `POST /esims/{id}/resend`, `POST /orders/{id}/cancel` (unpaid link, or refund while not installed), `POST /esims/{id}/suspend|resume` (net). The `esim.id` also feeds the `esim-status` element in the partner's customer area (terudo-elements skill).

## Errors and limits

401 `unauthorized`, 402 `insufficient_credit`, 403 `forbidden`/`demo_key`, 404 `not_found`, 409 `conflict`/`not_refundable`/`not_topupable`/`not_remindable`, 422 `invalid_request`/`unknown_plan`/`invalid_trip`/`plan_too_short`, 429 `rate_limited` (600/min per key: back off and retry), 503 `payments_closed`. Lists: `{ object: "list", data }`, 50 per page, `starting_after=<last id>`.

## Write a small client

Wrap the API in one module: base URL, key from env, JSON in/out, throw an error carrying `status` and `error.code`, retry once on network errors and 429 (respect a pause), always pass `reference` on orders. Keep it free of business rules.

## Verify before finishing

- [ ] Secret key only in server code and env; `.env*` files git-ignored.
- [ ] Orders carry `reference`; retrying the same request is safe.
- [ ] Webhook handler verifies the signature on the raw body, checks the timestamp, dedupes on event id, answers fast.
- [ ] Works end to end with `sk_test_…` (create an order, open `checkout_url`, simulate payment, receive `order.paid` and `order.delivered`).
- [ ] Nothing traveller-facing shows `net_cents` or commissions.
