# Terudo developer documentation > Travel data eSIMs for travel businesses: Elements for web pages, the partner API, webhooks, test mode and AI tools. Every page is also at https://terudo.com/developers/docs/.md; an open MCP server serves them at https://terudo.com/mcp/. # Terudo for developers Terudo supplies travel data eSIMs to travel businesses (agencies, visa services, tour operators, travel apps), which offer them to their own travellers. Terudo never sells to travellers on its own: every sale belongs to a partner. There are three ways to put an eSIM offer in your traveller's flow. Most partners start with Elements and add the API later. | You want to | Use | Key | | --- | --- | --- | | Show the right plan on a page you already have (booking confirmation, visa approval, trip page, customer account) | [Elements](https://terudo.com/developers/docs/elements.md): one script, five drop-in pieces | Publishable `pk_…` | | Create orders from your back office, deliver eSIMs at your net price, follow usage, receive events | [Partner API](https://terudo.com/developers/docs/api.md) and [webhooks](https://terudo.com/developers/docs/webhooks.md) | Secret `sk_…` | | Let an AI assistant quote and sell, or let your coding agent integrate Terudo | [AI tools](https://terudo.com/developers/docs/ai-tools.md): MCP servers, skills, CLI | None for the docs, `sk_`/`pk_` for the partner MCP | ## Two ways a sale is paid Chosen on each order, never fixed per account. - **Commission** (default). Terudo sells to the traveller, who pays the public price on Terudo's hosted checkout (`checkout_url`). You earn a commission, paid monthly. Your client discount (0 to 15 %, default 5 %) comes out of your commission. This is what Elements do. - **Net price**. You buy the eSIM at your net price from your prepaid credit, it is delivered at once (or on a date), and you invoice your client yourself. API only, secret key only. Net prices, commissions and credit are your business data. They never appear with a publishable key, in Elements or in any public page. Never show them to a traveller. ## Keys Keys are created in the partner portal (https://terudo.com/portal/developers/) once Terudo has opened the API for your account. | Prefix | Where it lives | What it reaches | | --- | --- | --- | | `sk_live_…` | Your server only | The whole partner API, real sales | | `sk_test_…` | Your server only | The whole API in the sandbox: nothing is charged or billed | | `pk_live_…` | Your web pages | Public prices, quotes, checks and a payment link on Terudo's checkout | | `pk_test_…` | Your web pages | Same, sandbox: the pay page simulates the payment | A publishable key can list the sites (origins) allowed to use it. With a list, browsers on other sites are refused, and a `return_url` coming from the page is kept only on those sites. Without a list, the key works from any page but no return address is taken from the page. The public demo key `pk_test_terudo_demo_public` needs no account: real catalogue, prices, quotes and checks, payment links disabled. Use it to prototype. ## Base URLs - Partner API: `https://terudo.com/api/partner/v1` - Elements script: `https://terudo.com/elements/v1.js` - Partner MCP server (your keys): `https://terudo.com/api/partner/v1/mcp/` - Docs MCP server (open, no key): `https://terudo.com/mcp/` - OpenAPI: `https://terudo.com/developers/openapi.json` ## Pages of this documentation - [Quickstart](https://terudo.com/developers/docs/quickstart.md): an eSIM offer on your confirmation page in five minutes. - [Elements](https://terudo.com/developers/docs/elements.md): every element, option, callback and style. - [Put Terudo in your flow](https://terudo.com/developers/docs/integrate-in-your-flow.md): recipes per page type and framework. - [Partner API](https://terudo.com/developers/docs/api.md): authentication, quotes, orders, idempotency, errors. - [API reference](https://terudo.com/developers/docs/api-reference.md): every route, generated from the live definition. - [Webhooks](https://terudo.com/developers/docs/webhooks.md): events, payload, signature checks in several languages. - [Testing and going live](https://terudo.com/developers/docs/testing.md): test keys, sandbox, checklist. - [AI tools](https://terudo.com/developers/docs/ai-tools.md): MCP servers, agent skills, CLI, llms.txt. - [CLI](https://terudo.com/developers/docs/cli.md): `terudo` on the command line. Every page is plain Markdown at its `.md` address. All of them in one file: https://terudo.com/llms-full.txt --- # Quickstart Goal: on the page a traveller sees after booking (or after their visa is approved), show the eSIM plan that fits their trip, with a button that takes them to Terudo's checkout. No backend change. ## 1. Add the script Anywhere in the page, once: ```html ``` `pk_test_terudo_demo_public` is Terudo's public demo key: real catalogue and prices, payment links disabled. Replace it with your own `pk_test_…` key to test the full payment, then your `pk_live_…` key. ## 2. Place the element Where the offer should appear, with the trip your page already knows: ```html
``` - `data-destination`: ISO country code (`MA`, `JP`, `US`) or a region (`EU-35`). - `data-start`, `data-end`: arrival and departure dates, `YYYY-MM-DD`. - `data-email`: optional; without it, the element asks the traveller. - `data-return-url`: where Terudo's checkout sends the traveller once paid. Kept only if its site is listed on your publishable key. The banner shows one recommended plan sized to the trip (one buffer day for a late flight). A click opens the full trip offer, then an email field and the checkout. ## 3. Match your look Elements inherit your font. Set your colour and corners: ```html ``` ## 4. Test a payment With your own `pk_test_…` key, click through: the pay page simulates the payment, the eSIM is a sandbox one, nothing is charged. With a secret test key you can follow the order through the API or webhooks. ## 5. Go live 1. In the portal (Développeurs), create a `pk_live_…` key and list the sites that will use it (`https://www.your-agency.example`). 2. Swap the key in the script tag. 3. If you set a Content Security Policy, allow `script-src https://terudo.com`, `connect-src https://terudo.com` and `img-src https://terudo.com`. Next: [Elements reference](https://terudo.com/developers/docs/elements.md), [recipes for your flow](https://terudo.com/developers/docs/integrate-in-your-flow.md). --- # Elements Elements are pieces of Terudo's eSIM shop that render inside your own page: no iframe, your font, your colours. The traveller chooses a plan in your page and pays on Terudo's hosted checkout, where Terudo is the seller (commission model). One script, one publishable key. ## Load the script ```html ``` Script attributes: | Attribute | Meaning | | --- | --- | | `data-key` | Your publishable key (`pk_live_…`, `pk_test_…`, or the demo key `pk_test_terudo_demo_public`). With it, the script creates `window.terudo` and mounts every element marked `data-terudo` once the page is ready. | | `data-locale` | `fr`, `en`, `de`, `es`, `it`, `pt`, `zh`, `ar` or `hi`. Default: the page's ``, then `fr`. | | `data-session` | A `client_token` from an element session (see Sessions). | Never put a secret key (`sk_…`) in a page. ## Two ways to mount **Declarative**: any element with `data-terudo=""` and its options as `data-*` attributes (camelCase options become kebab-case: `returnUrl` → `data-return-url`, `esimId` → `data-esim-id`, `plansInto` → `data-plans-into`). ```html
``` Elements added to the page later (single-page apps) are mounted by calling `terudo.autoMount()` (or `terudo.autoMount(containerElement)`). An element is mounted once; it carries `data-terudo-mounted` afterwards. **JavaScript**: create an instance and mount where you want. Callbacks are only available this way. ```js const terudo = Terudo({ key: 'pk_live_…', locale: 'en', session: clientToken, // optional, from POST /element_sessions appearance: { color: '#0a5b8a', onColor: '#ffffff', radius: '10px', font: 'inherit' }, }); const offer = terudo.mount('trip-offer', '#esim-offer', { destination: 'MA', start: '2026-12-03', end: '2026-12-09', profile: 'normal', email: 'marie@example.com', returnUrl: 'https://www.your-agency.example/bookings/48213', onCheckout(order) { analytics.track('esim_checkout', { order: order.order_id }); }, onError(error) { console.warn(error.code, error.message); }, }); offer.update({ start: '2026-12-04' }); // re-renders with new options ``` `Terudo(config)` returns `{ mount(name, target, options), autoMount(scope?), api(path, options), locale, key, origin, elements }`. `target` is a CSS selector or a DOM element; `mount` empties it first. `mount` returns `{ host, update(options) }`. ## The elements ### destination-picker The destinations of the catalogue, each with its "from" price. | Option | Type | | | --- | --- | --- | | `title` | string | Heading. Default "Choose your destination". | | `onSelect(code, plan)` | function | Called when a destination is picked, with its code and cheapest plan. | | `plansInto` | selector or element | Mounts `plans` for the picked destination there. | ### plans Every plan of one destination, cheapest first, each with its price and a button that asks the traveller's email and opens the checkout. | Option | Type | | | --- | --- | --- | | `destination` | string, required | ISO code (`MA`) or region (`EU-35`). | | `days` | integer 1–30 | Adds daily plans of that length. | | `email`, `returnUrl`, `onSelect(plan)`, `onCheckout(order)`, `onError(error)` | | See Checkout and Callbacks. | ### trip-offer Destination and dates in, the recommended plan out (the cheapest that covers the stay plus one buffer day), the other plans a click away. The piece for a visa approval, an itinerary or a trip page. | Option | Type | | | --- | --- | --- | | `destination` | string, required | | | `start`, `end` | `YYYY-MM-DD`, required | Arrival and departure. 60 days at most. | | `profile` | `light` \| `normal` \| `heavy` | Estimated use: 0.5, 1 or 2 GB a day. Default `normal`. | | `title` | string | Replaces "Your trip: dates". | | `email`, `returnUrl`, `onSelect`, `onCheckout`, `onError` | | | ### booking-banner One line for a booking confirmation: the recommended plan and its price, with a button that opens into the full `trip-offer` in place. | Option | Type | | | --- | --- | --- | | `destination`, `start`, `end`, `profile` | | As `trip-offer`. | | `title`, `subtitle`, `cta` | string | Override the banner's texts and the button label. | | `onOpen(quote)` | function | Called when the traveller opens the offer. | | `email`, `returnUrl`, `onCheckout`, `onError` | | | ### esim-status The traveller's eSIM inside your own customer area: state, data used, install buttons while not installed, validity, link to Terudo's full eSIM page. | Option | Type | | | --- | --- | --- | | `esimId` | string, required | The `esim.id` of the order (API) or the token in the traveller's eSIM page link. | | `pageUrl` | `false` | Hides the link to the full page. | | `onLoad(esim)` | function | Called with the eSIM data (no activation code: that stays server-side). | States shown: `preparing`, `not_installed`, `active`, `used_up`, `ended`, `suspended`, `deleted`, `refunded`. ## Checkout The traveller never pays in your page. After choosing a plan they give their email (unless you passed `email`), the element creates a commission order (`POST /public/checkout`) and sends the browser to `checkout_url` on terudo.com. There the traveller accepts Terudo's terms and pays in their currency; the eSIM arrives by email right after payment. Then they are sent to `returnUrl`: - kept when its origin is one of the sites listed on your publishable key, or when it comes from an element session; - otherwise ignored, and the traveller stays on Terudo's confirmation page. `onCheckout(order)` runs just before the redirect, with `{ object: "checkout", order_id, checkout_url, status }`. Return `false` to stop the redirect and handle `order.checkout_url` yourself (open it in a new tab, a webview, …). Checkout from a page is limited to 30 orders an hour per IP address. ## Callbacks | Callback | Elements | Called with | | --- | --- | --- | | `onSelect` | destination-picker: `(code, plan)`; plans, trip-offer: `(plan)` | The choice | | `onCheckout` | plans, trip-offer, booking-banner | The order, before redirecting. `false` stops the redirect. | | `onOpen` | booking-banner | The quote | | `onLoad` | esim-status | The eSIM | | `onError` | all | An `Error` with `message` (shown to the traveller too), `code` and `status` | ## Styling Elements inherit the page's font and render in a `.tdo` container. Set the brand with `appearance` or CSS variables on the host element: | Variable | `appearance` key | | | --- | --- | --- | | `--tdo-color` | `color` | Buttons, selected tiles, links | | `--tdo-on` | `onColor` | Text on the colour | | `--tdo-radius` | `radius` | Corners | | `--tdo-font` | `font` | Font family (default: inherit) | Elements are responsive and accessible (buttons, ARIA states, progress bar for usage). Test keys show a small "Test" badge. ## Sessions Without a session, an element shows public prices to anyone and asks the traveller for their email. An **element session**, created by your server with your secret key, signs what the page may not decide on its own: ```bash curl -X POST https://terudo.com/api/partner/v1/element_sessions \ -H "Authorization: Bearer sk_live_…" -H "Content-Type: application/json" \ -d '{ "discount_percent": 10, "reference": "visa-48213", "client_email": "marie@example.com", "trip": { "destination": "MA", "start_date": "2026-12-03", "end_date": "2026-12-09", "profile": "normal" }, "return_url": "https://www.your-agency.example/bookings/48213", "locale": "fr", "metadata": { "booking_id": "48213" } }' # → { "object": "element_session", "client_token": "…", "expires_at": "…", "session": { … } } ``` - `discount_percent`: client discount 0–15 (default 5), taken from your commission. - `reference`: your booking or file id; the order carries it, and webhooks with it. - `client_email`, `return_url`, `locale`, `trip`, `metadata`: prefilled and trusted. - Your member rate applies when your account has it. Pass `client_token` as `data-session` on the script or `session` in `Terudo({...})`. It lasts two hours: create it when you render the page. ## Test mode `pk_test_…` keys work exactly like live ones but create sandbox sales: the pay page simulates the payment, the eSIM is not a real one, nothing is billed. The demo key `pk_test_terudo_demo_public` shows real data but refuses checkout (`demo_key`). See [Testing](https://terudo.com/developers/docs/testing.md). ## Content Security Policy Allow `script-src https://terudo.com`, `connect-src https://terudo.com` and `img-src https://terudo.com`. ## Errors A failed element shows a short message in place (in the element's language) and calls `onError`. Common codes: `unauthorized` (key unknown or revoked), `forbidden` (page origin not listed on the key), `invalid_trip` (dates), `unknown_plan`, `demo_key` (checkout with the demo key), `rate_limited`. ## Raw calls `terudo.api(path, { method, query, body })` calls the public routes with your key (and session): `destinations`, `plans`, `quotes`, `checkout`, `esims/`. It returns a promise of the JSON. Useful to build your own interface on Terudo's data; the checkout still happens on terudo.com. --- # Put Terudo in your flow The best moment to offer an eSIM is when the traveller's trip becomes real: the booking is confirmed, the visa is approved, the itinerary is sent. You already know the destination and the dates; pass them, and Terudo sizes the plan to the trip. ## Pick the element | Page in your flow | Element | Why | | --- | --- | --- | | Booking or payment confirmation | `booking-banner` | One discreet line, opens into the full offer | | Visa approved, itinerary, trip page | `trip-offer` | The recommended plan, first thing seen | | Destination or country guide | `plans` (with `days` if you know the length) | Every plan of the country | | Travel shop, "eSIM" menu entry | `destination-picker` + `plansInto` | Browse all destinations | | Customer area, "My trip" | `esim-status` | The eSIM bought, its usage, install buttons | | Email, SMS, chat, mobile app | API: quote then order, send `checkout_url` | No JavaScript there | ## Booking or visa confirmation (server-rendered page) Render the trip you know, and an element session so your discount, reference and return page are trusted: ```js // Server (Node), when rendering the confirmation page const res = await fetch('https://terudo.com/api/partner/v1/element_sessions', { method: 'POST', headers: { Authorization: `Bearer ${process.env.TERUDO_SECRET_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ reference: booking.id, client_email: booking.email, discount_percent: 5, trip: { destination: booking.countryCode, start_date: booking.arrival, end_date: booking.departure }, return_url: `https://www.your-agency.example/bookings/${booking.id}`, locale: booking.language, }), }); const { client_token } = await res.json(); ``` ```html
``` If the session cannot be created (network), render the element without it: it still works at public prices and asks for the email. ## React ```jsx import { useEffect, useRef } from 'react'; function loadTerudo() { if (window.Terudo) return Promise.resolve(window.Terudo); return new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = 'https://terudo.com/elements/v1.js'; s.onload = () => resolve(window.Terudo); s.onerror = reject; document.head.appendChild(s); }); } export function TripOffer({ destination, start, end, email, session, onCheckout }) { const ref = useRef(null); useEffect(() => { let cancelled = false; loadTerudo().then((Terudo) => { if (cancelled || !ref.current) return; const terudo = Terudo({ key: process.env.NEXT_PUBLIC_TERUDO_PK, session, locale: 'en' }); terudo.mount('trip-offer', ref.current, { destination, start, end, email, onCheckout }); }); return () => { cancelled = true; }; }, [destination, start, end, email, session, onCheckout]); return
; } ``` Load the script without `data-key` when you mount from JavaScript, so nothing mounts twice. ## Next.js (App Router) Create the session in a Server Component and pass the token to a Client Component that mounts the element (as above, marked `'use client'`). The publishable key can be `NEXT_PUBLIC_…`; the secret key stays in server code only. Use `next/script` with `strategy="afterInteractive"` if you prefer it to the loader above. ```tsx // app/bookings/[id]/page.tsx (Server Component) const session = await fetch('https://terudo.com/api/partner/v1/element_sessions', { method: 'POST', headers: { Authorization: `Bearer ${process.env.TERUDO_SECRET_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ reference: booking.id, client_email: booking.email, trip: { destination: booking.country, start_date: booking.from, end_date: booking.to } }), cache: 'no-store', }).then((r) => r.json()); return ; ``` ## Vue 3 ```vue ``` ## Angular Load the script once (in `index.html` without `data-key`), then in a component: `ngAfterViewInit() { this.offer = (window as any).Terudo({ key: environment.terudoPk }).mount('trip-offer', this.host.nativeElement, { destination, start, end }); }` and call `this.offer.update({...})` in `ngOnChanges`. ## Single-page apps with the declarative tags When you navigate without reloading, call `window.terudo.autoMount()` after the new view is in the DOM. Already mounted elements are skipped. ## WordPress, Webflow, Wix, Shopify (no code) Paste the script tag in the site's footer code (once), then an HTML block with the `
` where the offer goes. List the site in your publishable key's sites. For a page per destination, use `plans` with that destination. ## Customer area Store `order.esim.id` (from the API or the `order.delivered` webhook) with the booking, then: ```html
``` ## Emails, SMS, chat, mobile apps No JavaScript runs there. From your server: 1. `POST /quotes` with destination and dates: take the option with `recommended: true`. 2. `POST /orders` with that plan, the traveller's email, your `reference` and `return_url`. The answer has `checkout_url`. 3. Put `checkout_url` in your email, SMS or button. Or pass `notify_client: true` and Terudo emails the payment link itself, in the traveller's language. Reusing the same `reference` returns the same order, so sending the email twice never makes two orders. In a mobile app, open `checkout_url` in the system browser or an in-app browser (not a bare webview), and use a `return_url` your app can catch (universal link). ## Before selling: tell the truth Two free checks avoid selling a useless eSIM: - `GET /coverage/{destination}` (any key): which main French mobile plans already include the destination. - `GET /compatibility?device=iPhone 13 mini` (any key): whether the phone takes an eSIM. ## Net price instead of commission If you sell the eSIM inside your own package or invoice it yourself, order with `"kind": "net"` from your server: it is paid from your prepaid credit, delivered at once (or `deliver_at`), and you get `esim.page_url` and the install links to show in your own pages. See [Partner API](https://terudo.com/developers/docs/api.md). --- # Partner API REST, JSON, one base URL: `https://terudo.com/api/partner/v1`. Every call carries a key: ```bash curl https://terudo.com/api/partner/v1/me -H "Authorization: Bearer sk_test_…" ``` Use secret keys (`sk_…`) from your server only. A publishable key (`pk_…`) reaches only the public routes Elements use. The full list of routes is in the [API reference](https://terudo.com/developers/docs/api-reference.md); an OpenAPI file is at https://terudo.com/developers/openapi.json. ## The usual flow 1. **Quote** the trip: `POST /quotes` with `destination`, `start_date`, `end_date` (and `profile`: `light`, `normal`, `heavy`). The answer lists the plans that cover the stay; one has `recommended: true`. 2. **Check** if needed: `GET /coverage/{destination}` (is the traveller's French plan enough?) and `GET /compatibility?device=…` (does the phone take an eSIM?). 3. **Order**: `POST /orders` with the plan id, the client's email and your `reference`. 4. **Follow**: webhooks (`order.paid`, `order.delivered`, `esim.installed`, `esim.low_data`, …) or `GET /orders/{id}`. 5. **After the sale**: usage, top-ups, resending the install email, cancelling or refunding an eSIM never installed. ## Plans - Fixed plans from the grid: `MA-10` is Morocco, 10 GB, 30 days. - Daily plans, built for a number of days: `MA-DAY2-8D` is Morocco, 2 GB a day for 8 days, then reduced speed. `GET /plans?destination=MA&days=8` lists both at your rate: public price, default client price, your net price and commission. `GET /destinations` lists the codes (countries, and regions such as `EU-35`). A plan id is passed as is to `POST /orders`. ## Orders: commission or net ```bash curl -X POST https://terudo.com/api/partner/v1/orders \ -H "Authorization: Bearer sk_test_…" -H "Content-Type: application/json" \ -d '{ "plan": "MA-DAY2-8D", "kind": "commission", "reference": "visa-48213", "client": { "email": "marie@example.com", "locale": "fr" }, "trip": { "start_date": "2026-12-03", "end_date": "2026-12-09" }, "return_url": "https://www.your-agency.example/bookings/48213", "metadata": { "booking_id": "48213" } }' ``` - **`kind: "commission"`** (default): the order is `pending` with a `checkout_url`. Show it to the traveller, or pass `notify_client: true` and Terudo emails it. `discount_percent` (0–15, default 5) is the client discount, taken from your commission. `currency` lets the traveller pay in USD, GBP, CHF, CAD, AUD, JPY, AED, SAR, CNY, INR, BRL or MXN at Terudo's own rounded price; amounts in your statements stay in euros. `POST /orders/{id}/remind` emails the payment link again (two reminders at most, 24 h apart). - **`kind: "net"`**: paid from your prepaid credit at your net price, delivered at once, or at `deliver_at` (ISO datetime, 90 days at most), or `days_before_trip` with `trip`. A daily plan must be installed within 30 days: sold more than 25 days before the trip, it is delivered 7 days before departure unless you say otherwise. `auto_topup_max` (0–10) adds the same plan from your credit when data runs low. Insufficient credit answers `402 insufficient_credit`. Order statuses: `pending`, `paid`, `ordered`, `delivered`, `failed`, `refunded`, `cancelled`, `expired` (an unpaid link that lapsed). Once delivered, the order carries an `esim` object: `id`, `status`, install links, `page_url`, `qr_url`, activation code (secret key only). ## Idempotency Give every order a `reference` (your booking or file id, 120 characters at most, unique per account). Creating an order with a reference that exists returns that order (`200`) instead of a duplicate (`201`). Without a reference in the body, an `Idempotency-Key` header plays the same role. Retry safely on timeouts. `GET /orders/{id}` accepts the Terudo id or your reference. ## eSIMs after the sale | Route | | | --- | --- | | `GET /esims/{id}` | Status, installation, validity, usage, activation code, iOS and Android install links, QR code | | `GET /esims/{id}/usage` | Data used and total, refreshed at most every 10 minutes | | `GET /esims/{id}/topups` then `POST /esims/{id}/topups` | Add data or days to the same eSIM. Net: from your credit. Commission: a new payment link for the traveller, returned as a new order. | | `POST /esims/{id}/resend` | Emails the install instructions again (three a day) | | `POST /esims/{id}/suspend`, `/resume` | Net orders: cut or restore data (lost phone, traveller gone) | | `POST /orders/{id}/cancel` | Cancels an unpaid link, or refunds an eSIM never installed (to your credit in net, to the traveller's card in commission) | The eSIM `id` is also the token of the traveller's "My eSIM" page (`page_url`) and the `esimId` of the `esim-status` element. ## Lists and pagination Lists answer `{ "object": "list", "data": [ … ] }`, newest first, 50 a page. Pass `starting_after=` for the next page. `GET /orders` filters on `reference`, `status` and `email`. ## Errors Errors answer `{ "error": { "code": "…", "message": "…" } }` with an HTTP status: | Status | Codes | | | --- | --- | --- | | 401 | `unauthorized` | Key missing, revoked or unknown | | 402 | `insufficient_credit` | Credit too low for a net order | | 403 | `forbidden`, `demo_key` | Wrong key kind, origin not listed, or checkout with the demo key | | 404 | `not_found` | Unknown, or another account's | | 409 | `conflict`, `not_refundable`, `not_topupable`, `not_remindable` | The order's state no longer allows it | | 422 | `invalid_request`, `unknown_plan`, `invalid_trip`, `plan_too_short` | A parameter was refused; the message says which | | 429 | `rate_limited` | 600 requests a minute per key | | 503 | `payments_closed` | Live payments not open yet | Messages are written to be shown to your staff; `code` is what your code should test. ## Statements `GET /statements` gives, per month, the sales, the commissions due and the net purchases, with the payout once made; `GET /statements/{YYYY-MM}` lists the month's paid sales. ## Test mode `sk_test_…` keys reach the same API in the sandbox: orders are test sales, the pay page simulates the payment, eSIMs are not real, nothing is billed, statements stay empty. Webhooks have their own test endpoints. See [Testing](https://terudo.com/developers/docs/testing.md). --- # API reference Base URL: `https://terudo.com/api/partner/v1`. Every call: `Authorization: Bearer `. JSON in and out. OpenAPI 3.1: https://terudo.com/developers/openapi.json ## Account ### GET /me Your account. Key: secret key. The key’s account, its mode, whether the member rate is on, and the prepaid credit left for net orders. Response: ```json { "object": "account", "id": 12, "name": "SimpleVisa", "mode": "live", "member_rate": false, "payments_open": true, "credit_cents": 48200, "currency": "EUR" } ``` ## Catalogue Two plan families. Fixed plans from the grid (“MA-10”: Morocco, 10 GB, 30 days). Daily plans, built on demand for a number of days (“MA-DAY2-8D”: Morocco, 2 GB a day for 8 days, then reduced speed). A plan id is passed as is when ordering. ### GET /destinations Destinations. Key: secret key. The destinations of the catalogue, with their fixed plan ids and whether daily plans exist. Response: ```json { "object": "list", "data": [ { "code": "MA", "name": "Maroc", "name_en": "Morocco", "fixed_plans": ["MA-3", "MA-10"], "daily_plans": true }, … ] } ``` ### GET /plans Plans. Key: secret key. Plans at your rate: recommended public price, default client price, net price and commission. With destination and days, daily plans of that length are added. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `destination` | string | no | ISO code (MA) or region (EU-35). Omitted: the whole grid. | | `days` | integer 1–30 | no | Length of the daily plans to add. | | `currency` | USD \| GBP \| CHF \| CAD \| AUD \| JPY \| AED \| SAR \| CNY \| INR \| BRL \| MXN | no | Adds local_price: the client price in that currency, in minor units (whole yen). Our own rounded price, fixed until rates are next set. Every other amount stays in euros. | Response: ```json { "object": "list", "data": [ { "id": "MA-10", "object": "plan", "kind": "fixed", "destination": "MA", "name": "Maroc", "gb": 10, "days": 30, "public_price_cents": 2999, "client_price_cents": 2849, "net_cents": 1780, "commission_cents": 594 }, { "id": "MA-DAY2-7D", "kind": "daily", "gb": 2, "days": 7, "speed_after_quota": "1 Mbit/s", … } ] } ``` ### GET /plans/{id} One plan. Key: secret key. One plan by id, fixed or daily. ### POST /quotes Quote for a trip. Key: secret key. Give the destination and dates: the answer lists the plans that cover the stay and recommends one, the cheapest that covers it all, with a buffer day for a late flight. Nothing is created. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `destination` | string | yes | Destination code. | | `start_date` | date | yes | Arrival, YYYY-MM-DD. | | `end_date` | date | yes | Departure, YYYY-MM-DD. 60 days at most. | | `profile` | light \| normal \| heavy | no | Estimated use: 0.5, 1 or 2 GB a day. Default normal. | | `buffer_days` | integer 0–3 | no | Days added to the covered length. Default 1. | | `currency` | USD \| GBP \| CHF \| CAD \| AUD \| JPY \| AED \| SAR \| CNY \| INR \| BRL \| MXN | no | Adds local_price to each option, the client price in that currency. | Request: ```json { "destination": "MA", "start_date": "2026-12-03", "end_date": "2026-12-09", "profile": "normal" } ``` Response: ```json { "object": "quote", "trip_days": 7, "covered_days": 8, "needed_gb": 7, "options": [ { "id": "MA-DAY1-8D", "kind": "daily", "gb": 1, "days": 8, "public_price_cents": 2099, "net_cents": 1246, "covers_trip": true, "recommended": true, "reason": "1 Go par jour pendant 8 jours, puis débit réduit" }, { "id": "MA-10", "kind": "fixed", "gb": 10, "days": 30, "public_price_cents": 2999, "covers_trip": true, "recommended": false, "reason": "10 Go sur 30 jours, couvre 7 Go estimés" }, … ] } ``` ### GET /coverage/{destination} Does the traveller’s plan already cover it?. Key: any key. Which main French mobile plans already include the destination, from our verified records. With brand and plan, checks one plan. To tell the client the truth before selling. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `brand` | string | no | The traveller’s plan brand (Free, Sosh, …). | | `plan` | string | no | Plan name. | Response: ```json { "object": "coverage", "destination": "MA", "french_plans_including": [], "checked_plan": null, "advice": "Aucun forfait français principal n'inclut cette destination : une eSIM est le bon réflexe." } ``` ### GET /compatibility Does the phone take an eSIM?. Key: any key. From the model as the traveller gives it: yes, depends (some versions only, the note says which), no or unknown (model not in our list, the note describes the brand’s range). The two checks to do on the phone always come along: *#06# for the EID, phone not carrier-locked. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `device` | string | yes | Model: iPhone 13 mini, Galaxy S23, Pixel 7a… | Response: ```json { "object": "compatibility", "device": "iPhone 13 mini", "brand": "Apple", "compatible": "yes", "note": "Sauf modèle acheté en Chine continentale…", "checks": ["Composer *#06# : si un numéro EID s'affiche, le téléphone prend une eSIM.", "Le téléphone doit être désimlocké…"] } ``` ## Orders Two models, chosen on every order. Commission: Terudo sells to the client, who pays on our page (checkout_url) and accepts our terms; you earn the commission. Net: you buy at the net price from your prepaid credit, the eSIM is delivered at once and you invoice your client yourself. An order with a reference is idempotent: the same reference returns the same order. ### POST /orders Create an order. Key: secret key. Creates the order. In commission mode, returns a checkout_url to show the client (or notify_client to have us mail the link). In net mode, debits the credit and starts delivery, now or at deliver_at. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `plan` | string | yes | Plan id, fixed or daily. | | `kind` | commission \| net | no | Default commission. | | `client.email` | string | yes | The traveller. The eSIM goes to this address. | | `client.locale` | fr \| en \| de \| es \| it \| pt \| zh \| ar \| hi | no | Language of the emails, the pay page and the eSIM page. A browser tag works (de-AT). Default: your shop’s language, else fr. The client can change it on their pages. | | `reference` | string ≤ 120 | no | Your id (file, booking). Unique per account, idempotency key: the same reference returns the same order (200). An Idempotency-Key header stands in for it when the body has none. | | `trip.start_date, trip.end_date` | date | no | Trip dates. Refused if the plan is too short. Drives reminders. | | `discount_percent` | integer 0–15 | no | Commission only. Client discount taken from your commission. Default 5. | | `currency` | EUR \| USD \| GBP \| CHF \| CAD \| AUD \| JPY \| AED \| SAR \| CNY \| INR \| BRL \| MXN | no | Commission only. The currency the client pays in, at our rounded price in it; they can change it on the pay page. The response gives charge (currency and amount in minor units); price_cents, the commission and your statements stay in euros. Default EUR. Net orders are always EUR. | | `return_url` | https URL | no | Where the pay page sends the client once paid. | | `notify_client` | boolean | no | Commission: we email the payment link. Default false. | | `deliver_at` | datetime ISO 8601 | no | Deferred delivery, 90 days at most. Or days_before_trip (integer) with trip. A daily plan must be installed within 30 days: sold more than 25 days before the trip, it is delivered 7 days before departure unless told otherwise. | | `auto_topup_max` | integer 0–10 | no | Net only: dynamic plan. When data runs low, the same plan is added from your credit, at most this many times. | | `metadata` | object ≤ 20 clés | no | Your data, returned as is. | Request: ```json { "plan": "MA-DAY2-8D", "kind": "commission", "reference": "visa-48213", "client": { "email": "marie@example.com", "locale": "fr" }, "trip": { "start_date": "2026-12-03", "end_date": "2026-12-09" }, "return_url": "https://partner.example/trips/48213", "metadata": { "trip_id": "48213" } } ``` Response: ```json { "id": "6b1f…", "object": "order", "reference": "visa-48213", "mode": "live", "kind": "commission", "status": "pending", "plan": { "id": "MA-DAY2-8D", "kind": "daily", "destination": "MA", "name": "Maroc", "gb": 2, "days": 8 }, "client": { "email": "marie@example.com", "locale": "fr" }, "trip": { "start_date": "2026-12-03", "end_date": "2026-12-09" }, "price_cents": 2944, "currency": "EUR", "discount_percent": 5, "net_cents": 1840, "commission_cents": 613, "checkout_url": "https://terudo.com/pay/…/", "return_url": "https://partner.example/trips/48213", "deliver_at": null, "auto_topup_max": 0, "metadata": { "trip_id": "48213" }, "created_at": "2026-11-08T10:12:00Z", "paid_at": null, "esim": null } ``` ### GET /orders List. Key: secret key. Your orders, newest first, 50 a page (starting_after = last id). Filters: reference, status, email. ### GET /orders/{id} One order. Key: secret key. By Terudo id or by your reference. Statuses: pending, paid, ordered, delivered, failed, refunded, cancelled, expired. The esim object appears once delivered. ### POST /orders/{id}/cancel Cancel. Key: secret key. An unpaid link is cancelled. A paid order is refunded while the eSIM was never installed: to your credit in net mode, to the client’s card in commission mode. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `reason` | string | no | Reason, kept in the log. | ### POST /orders/{id}/remind Remind the client. Key: secret key. Mails the payment link again (commission, two reminders at most, 24 h apart). ## eSIMs An eSIM’s id is that of its order’s esim object; it is also the token of the client’s “My eSIM” page (page_url), which you may show in your own space. ### GET /esims/{id} One eSIM. Key: secret key. Status, installation, usage, activation code, iOS and Android install links, QR code. Response: ```json { "id": "k8Zs…", // esim id, also the token of the client's page "object": "esim", "status": "active", "profile_status": "enabled", "installed_at": "2026-12-02T18:04:11Z", "expires_at": "2026-12-11T00:00:00Z", "used_bytes": 1431655765, "total_bytes": 17179869184, "iccid": "8944…", "activation_code": "LPA:1$…$…", "smdp_address": "…", "matching_id": "…", "ios_install_url": "https://esimsetup.apple.com/…", "android_install_url": "https://esimsetup.android.com/…", "page_url": "https://terudo.com/esim/k8Zs…/", "qr_url": "https://terudo.com/api/esims/k8Zs…/qr/", "topups": 0 } ``` ### GET /esims/{id}/usage Usage. Key: secret key. Used and total data, refreshed from the supplier at most every 10 minutes. ### GET /esims/{id}/topups Available top-ups. Key: secret key. The same plan again and, for a daily plan, extra days (1, 3, 7), on the same eSIM when the supplier allows it. ### POST /esims/{id}/topups Top up. Key: secret key. Net: the top-up is taken from the credit and added to the eSIM. Commission: a payment link opens for the client and comes back as a new order. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `plan` | string | no | A plan of the same destination. Default: the eSIM’s. | ### POST /esims/{id}/resend Resend the instructions. Key: secret key. Mails the install e-mail (QR code, install link) again to the traveller’s address on the order: lost e-mail, new phone. Delivered eSIMs only, three times a day at most. ### POST /esims/{id}/suspend Suspend. Key: secret key. Cuts the data of an eSIM bought at the net price (traveller gone, lost device). Nothing is refunded, the plan keeps running. ### POST /esims/{id}/resume Resume. Key: secret key. Restores the data. ## Webhooks We call your https address at every step. Each call carries Terudo-Signature: t=,v1=. Verify the signature and reject a timestamp older than 5 minutes. Failures are retried after 1, 5, 30, 120 and 720 minutes; 25 failures in a row switch the endpoint off, to be re-enabled from the portal or the API. Body: { id, type, created_at, mode, data: { order } }. ### GET /webhooks List. Key: secret key. Your endpoints in the key’s mode, and the list of events. ### POST /webhooks Create. Key: secret key. public https url (port 443) and events (empty: all). The secret is returned. Request: ```json { "url": "https://partner.example/hooks/terudo", "events": ["order.paid", "order.delivered", "esim.installed", "esim.low_data"] } ``` Response: ```json { "id": 3, "object": "webhook_endpoint", "url": "https://partner.example/hooks/terudo", "mode": "live", "events": [ … ], "secret": "whsec_…" } ``` ### PATCH /webhooks/{id} Update. Key: secret key. url, events, enabled. ### DELETE /webhooks/{id} Delete. Key: secret key. ### POST /webhooks/{id}/test Send a test. Key: secret key. A signed test event, right away. ### GET /webhooks/{id}/deliveries Deliveries. Key: secret key. The last 100 deliveries, with their state and your server’s answer. ### POST /webhooks/{id}/deliveries/{delivery_id}/resend Resend. Key: secret key. ## Statements ### GET /statements By month. Key: secret key. Sales, commissions due and net purchases per month, with the payout once made. ### GET /statements/{month} One month. Key: secret key. The paid sales of the month (YYYY-MM). ## Elements: the publishable key These routes serve the Elements in your pages, with a pk_… key and CORS open to the key’s sites. Never a net price. An element_session created by your server with the secret key unlocks the client discount, the member rate, and a prefilled trip and reference. ### POST /element_sessions Session for a page. Key: secret key. Returns client_token, valid 2 hours, to pass to the Elements (session option). | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `discount_percent` | integer 0–15 | no | Client discount on this page. | | `client_email, reference, return_url, locale, metadata` | — | no | As for an order. | | `trip` | object | no | destination, start_date, end_date, profile. | ### GET /public/destinations Destinations. Key: publishable key. ### GET /public/plans Plans at the public price. Key: publishable key. ?destination, ?days, ?session. ### POST /public/quotes Quote. Key: publishable key. As /quotes, at the public price. ### POST /public/checkout Hosted checkout. Key: publishable key. Creates a commission order and returns checkout_url. 30 an hour per IP address. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `plan` | string | yes | | | `email` | string | yes | Unless the session carries it. | | `return_url, locale, trip, session` | — | no | return_url is kept only when its origin is among the sites listed on the publishable key, or when it comes from the signed session. A key with no site list takes no return address from the page. | ### GET /public/esims/{id} eSIM page. Key: publishable key. The “My eSIM” page data, without the activation code. ## Webhook events | Type | When | | --- | --- | | `order.paid` | The client paid (commission), or the credit was debited (net). | | `order.delivered` | The eSIM was sent to the client; the esim object is filled. | | `order.failed` | The supplier did not deliver; we step in. | | `order.refunded` | Refunded, by you or by us. | | `order.cancelled` | Payment link cancelled. | | `esim.installed` | The profile is installed on a phone. | | `esim.low_data` | A quarter of the data or less. | | `esim.used_up` | Out of data. | | `esim.expiring` | Validity ends soon. | | `esim.expired` | Validity over. | | `esim.deleted` | The client removed the profile from the phone. | | `esim.suspended` | Data cut. | | `esim.resumed` | Data restored. | | `topup.delivered` | A top-up was added to the eSIM. | ## Errors Body: `{ "error": { "code": "…", "message": "…" } }`. | Status | Code | Meaning | | --- | --- | --- | | 401 | `unauthorized` | Key missing, revoked or unknown. | | 403 | `forbidden` | Wrong key kind, or origin not allowed. | | 404 | `not_found` | Unknown resource or another account’s. | | 402 | `payment_required / insufficient_credit` | Credit too low for a net order. | | 422 | `invalid_request, unknown_plan, invalid_trip, plan_too_short` | Parameter refused; explicit message. | | 409 | `conflict, not_refundable, not_topupable, not_remindable` | The order’s state no longer allows it. | | 429 | `rate_limited` | 600 requests a minute per key. | | 503 | `payments_closed` | Payments are not open (live mode). | --- # Webhooks Terudo calls your HTTPS endpoint at each step of an order and of its eSIM. Create endpoints in the portal (Développeurs) or with the API: ```bash curl -X POST https://terudo.com/api/partner/v1/webhooks \ -H "Authorization: Bearer sk_live_…" -H "Content-Type: application/json" \ -d '{ "url": "https://www.your-agency.example/hooks/terudo", "events": ["order.paid", "order.delivered", "esim.installed", "esim.low_data"] }' # → { "id": 3, "object": "webhook_endpoint", "mode": "live", "events": [ … ], "secret": "whsec_…" } ``` Keep `secret`: it signs every call. An endpoint belongs to the key's mode: a test key creates test endpoints, which receive the sandbox's events only. `events` empty means all events. The URL must be public HTTPS on port 443. ## Events | Type | When | | --- | --- | | `order.paid` | The traveller paid (commission), or your credit was debited (net) | | `order.delivered` | The eSIM was sent; the order's `esim` object is filled | | `order.failed` | The supplier did not deliver; Terudo steps in | | `order.refunded` | Refunded, by you or by Terudo | | `order.cancelled` | Payment link cancelled | | `esim.installed` | The profile is installed on a phone | | `esim.low_data` | A quarter of the data or less is left | | `esim.used_up` | No data left | | `esim.expiring` | Validity ends soon | | `esim.expired` | Validity over | | `esim.deleted` | The traveller removed the profile from the phone | | `esim.suspended`, `esim.resumed` | Data cut, restored | | `topup.delivered` | A top-up was added to the eSIM | ## Payload ```http POST /hooks/terudo HTTP/1.1 Content-Type: application/json User-Agent: Terudo-Webhooks/1 Terudo-Signature: t=1764756000,v1=5f2c… Terudo-Event: order.delivered Terudo-Delivery: 9812 { "id": "evt_8Hq…", "type": "order.delivered", "created_at": "2026-12-01T08:00:00Z", "mode": "live", "data": { "order": { "id": "6b1f…", "object": "order", "reference": "visa-48213", "status": "delivered", "kind": "commission", "plan": { "id": "MA-DAY2-8D", … }, "client": { "email": "marie@example.com", "locale": "fr" }, "esim": { "id": "k8Zs…", "status": "…", "page_url": "https://terudo.com/esim/k8Zs…/", … }, … } } } ``` `data.order` is the same object as `GET /orders/{id}`, with your `reference` and `metadata`. Use `id` (the event id) to ignore a duplicate: a retried delivery has the same event id. ## Verify the signature `Terudo-Signature: t=,v1=." with your endpoint secret>`. Compute it on the **raw body** (bytes as received, before any JSON parsing), compare in constant time, and reject a timestamp more than 5 minutes old. **Node.js (Express)** ```js import crypto from 'node:crypto'; import express from 'express'; const app = express(); app.post('/hooks/terudo', express.raw({ type: 'application/json' }), (req, res) => { const header = req.get('Terudo-Signature') || ''; const parts = Object.fromEntries(header.split(',').map((p) => p.split('='))); const expected = crypto.createHmac('sha256', process.env.TERUDO_WEBHOOK_SECRET).update(`${parts.t}.${req.body}`).digest('hex'); const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300; const valid = parts.v1 && parts.v1.length === expected.length && crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected)); if (!fresh || !valid) return res.status(400).send('bad signature'); const event = JSON.parse(req.body); // handle event.type, event.data.order … res.sendStatus(200); }); ``` **Python (Flask)** ```python import hashlib, hmac, os, time from flask import Flask, request, abort app = Flask(__name__) @app.post("/hooks/terudo") def terudo_hook(): parts = dict(p.split("=", 1) for p in request.headers.get("Terudo-Signature", "").split(",") if "=" in p) body = request.get_data() # raw bytes expected = hmac.new(os.environ["TERUDO_WEBHOOK_SECRET"].encode(), f"{parts.get('t')}.".encode() + body, hashlib.sha256).hexdigest() if not parts.get("t") or abs(time.time() - int(parts["t"])) > 300 or not hmac.compare_digest(expected, parts.get("v1", "")): abort(400) event = request.get_json() return "", 200 ``` **PHP** ```php $body = file_get_contents('php://input'); parse_str(str_replace(',', '&', $_SERVER['HTTP_TERUDO_SIGNATURE'] ?? ''), $parts); $expected = hash_hmac('sha256', ($parts['t'] ?? '') . '.' . $body, getenv('TERUDO_WEBHOOK_SECRET')); if (abs(time() - (int)($parts['t'] ?? 0)) > 300 || !hash_equals($expected, $parts['v1'] ?? '')) { http_response_code(400); exit; } $event = json_decode($body, true); http_response_code(200); ``` **Ruby (Rails)** ```ruby def terudo body = request.raw_post parts = request.headers["Terudo-Signature"].to_s.split(",").to_h { |p| p.split("=", 2) } expected = OpenSSL::HMAC.hexdigest("SHA256", ENV.fetch("TERUDO_WEBHOOK_SECRET"), "#{parts['t']}.#{body}") fresh = (Time.now.to_i - parts["t"].to_i).abs < 300 return head(:bad_request) unless fresh && ActiveSupport::SecurityUtils.secure_compare(expected, parts["v1"].to_s) event = JSON.parse(body) head :ok end ``` ## Answer fast, retry safe Answer `2xx` within 10 seconds, then do the work. Any other answer, or no answer, is retried after 1, 5, 30, 120 and 720 minutes. After 25 failures in a row the endpoint is switched off; turn it back on in the portal or with `PATCH /webhooks/{id}` `{ "enabled": true }`. `GET /webhooks/{id}/deliveries` shows the last 100 deliveries with your server's answer; `POST /webhooks/{id}/deliveries/{delivery_id}/resend` sends one again. ## Test locally - `POST /webhooks/{id}/test` sends a signed test event to the endpoint right away. - With the [CLI](https://terudo.com/developers/docs/cli.md), sign and send a sample event to your local server: `terudo webhooks trigger order.delivered --url http://localhost:3000/hooks/terudo --secret whsec_…`. - With a test key, a sandbox order goes through `order.paid` and `order.delivered` like a real one. --- # Testing and going live ## Three levels | Key | What works | What does not | | --- | --- | --- | | Demo `pk_test_terudo_demo_public` (no account) | Real catalogue, prices, quotes, coverage and compatibility checks, Elements rendering | Checkout (`403 demo_key`): no payment link, no order | | Your `pk_test_…` / `sk_test_…` | Everything: orders, simulated payment on the pay page, sandbox eSIMs, webhooks to test endpoints | Nothing is charged, delivered for real or billed | | Your `pk_live_…` / `sk_live_…` | Real sales | | Test sales never appear in statements or invoices. A test eSIM page looks like a real one but the eSIM cannot be installed. ## Simulate a payment With a `pk_test_…` key in Elements, or an order created with `sk_test_…`, open `checkout_url`: the pay page offers to simulate a successful payment. The order then becomes `paid`, then `delivered`, and your test webhook endpoints receive `order.paid` and `order.delivered`. ## Test webhooks - `POST /webhooks/{id}/test` (or the portal button) sends a signed test event now. - `terudo webhooks trigger --url http://localhost:… --secret whsec_…` (see [CLI](https://terudo.com/developers/docs/cli.md)) signs a sample event with your secret and posts it to your local server, to check your signature code before deploying. ## Go-live checklist 1. Terudo has opened the API for your account (ask your Terudo contact). 2. Live keys created in the portal. The publishable key lists the sites that use it (`https://www.your-agency.example`), so return URLs from your pages are kept. 3. Secret key in your server's secrets, never in a page, an app bundle or a repository. 4. Webhook endpoint created with the live key, its `whsec_…` secret stored, signature verified on the raw body, duplicates ignored by event id. 5. Every order carries your `reference`, so retries never duplicate. 6. Net orders: prepaid credit topped up in the portal (Développeurs) and `402 insufficient_credit` handled. 7. Content Security Policy allows `https://terudo.com` for scripts, connections and images. 8. One real purchase made and refunded (`POST /orders/{id}/cancel` while not installed) to see the whole path. --- # AI tools Everything a coding agent or an AI assistant needs to integrate or sell Terudo eSIMs. ## Docs MCP server (open, no key) `https://terudo.com/mcp/`, Streamable HTTP, no authentication. For coding agents that integrate Terudo: it searches and reads this documentation, writes Elements snippets for your page and framework, gives webhook verification code and serves the agent skills. Tools: `search_docs`, `read_doc`, `list_docs`, `api_reference`, `elements_snippet`, `webhook_verifier`, `get_skill`. Every docs page is also a resource, and each skill a prompt. **Claude Code** ```bash claude mcp add --transport http terudo-docs https://terudo.com/mcp/ ``` **Cursor** (`.cursor/mcp.json`), **Windsurf**, **Claude Desktop** and most clients: ```json { "mcpServers": { "terudo-docs": { "url": "https://terudo.com/mcp/" } } } ``` **VS Code** (`.vscode/mcp.json`): ```json { "servers": { "terudo-docs": { "type": "http", "url": "https://terudo.com/mcp/" } } } ``` **Codex CLI** (`~/.codex/config.toml`): ```toml [mcp_servers.terudo-docs] url = "https://terudo.com/mcp/" ``` Or let the CLI write it: `npx -y @terudo/cli mcp add cursor`. ## Partner MCP server (your keys) `https://terudo.com/api/partner/v1/mcp/` with `Authorization: Bearer `. It acts on your account: with a secret key the whole partner API as tools (quotes, orders, eSIMs, top-ups, webhooks, statements); with a publishable key the public tools a traveller-facing assistant needs (prices, quotes, checks, a payment link on Terudo's checkout). Try it with `Bearer pk_test_terudo_demo_public`. Use a test key (`sk_test_…`) while developing. Details: https://terudo.com/developers/mcp/ ```bash claude mcp add --transport http terudo https://terudo.com/api/partner/v1/mcp/ --header "Authorization: Bearer sk_test_…" ``` ## Agent skills Skills teach a coding agent how to integrate Terudo the right way (which element where, keys and sessions, idempotency, webhook signatures, test mode), in the open Agent Skills format (`SKILL.md`), read by Claude Code, Codex, Cursor, Gemini CLI and others. | Skill | For | | --- | --- | | `terudo-elements` | Adding the eSIM offer to a website or web app: booking confirmation, visa approval, trip page, customer area | | `terudo-api` | Server-side integration: quotes, commission and net orders, eSIMs, top-ups, webhooks, statements | Install into your project: ```bash npx -y @terudo/cli skills add # .claude/skills/ (Claude Code) npx -y @terudo/cli skills add --agent codex # .agents/skills/ ``` Or download them: https://terudo.com/developers/skills/terudo-elements/SKILL.md and https://terudo.com/developers/skills/terudo-api/SKILL.md. Through the docs MCP: the `get_skill` tool, or the prompts `terudo-elements` and `terudo-api`. ## CLI `terudo`: read and search the docs, generate snippets, install skills, configure MCP clients, call the API, sign test webhooks. See [CLI](https://terudo.com/developers/docs/cli.md). ## Plain text - Every docs page in Markdown at its `.md` address, listed in https://terudo.com/developers/docs/index.md - https://terudo.com/llms.txt: what Terudo is and where everything lives - https://terudo.com/llms-full.txt: the whole documentation in one file - https://terudo.com/developers/openapi.json: the API as OpenAPI 3.1 --- # CLI Node.js 18 or later, no dependencies. Run it without installing: ```bash npx -y @terudo/cli help ``` or install it once: `npm install -g @terudo/cli`, then `terudo help`. ## Docs ```bash terudo docs # the docs index terudo docs elements # one page, as Markdown terudo docs search "return url" # search every page terudo api-ref "POST /orders" # one route of the API reference ``` ## Snippets ```bash terudo snippet booking-banner --framework html --destination MA --start 2026-12-03 --end 2026-12-09 terudo snippet trip-offer --framework react --key pk_test_… terudo snippet webhook --language python ``` Frameworks: `html`, `react`, `next`, `vue`. Elements: `destination-picker`, `plans`, `trip-offer`, `booking-banner`, `esim-status`. Webhook languages: `node`, `python`, `php`, `ruby`. ## Agent skills ```bash terudo skills list terudo skills add # every skill into .claude/skills/ terudo skills add terudo-elements --agent codex # into .agents/skills/ terudo skills add --dir ./my/skills # anywhere ``` ## MCP clients ```bash terudo mcp add claude # runs: claude mcp add --transport http terudo-docs https://terudo.com/mcp/ terudo mcp add cursor # writes .cursor/mcp.json terudo mcp add vscode # writes .vscode/mcp.json terudo mcp add codex # prints the lines for ~/.codex/config.toml terudo mcp add cursor --partner --key sk_test_… # the partner MCP with your key instead ``` ## Partner API Set `TERUDO_API_KEY` (a test key while developing), or pass `--key`. ```bash terudo quote MA 2026-12-03 2026-12-09 # works with the demo key when no key is set terudo request GET /plans --query destination=JP terudo request POST /orders --data '{"plan":"MA-10","client":{"email":"test@example.com"},"reference":"test-1"}' ``` ## Webhooks ```bash # Sign a sample event with your endpoint secret and post it to your local server terudo webhooks trigger order.delivered --url http://localhost:3000/hooks/terudo --secret whsec_… # Check a signature you received terudo webhooks verify --secret whsec_… --header "t=…,v1=…" --body-file body.json ``` Events: `order.paid`, `order.delivered`, `order.failed`, `order.refunded`, `order.cancelled`, `esim.installed`, `esim.low_data`, `esim.used_up`, `esim.expiring`, `esim.expired`, `esim.deleted`, `esim.suspended`, `esim.resumed`, `topup.delivered`.