v1.0.0 launch

Destination-Scoped eSIM at Checkout: The 40-Line Integration (Full Walkthrough with Code)

The complete implementation for adding a destination-scoped eSIM attach to a travel-app booking flow. Real curl, real webhook handler, real activation-completion event, real refund handler — under 40 lines each, framework-agnostic. This is what a first-day partner integration against api.yonosim.com actually looks like end-to-end.

By · Founder, YonoSIMLinkedIn ↗·Published August 10, 2026·12 min read

Summary

This is the complete integration for adding a destination-scoped eSIM attach to a travel-app booking flow — from the plan lookup at booking time to the webhook handler that marks the trip "connectivity ready" when the user completes install. Every block below is under 40 lines and framework-agnostic. Real endpoints, real payloads, real code you can lift into a production PR this afternoon.

The full flow in one diagram

Five nodes, three of them yours, two of them YonoSIM's. The pattern is the same for a flight booking, a hotel booking, an itinerary save, or an expense report generation.

  1. Your booking API completes a booking, extracts destination + dates, calls YonoSIM plan search, and stashes the matched SKU on the booking record.
  2. Your checkout UI renders a pre-selected eSIM card with the price and coverage. User taps "Add to trip". Your payment intent updates.
  3. Your backend fires POST /v1/orders on Stripe success, stores the returned order.id against the booking.
  4. YonoSIM's activation page at api.yonosim.com/a/:orderId (your logo, your color) renders the QR + install steps.
  5. Your webhook endpoint receives order.activated when the traveler installs. You mark the trip ready and (optionally) schedule the top-up upsell at 80% usage via order.data.low.

Step 1 — Plan search at booking time (12 lines)

At booking completion, look up the matching SKU so the checkout card renders with a real price, not a placeholder. Cache the response for 24 hours per country/duration combo — plan availability changes daily, not per-request.

// on-booking-created.ts
async function attachEsimSku(booking: Booking) {
  const res = await fetch('https://api.yonosim.com/v1/plans/search', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.YONOSIM_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      country: booking.destinationCountryIso,     // e.g. 'JP'
      minDataGb: 3,                                // sensible default per trip length
      validForDays: booking.tripDurationDays,      // 14
      sort: 'price',                               // cheapest matching plan first
    }),
  });
  const { plans } = await res.json();
  return plans[0];  // { id, name, dataGb, days, priceUsd }
}

Step 2 — Render the pre-selected card (~30 lines JSX)

The UX rule that matters: one plan pre-selected, one tap to add. If you present a catalog, the attach rate craters. Show the price inline, hide the SKU, and let the user tap Add without a modal.

// checkout-esim-card.tsx
export function EsimAttachCard({ booking, sku }: Props) {
  const [added, setAdded] = useState(false);
  return (
    <div className="flex items-center justify-between rounded-lg border p-4">
      <div>
        <p className="text-sm font-semibold">
          Stay connected in {booking.destinationName}
        </p>
        <p className="text-xs text-zinc-600">
          {sku.dataGb} GB · {sku.days} days · works on iPhone + Android
        </p>
      </div>
      <button
        onClick={() => { setAdded(true); addLineItem(sku); }}
        disabled={added}
        className="rounded-md bg-sky-700 px-4 py-2 text-sm font-semibold text-white"
      >
        {added ? 'Added' : `Add for $${sku.priceUsd}`}
      </button>
    </div>
  );
}

Step 3 — Create the order on Stripe success (18 lines)

In your Stripe payment_intent.succeeded handler, if the line items include the eSIM SKU, fire POST /v1/orders. The response returns everything you need to route the user to activation.

// stripe-webhook.ts (relevant handler)
async function onPaymentSucceeded(intent: Stripe.PaymentIntent) {
  const booking = await getBookingFromIntent(intent);
  if (!booking.esimSku) return;  // no attach purchased

  const res = await fetch('https://api.yonosim.com/v1/orders', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.YONOSIM_API_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': `booking-${booking.id}`,  // safe against retries
    },
    body: JSON.stringify({
      planId: booking.esimSku,
      customerEmail: booking.travelerEmail,
      metadata: { bookingId: booking.id },
    }),
  });
  const order = await res.json();  // { id, iccid, lpaString, activationUrl }

  await saveEsimOrder(booking.id, order);
  await sendActivationEmail(booking.travelerEmail, order.activationUrl);
}

Step 4 — Verify + handle lifecycle webhooks (24 lines)

One endpoint handles all five events. HMAC verification is 6 lines, the event router is the rest. Idempotency is enforced by dropping duplicate event.ids at the top of the handler.

// yonosim-webhook.ts
import crypto from 'crypto';

export async function POST(req: Request) {
  const raw = await req.text();
  const sig = req.headers.get('x-yonosim-signature') ?? '';
  const expected = crypto
    .createHmac('sha256', process.env.YONOSIM_WEBHOOK_SECRET!)
    .update(raw)
    .digest('hex');
  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return new Response('bad signature', { status: 401 });
  }

  const event = JSON.parse(raw);
  if (await eventAlreadyProcessed(event.id)) return new Response('ok');
  await markEventProcessed(event.id);

  switch (event.type) {
    case 'order.activated':
      await markTripConnectivityReady(event.data.metadata.bookingId);
      break;
    case 'order.data.low':
      await scheduleTopUpUpsell(event.data.metadata.bookingId);
      break;
    case 'order.data.exhausted':
      await notifyTravelerOfExhaustion(event.data.iccid);
      break;
    case 'order.refunded':
      await refundBookingPortion(event.data.metadata.bookingId, event.data.amountUsd);
      break;
  }
  return new Response('ok');
}

