Cuppa Full API — Integration Guide
Overview
The Cuppa Full API lets you sell and manage travel eSIMs entirely inside your own website, app or booking system. You build the plan selector and own the customer journey; Cuppa supplies the plans, bills your business, provisions each eSIM and delivers it.
| You build | Cuppa handles |
|---|---|
| The plan selector, cart and checkout | The catalogue of destinations and plans |
| Collecting the traveller's payment | Billing your business the wholesale price |
| One server call after the booking is paid | Buying, provisioning and delivering the eSIM |
| Optional: your own eSIM management screens | Usage, top-ups, suspension and cancellation |
Your checkout stays yours. Cuppa never collects the traveller's payment, never receives their card details and does not require you to adopt a payment processor.
If you would rather not build a plan selector, use the Low-Code Embed. Both use the same POST /v1/fulfilments call.
How it works
- Your server lists the plans available for the traveller's destination.
- You show those plans in your own interface at your retail price.
- The traveller selects a plan and pays through your existing checkout.
- Once the booking is paid, your server calls
POST /v1/fulfilments. - Cuppa bills your business, provisions the eSIM, and either emails it to the traveller or hands you the installation details.
Only step 4 is required. Listing plans, tracking status and managing eSIMs afterwards are there when you need them.
Before you begin
Set up three things in your Cuppa account:
- The plans you want to sell and your retail price for each
- A company card on file, or a prepaid credit balance
- Your branding for the traveller's activation email, if Cuppa will send it
During early pilots, Cuppa can configure these for you.
| Environment | Key | Behaviour |
|---|---|---|
| Test | sk_test_EXAMPLE | Simulated billing and provisioning. No real charge, no real eSIM |
| Live | sk_live_EXAMPLE | Real billing and real eSIMs |
Base URL: https://api.cuppa.so/v1
Every request is HTTPS and JSON, and carries Authorization: Bearer <secret key>. Call the API from your server only; never place a secret key in browser or mobile app code. Amounts are integers in the currency's minor unit (1000 = $10.00).
Step 1: List regions and plans
Ask Cuppa which destinations you can sell, then fetch the plans for one of them.
curl 'https://api.cuppa.so/v1/regions' \
-H 'Authorization: Bearer sk_test_EXAMPLE'
{
"data": [
{ "code": "JP", "name": "Japan", "type": "country" },
{ "code": "EU", "name": "Europe", "type": "region", "countries": ["FR", "DE", "ES", "IT"] }
]
}
curl 'https://api.cuppa.so/v1/offers?destination=JP' \
-H 'Authorization: Bearer sk_test_EXAMPLE'
{
"data": [
{
"offerId": "offer_jp_1gb_7d",
"destination": "JP",
"name": "Japan 1 GB · 7 days",
"dataMb": 1024,
"validityDays": 7,
"currency": "USD",
"retailAmountMinor": 1000,
"wholesaleAmountMinor": 400,
"supportsTopUp": true,
"supportsSms": false
}
]
}
retailAmountMinoris the price you set in your Cuppa account and charge the traveller.wholesaleAmountMinoris what Cuppa bills your business for each eSIM.GET /v1/offers/{offerId}returns one plan, for when you only need to confirm its price.
Plans and prices change rarely. You may cache these responses for up to an hour rather than calling on every page view.
Step 2: Sell the plan in your own checkout
Show the plans however suits your product, add the chosen one to your cart at retailAmountMinor, and store its offerId against the booking. Then take payment exactly as you do today.
Prices are fixed. There is nothing to quote, lock or reserve before the traveller pays.
Have your server decide the amount for the eSIM line from the offer, as it does for your other extras, rather than trusting a price sent by the browser.
Step 3: Request the eSIM after payment
Once your own backend has confirmed the booking is paid, tell Cuppa. This is the only call you must make.
Option A: Cuppa emails the traveller
curl -X POST 'https://api.cuppa.so/v1/fulfilments' \
-H 'Authorization: Bearer sk_test_EXAMPLE' \
-H 'Content-Type: application/json' \
-d '{
"orderId": "BK-104233",
"itemId": "esim-1",
"offerId": "offer_jp_1gb_7d",
"customerEmail": "traveller@example.com",
"customerName": "Example Traveller"
}'
Cuppa sends an activation email with a QR code, carrying your name, logo, colours and wording.
Option B: You deliver the eSIM yourself
Use this to send the eSIM in your own booking email or app, without sharing the traveller's name or email with Cuppa.
{
"orderId": "BK-104233",
"itemId": "esim-1",
"offerId": "offer_jp_1gb_7d",
"delivery": "partner_api"
}
Cuppa does not email the traveller. You retrieve the installation details in Step 4.
The response
Both options return 202 Accepted:
{
"fulfilmentId": "ful_3Xa9Lw7c",
"status": "processing",
"orderId": "BK-104233",
"itemId": "esim-1",
"offerId": "offer_jp_1gb_7d",
"wholesaleAmountMinor": 400,
"currency": "USD"
}
- Call from your server, at the point you know you have been paid. That is usually your payment processor's server-side confirmation. Never trigger it from a browser success page, or when the traveller merely clicks Pay.
- Retries are safe.
orderIdis your booking reference anditemIdnames one eSIM within it. Sending the same pair again returns the existing fulfilment; it never bills or provisions twice. - A timeout is not a failure. If a request times out, send it again with the same
orderIdanditemId. Never change them to force a new attempt. - Several eSIMs on one booking. Make one call per eSIM, each with its own
itemId. 202is an acknowledgement, not delivery. A fulfilment is one eSIM, from this request until the plan expires.
Step 4: Track the outcome and deliver
Most fulfilments finish within seconds. Read one at any time with GET /v1/fulfilments/{fulfilmentId}.
{
"fulfilmentId": "ful_3Xa9Lw7c",
"status": "delivered",
"orderId": "BK-104233",
"itemId": "esim-1",
"offerId": "offer_jp_1gb_7d",
"billing": { "status": "charged", "amountMinor": 400, "currency": "USD" },
"esim": { "iccid": "8910300000000000000", "state": "not_installed" }
}
| Status | Meaning | What you do |
|---|---|---|
processing | Cuppa is billing your account and provisioning the eSIM | Nothing |
provisioned | The eSIM is ready. Final state for partner_api delivery | Retrieve the installation details and send them |
delivered | Cuppa emailed the activation details to the traveller | Nothing |
action_required | Your card was declined or your credits ran out. No eSIM has been bought | Update your card or top up; Cuppa then resumes automatically |
failed | Cuppa could not provision the plan and has reversed the wholesale charge | Refund the traveller's eSIM line, or offer another plan |
billing.status is pending, charged, refunded or not_charged.
If provisioning fails
- Cuppa retries first. A provisioning error is retried automatically before a fulfilment is marked
failed, and a retry can never buy a second eSIM. - Your money is returned automatically. On
failed, Cuppa refunds the wholesale charge to your card or restores your credits. failedis final. To try again or offer a different plan, send a newitemId.
Installation details for your own delivery
Once a partner_api fulfilment is provisioned:
curl 'https://api.cuppa.so/v1/fulfilments/ful_3Xa9Lw7c/install' \
-H 'Authorization: Bearer sk_test_EXAMPLE'
{
"iccid": "8910300000000000000",
"smdpAddress": "rsp.example.com",
"activationCode": "EXAMPLE_NONFUNCTIONAL_CODE",
"lpaString": "LPA:1$rsp.example.com$EXAMPLE_NONFUNCTIONAL_CODE",
"qrPayload": "LPA:1$rsp.example.com$EXAMPLE_NONFUNCTIONAL_CODE"
}
Render qrPayload as a QR code for the traveller to scan. These values are credentials: restrict access, and never put them in logs, analytics or URLs.
Webhooks
To be told about changes instead of polling, register one HTTPS endpoint.
curl -X PUT 'https://api.cuppa.so/v1/webhook' \
-H 'Authorization: Bearer sk_test_EXAMPLE' \
-H 'Content-Type: application/json' \
-d '{ "url": "https://book.example.com/hooks/cuppa" }'
The response includes a signingSecret, shown once.
| Event | Sent when |
|---|---|
fulfilment.provisioned | The eSIM is ready for your own delivery |
fulfilment.delivered | The activation email has been sent |
fulfilment.action_required | Billing your business failed |
fulfilment.failed | Provisioning failed and the charge was reversed |
esim.data_low | The traveller is close to using all their data |
esim.expiring | The plan is about to expire |
esim.state_changed | The eSIM was installed, suspended, cancelled, revoked or expired |
{
"type": "fulfilment.delivered",
"createdAt": "2026-12-01T10:02:11Z",
"data": { "fulfilmentId": "ful_3Xa9Lw7c", "orderId": "BK-104233", "itemId": "esim-1", "status": "delivered" }
}
Each request carries a Cuppa-Signature header, an HMAC-SHA256 of the raw body using your signing secret. Verify it before acting, reply 2xx promptly, and expect the occasional duplicate.
Manage eSIMs after delivery
Every action below applies to one fulfilment, and only to fulfilments on your own account. Not every plan supports every action; an unsupported one returns not_supported.
| Action | Request | Effect |
|---|---|---|
| List eSIMs | GET /v1/fulfilments?orderId=BK-104233 | Your fulfilments, newest first. Filter by orderId, status or esimState; page with cursor |
| Check usage | GET /v1/fulfilments/{id}/usage | Data used, data remaining and expiry |
| Top up | POST /v1/fulfilments/{id}/topups | Adds data to an active eSIM. Billed to your business at the top-up's wholesale price |
| Suspend | POST /v1/fulfilments/{id}/suspend | Pauses data service. Reversible |
| Unsuspend | POST /v1/fulfilments/{id}/unsuspend | Restores a suspended eSIM |
| Cancel | POST /v1/fulfilments/{id}/cancel | Cancels an eSIM that has not been installed, and refunds the wholesale charge |
| Revoke | POST /v1/fulfilments/{id}/revoke | Permanently disables the eSIM. Irreversible; no refund |
| Send SMS | POST /v1/fulfilments/{id}/sms | Sends a text message to the eSIM, on plans where supportsSms is true |
eSIM states
esim.state is not_installed, active, suspended, cancelled, revoked or expired.
Usage
{
"fulfilmentId": "ful_3Xa9Lw7c",
"state": "active",
"dataUsedMb": 412,
"dataRemainingMb": 612,
"expiresAt": "2026-12-08T10:02:11Z"
}
Usage is reported by the mobile network and can lag real use by up to a few hours.
Top up
List the top-ups a plan allows with GET /v1/offers/{offerId}/topups, take payment from the traveller in your own checkout, then:
{
"topupOfferId": "topup_jp_1gb",
"topupId": "topup-1"
}
topupId is your own reference. As with itemId, repeating it returns the existing top-up and never bills twice. Top-ups follow the same statuses as a fulfilment.
Cancel and revoke
- Cancel is for an eSIM the traveller will not use, typically after a booking is cancelled. It is only possible while
esim.stateisnot_installed. Cuppa refunds the wholesale charge; refunding the traveller is yours to do. - Revoke is for fraud or misuse. It cannot be undone and is never refunded.
Send SMS
{ "message": "Your rental car is ready at Bay 14." }
Messages go only to the eSIM on that fulfilment, up to 160 characters.
Pricing and billing
Every plan has two fixed prices. There is no dynamic pricing and nothing to quote.
| Price | Set by | Example |
|---|---|---|
| Retail price | You, in your Cuppa account | $10.00, charged by you to the traveller |
| Wholesale price | Cuppa | $4.00, billed to your business for each eSIM fulfilled |
In this example you keep $6.00 per eSIM. Each plan you sell is set up in one of two ways:
- Paid add-on. The traveller pays your retail price, which must be at or above the wholesale price.
- Included eSIM. You fund the eSIM as part of the booking. The traveller pays $0 and Cuppa bills you the wholesale price. The retail minimum does not apply. Ask your Cuppa contact to enable this for a plan.
The wholesale price never rises without 30 days' written notice. You are billed the wholesale price in force when you call POST /v1/fulfilments, and the response states the amount.
How Cuppa bills you
- Pay as you go. Cuppa charges your saved company card for each eSIM or top-up before provisioning it. No upfront cost.
- Prepaid credits. Cuppa deducts each eSIM or top-up from your credit balance, with no card charge per order.
Cuppa never buys an eSIM before your business has been billed, and never bills twice for one orderId and itemId. A declined card or empty balance pauses the fulfilment as action_required; it does not affect the traveller's payment to you.
Check your balance
curl 'https://api.cuppa.so/v1/balance' \
-H 'Authorization: Bearer sk_test_EXAMPLE'
{
"billingMethod": "prepaid_credits",
"availableAmountMinor": 48200,
"currency": "USD",
"fulfilmentsAwaitingPayment": 0
}
Reference
Endpoints
| Operation | Method and path |
|---|---|
| Supported regions | GET /v1/regions |
| Get all data packages | GET /v1/offers?destination={code} |
| Plan price request | GET /v1/offers/{offerId} |
| Order an eSIM | POST /v1/fulfilments |
| Query allocated eSIMs | GET /v1/fulfilments and GET /v1/fulfilments/{id} |
| Installation details | GET /v1/fulfilments/{id}/install |
| Usage check | GET /v1/fulfilments/{id}/usage |
| Top up | GET /v1/offers/{offerId}/topups, POST /v1/fulfilments/{id}/topups |
| Suspend | POST /v1/fulfilments/{id}/suspend |
| Unsuspend | POST /v1/fulfilments/{id}/unsuspend |
| Cancel | POST /v1/fulfilments/{id}/cancel |
| Revoke | POST /v1/fulfilments/{id}/revoke |
| Send SMS | POST /v1/fulfilments/{id}/sms |
| Balance query | GET /v1/balance |
| Set webhook | PUT /v1/webhook |
POST /v1/fulfilments body
| Field | Required | Description |
|---|---|---|
orderId | Yes | Your booking reference. Shown on your Cuppa statement |
itemId | Yes | Your name for this eSIM within the booking, e.g. esim-1. orderId and itemId together must be unique on your account |
offerId | Yes | The plan the traveller selected |
delivery | No | cuppa_email (default) or partner_api |
customerEmail | For cuppa_email | Where the activation email is sent |
customerName | No | Used to address the email |
Errors
Errors return a non-2xx status and a body of the form { "error": { "code": "offer_not_found", "message": "..." } }.
| HTTP | Code | Meaning |
|---|---|---|
| 401 | invalid_key | Missing, wrong or revoked secret key |
| 403 | access_denied | Your account is not enabled for this operation |
| 404 | offer_not_found | No such plan on your account, or it is no longer sold |
| 404 | fulfilment_not_found | No such fulfilment on your account |
| 409 | item_conflict | This orderId and itemId already have a fulfilment for a different plan |
| 409 | invalid_state | The eSIM is not in a state that allows this action, e.g. cancelling an installed eSIM |
| 422 | invalid_request | A required field is missing or malformed |
| 422 | not_supported | This plan does not support the action, e.g. SMS or top-up |
| 429 | rate_limited | Too many requests. Retry after the Retry-After interval |
| 5xx | server_error | Retry the same request with the same orderId and itemId |
A declined company card is not an API error. The request is accepted and the fulfilment moves to action_required.
Test mode and go-live checklist
Build against your sk_test_... key. In test mode there is no card charge and no real eSIM; installation details are non-functional. Cuppa sends a clearly labelled TEST activation email to authorised test recipients only. Switch to your sk_live_... key once Cuppa activates your production account.
- Plans and prices shown to travellers come from
GET /v1/offers - The
offerIdis stored against the booking - Your server, not the browser, decides the amount charged for the eSIM line
- Only bookings confirmed paid by your server trigger the fulfilment call
- Repeated payment callbacks and timeouts resend the same
orderIdanditemId - A
failedfulfilment leads to a refund of the traveller's eSIM line action_requiredreaches someone who can fix your billing- For
partner_apidelivery, installation details are kept out of logs and URLs - Webhook signatures are verified, if you use webhooks
- A cancelled booking cancels its eSIM, where it has not been installed
- The secret key exists only on your server