Cuppa Embed — Low-Code Integration Guide
Overview
Cuppa Embed adds a travel eSIM to your booking extras with one HTML tag and one server call. Cuppa shows the eSIM at the price you set; your cart, checkout and payment processor stay exactly as they are.
Booking extras
Additional driver +$25.00
Child seat +$15.00
Travel eSIM Powered by Cuppa
Stay connected during your trip to Japan.
1 GB · 7 days
$10.00
[ Add eSIM ]
Booking total $200.00
| You build | Cuppa handles |
|---|---|
| One HTML tag on your extras page | Showing the eSIM plans at your prices |
| A few lines that add the selected eSIM to your cart | Billing your business the wholesale price |
| One server call after the booking is paid | Buying, provisioning and emailing the eSIM |
The component only handles selection. It never collects the traveller's card details and never talks to your payment processor. The traveller pays you; Cuppa bills your business separately.
If you would rather build the picker yourself, use the Full API. Both use the same fulfilment call.
How it works
- The traveller taps Add eSIM in the Cuppa component on your booking page.
- Your page adds the eSIM to your cart.
- The traveller pays you as usual, through your existing checkout.
- Your server calls
POST /v1/fulfilmentsonce the booking is paid. - Cuppa bills your business, provisions the eSIM and emails it to the traveller.
Before you begin
Set up four things in your Cuppa account:
- The plans you want to sell and your retail price for each, for example $10.00
- A company card on file, or a prepaid credit balance
- Your branding for the traveller's activation email
- The site origins where the component may load, for example
https://book.example.com
You will receive two keys.
| Key | Example | Where it belongs |
|---|---|---|
| Publishable key | pk_test_EXAMPLE | In your page HTML. Safe to expose; it only works on your approved origins and can only display your plans. |
| Secret key | sk_test_EXAMPLE | On your server only. It requests eSIMs that are billed to your business. Never place it in HTML or JavaScript. |
Component script: https://js.cuppa.so/v1/embed.js
REST API base: https://api.cuppa.so/v1
Step 1: Add the component
Place the tag where the eSIM should appear among your extras, and load the script anywhere on the page.
<script async src="https://js.cuppa.so/v1/embed.js"></script>
<cuppa-esim
publishable-key="pk_test_EXAMPLE"
destination="JP"
currency="USD">
</cuppa-esim>
There is no initialisation code and no load order to manage. The tag is inert until the script arrives, then renders itself. Set destination to the traveller's arrival or pick-up country.
The component fails quietly. If Cuppa is unreachable, or you sell no plan for the destination, it collapses to zero height and your booking flow continues without it.
Step 2: Add the selection to your cart
When the traveller adds or removes an eSIM, the component tells your page which plan was chosen and its price. Add it to your cart like any other extra, and store the offerId against the booking. Pick whichever option suits your page.
Option A: JavaScript event
Best when your booking total updates on the page without a reload.
const esim = document.querySelector("cuppa-esim");
esim.addEventListener("cuppa:change", (event) => {
const { offerId, plan, price } = event.detail;
if (offerId) {
// Your own cart code.
bookingCart.setExtra("esim", {
offerId,
label: plan.name,
amountMinor: price.amountMinor
});
} else {
bookingCart.removeExtra("esim"); // traveller removed the eSIM
}
});
The event detail when an eSIM is added:
{
"offerId": "offer_jp_1gb_7d",
"destination": "JP",
"plan": { "name": "Japan 1 GB · 7 days", "dataMb": 1024, "validityDays": 7 },
"price": { "amountMinor": 1000, "currency": "USD" }
}
When the traveller removes the eSIM, offerId is null.
Option B: Hidden form field
Best when your extras page is a classic HTML form that posts to your server. Add input-name and place the tag inside your form; no JavaScript is needed.
<form method="post" action="/booking/extras">
<!-- your other extras -->
<cuppa-esim
publishable-key="pk_test_EXAMPLE"
destination="JP"
currency="USD"
input-name="cuppa_offer_id">
</cuppa-esim>
<button type="submit">Continue</button>
</form>
The component keeps a hidden cuppa_offer_id field up to date. Your server reads it when the form is submitted; an empty value means no eSIM.
Pricing the cart line
The price is the fixed retail price you set in your Cuppa account, so it is the same for every traveller. As with any extra, have your server decide the amount rather than trusting a figure sent by the browser. Either keep your eSIM prices alongside your other extras, or look them up with GET /v1/offers/{offerId}.
If the traveller returns to the extras page, render the tag with offer-id="offer_jp_1gb_7d" so the eSIM shows as already added.
Step 3: Request the eSIM after payment
Once your own backend has confirmed the booking is paid, tell Cuppa. This is the only server call in the integration.
async function fulfilPaidBooking(booking) {
if (booking.paymentStatus !== "paid") return;
if (!booking.esimOfferId) return; // no eSIM on this booking
const response = await fetch("https://api.cuppa.so/v1/fulfilments", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CUPPA_SECRET_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
orderId: booking.id,
itemId: "esim-1",
offerId: booking.esimOfferId,
customerEmail: booking.customer.email,
customerName: booking.customer.name
})
});
if (!response.ok) throw new Error("eSIM fulfilment needs attention");
return response.json();
}
Cuppa replies 202 Accepted:
{
"fulfilmentId": "ful_3Xa9Lw7c",
"status": "processing",
"orderId": "BK-104233",
"itemId": "esim-1",
"offerId": "offer_jp_1gb_7d",
"wholesaleAmountMinor": 400,
"currency": "USD"
}
Cuppa then bills your business the wholesale price, buys the eSIM and emails the traveller.
- 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.
- Retries are safe.
orderIdis your booking reference anditemIdnames one eSIM within it. Together they identify one eSIM on your account. Sending the same pair again returns the existing fulfilment; it never bills or provisions twice. - Several eSIMs on one booking. Make one call per eSIM, each with its own
itemId(esim-1,esim-2) and, if you wish, its owncustomerEmail. - Conflicts are rejected. The same
orderIdanditemIdwith a differentofferIdreturnsitem_conflict. 202is an acknowledgement, not delivery. See Step 4.
This is the only call you must make. The status and offer endpoints described below are optional.
Step 4: Check the outcome
Most fulfilments reach delivered within seconds and need nothing further from you. To check one, call GET /v1/fulfilments/{fulfilmentId}.
| Status | Meaning | What you do |
|---|---|---|
processing | Cuppa is billing your account and provisioning the eSIM | Nothing |
delivered | The activation email has been sent 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 |
Cuppa also emails your account contact whenever a fulfilment becomes action_required or failed, so you do not need to poll to catch problems.
If provisioning fails
- Cuppa retries first. A supplier error is retried automatically before a fulfilment is marked
failed. Each fulfilment carries one supplier reference, so 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. No request is needed. failedis final. Re-sending the sameorderIdanditemIdreturns the failed fulfilment. To try again or offer a different plan, send a newitemId.- Reconciliation. Every fulfilment reports its billing state, and your Cuppa statement lists each one by
orderIdanditemIdso it can be matched to your bookings.
{
"fulfilmentId": "ful_3Xa9Lw7c",
"status": "failed",
"orderId": "BK-104233",
"itemId": "esim-1",
"billing": { "status": "refunded", "amountMinor": 400, "currency": "USD" }
}
billing.status is pending, charged, refunded or not_charged.
Reference
Component attributes
| Attribute | Required | Description |
|---|---|---|
publishable-key | Yes | Your pk_test_... or pk_live_... key |
destination | Yes | ISO 3166-1 two-letter country code, e.g. JP. Changing it reloads the plans and clears any selection |
currency | Yes | ISO 4217 code matching your booking currency, e.g. USD |
offer-id | No | Shows this plan as already added when the page is rendered again |
input-name | No | Name of a hidden form field the component keeps in sync with the selected offerId |
theme | No | auto (default, follows your page), light or dark |
Component events
Events are dispatched on the <cuppa-esim> element and bubble.
| Event | Fires when | event.detail |
|---|---|---|
cuppa:ready | Plans have loaded and the component is visible | { destination } |
cuppa:change | The traveller adds, changes or removes an eSIM | The selection shown in Step 2, or { offerId: null } on removal |
cuppa:error | Plans could not be loaded | { code, message } |
The picker renders inside an isolated Cuppa-hosted frame, so your page styles cannot break it and it cannot read your page.
API endpoints
All requests use HTTPS and JSON, and authenticate with Authorization: Bearer sk_.... Amounts are integers in the currency's minor unit (1000 = $10.00).
| Method and path | Required? | Purpose |
|---|---|---|
POST /v1/fulfilments | Yes | Request the eSIM for a paid booking |
GET /v1/fulfilments/{fulfilmentId} | Optional | Read a fulfilment's status and billing state |
GET /v1/offers/{offerId} | Optional | Read a plan's name, retail price and wholesale price |
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 |
customerEmail | Yes | 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 |
| 404 | offer_not_found | No such plan on your account, or it is no longer sold |
| 409 | item_conflict | This orderId and itemId already have a fulfilment for a different plan |
| 422 | invalid_request | A required field is missing or malformed |
| 429 | rate_limited | Too many requests. Retry after the Retry-After interval |
| 5xx | server_error | Retry 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.
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, shown in the component and 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 component shows it as included, 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.
Price changes:
- Your retail price takes effect in the component immediately. Bookings already in checkout keep the price in your cart.
- 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. A booking paid today is therefore never billed more than the price published today.
How Cuppa bills you
- Pay as you go. Cuppa charges your saved company card for each eSIM before provisioning it. No upfront cost.
- Prepaid credits. Cuppa deducts each eSIM from your credit balance, with no card charge per eSIM.
Cuppa never buys an eSIM before your business has been billed, and never bills twice for one booking.
What the traveller receives
An activation email with a QR code and install instructions, carrying your name, logo, colours and wording. To deliver the eSIM through your own channels instead, see the Full API guide.
Test mode and go-live checklist
Build against pk_test_... and sk_test_... keys. In test mode there is no card charge and no supplier purchase. Cuppa sends a clearly labelled TEST activation email, with non-functional activation data, to authorised test recipients only.
Going live means swapping both keys for their pk_live_... and sk_live_... equivalents.
- The component renders on the extras page, and the page still works when it is absent
- Adding and removing an eSIM updates your cart correctly
- Changing the destination clears the previous selection and its cart line
- 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 send the same
orderIdanditemId - A
failedfulfilment leads to a refund of the traveller's eSIM line action_requiredandfailedemails reach someone who can act on them- The activation email branding has been reviewed with a TEST email
- Your live site origins are approved in your Cuppa account