Step 5 — The activation surface (the smallest piece)

Either link the user to YonoSIM's branded activation page (5 lines — the default), or render your own page that opens the LPA universal link. Both work; the branded YonoSIM page is battle-tested against the full iOS/Android device matrix and rebrands to your palette.

// simplest possible: link to the branded hosted page
<Link href={order.activationUrl}>Install your eSIM →</Link>

// or self-host the install by opening the universal LPA link directly
// (works on iOS 17.4+ and Android 14+ — see the SDK-vs-REST spoke)
<button onClick={() => (window.location.href = order.lpaUniversalLink)}>
  Install your eSIM →
</button>

For the tradeoff between hosted vs self-rendered activation UX, see the SDK vs REST spoke — short version, hosted for <50k activations/month, self-rendered (or SDK) above that.

Total line count for a shipped integration

StepLines of codeTime to write
Plan search on booking created1230 min
Pre-selected checkout card30 (JSX + styles)1 hr
Order creation on Stripe success1845 min
Webhook verify + router241 hr
Activation link render510 min
Total~89 linesHalf a day

Testing the flow end-to-end before going live

Every code block above works against a shared demo Growth-tier key that returns real responses from the sandbox environment. To test the webhook path, register a https://webhook.site URL against POST /v1/webhooks and fire a test order from the playground — the resulting order.issued and order.activated events arrive within seconds and let you validate your signature-verification handler without deploying.

# End-to-end test in four commands
export KEY="sk_test_growth_daf7d4add37757f28cb4c9e020b41da3fd0aca463ee0af0b"

# 1. Register a webhook target (use your webhook.site URL)
curl -X POST "https://api.yonosim.com/v1/webhooks" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://webhook.site/YOUR-ID","events":["order.*"]}'

# 2. Find a Japan plan
curl -X POST "https://api.yonosim.com/v1/plans/search" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"country":"JP","minDataGb":5,"validForDays":14}'

# 3. Create an order — webhook fires within seconds
curl -X POST "https://api.yonosim.com/v1/orders" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: test-${RANDOM}" \
  -d '{"planId":"jp_5gb_30d","customerEmail":"[email protected]"}'

# 4. Simulate refund
curl -X POST "https://api.yonosim.com/v1/orders/ORD_ID/refund" \
  -H "Authorization: Bearer $KEY"

FAQ

QWhat is 'destination-scoped' eSIM at checkout?

AA single eSIM plan pre-selected for the traveler's destination country and trip length, shown as a one-tap add on the booking confirmation screen. Not a catalog to browse — the destination came from the booking record, the plan came from your partner API, the user sees 'Japan · 5 GB · 14 days · $12' with an Add button. Attach rate is 3–7× higher than presenting a generic plan menu because there's no decision fatigue.

QHow many API calls does a full attach flow require?

AThree. (1) POST /v1/plans/search with the destination country and validity window at booking time to fetch the SKU + price for the pre-selected card. (2) POST /v1/orders after the user taps Add and pays, returning the ICCID, LPA string, and QR URL. (3) One webhook receipt of order.activated when the user completes install. Everything else — refunds, top-up upsells, usage sync — is opt-in and fires later.

QWhere does the destination country come from?

AThe booking record you already have. For flights: the arrival airport country. For hotels: the property country. For tour bookings: the itinerary's primary country (or trigger a multi-country regional plan). For expense and currency apps: the trip you created. If your app already collects a destination for its core booking flow, you already have this field — no user prompt needed.

QHow do I handle refunds inside the integration?

ATwo paths — automatic and explicit. Automatic: if the traveler never uses the eSIM within 30 days of purchase, YonoSIM's auto-refund sweep credits the activation cost back to your prepaid balance and fires an order.refunded webhook. Your handler updates the booking record and (optionally) refunds the customer through Stripe. Explicit: on cancellation, POST /v1/orders/:id/refund cancels upstream and credits the balance immediately. Both use the same order.refunded webhook shape.

QWhat happens if my webhook endpoint is down when order.activated fires?

AYonoSIM retries with exponential backoff — 30 seconds, then 5 minutes, then 1 hour, then 6 hours. After four failed attempts the event moves to a dead-letter table and fires an alert to the partner's registered contact. The activation itself is unaffected — the traveler's eSIM works — but you can safely delay processing without losing the event. Every webhook payload includes an idempotency key so a retry doesn't double-process.

QHow do I verify the HMAC-SHA256 webhook signature?

AThe X-YonoSIM-Signature header contains the hex-encoded HMAC-SHA256 of the raw request body, keyed with your webhook secret (set once at the /v1/webhooks endpoint). Recompute it on receipt with the same secret; timing-safe compare against the header. The exact 8-line handler is in the code walkthrough below. If the signatures don't match, respond 401 and drop the event — YonoSIM will not retry a rejected event.

Bottom line

~89 lines of your code, half a day to write, one afternoon to test end-to-end against a real sandbox. That's the shape of a partner eSIM integration in 2026 — the API surface is small on purpose so the value shows up in a single sprint, not a quarter-long commitment. Playground live at api.yonosim.com/docs; real sandbox key + $10 test credit from the waitlist in 24 hours.

Back to the Travel apps hub. If you're still comparing partners, the Airalo Partners vs YonoSIM API spoke is the neighbor to this walkthrough.