SAP Concur, Navan & TripActions eSIM Integration Guide (2026): The Booking-Completion Webhook Walkthrough
The complete integration for provisioning a corporate eSIM at the moment a business flight confirms in your TMC. Real webhook payloads, real code, ~60 lines of integration total. Same shape works for SAP Concur, Navan (formerly TripActions), TravelPerk, and Egencia — the differences are just field names on the incoming booking-completion event.
Summary
Provisioning a corporate eSIM at the moment a business flight confirms in your TMC is one HTTPS webhook + one POST /v1/orders call + one branded activation page. The same code pattern works for SAP Concur, Navan (TripActions), TravelPerk, and Egencia — only the incoming booking payload's field names differ. Total integration: ~60 lines, half a day to write, one afternoon to test end-to-end against a real sandbox.
The end-to-end flow (four nodes)
- TMC (Concur / Navan / TripActions) confirms a business booking and fires an outbound webhook to your middleware.
- Your middleware receives the booking payload, runs the corporate policy engine (role + destination allowlist + data quota tier), and calls POST /v1/orders on YonoSIM.
- YonoSIM provisions a real eSIM ICCID + LPA in ~180ms and returns the activation URL.
- Your email service injects the activation URL into the TMC's confirmation email (or fires a separate "connectivity ready" email 24h before departure).
Step 1 — Register the TMC webhook (once, per environment)
Every TMC exposes an admin console for outbound webhooks. Register a POST endpoint on your middleware pointing at/tmc/booking-completed. Enable the events for booking created, booking modified, booking cancelled. Copy the signing secret into your middleware's environment.
- SAP Concur: App Center → Webhooks → Add Subscription. Events:
Travel/BookingCreated,Travel/BookingModified,Travel/BookingCancelled. Signature header:X-Concur-Signature(SHA-256). - Navan (TripActions): Admin → Integrations → Webhooks. Events:
trip.confirmed,trip.updated,trip.cancelled. Signature header:Navan-Signature(HMAC-SHA256). - TravelPerk: Settings → API → Webhooks. Events:
booking.confirmed,booking.cancelled. Signature header:X-TravelPerk-Signature. - Egencia: Partner API dashboard → Webhook Configuration. Events:
trip.finalized,trip.void. Signature header:Egencia-Sig.
Step 2 — Handle the incoming booking-completion webhook (Concur example, ~35 lines)
This is the full handler. Verify the Concur signature, extract destination + duration + employee, run policy, provision. Navan/TripActions/TravelPerk use identical structure with different field names.
// app/api/tmc/concur-booking-completed/route.ts
import crypto from 'crypto';
export async function POST(req: Request) {
const raw = await req.text();
const sig = req.headers.get('x-concur-signature') ?? '';
const expected = crypto
.createHmac('sha256', process.env.CONCUR_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 (event.type !== 'Travel/BookingCreated') return new Response('ok');
// Concur booking payload shape
const arrivalCountry = event.data.segments[0].arrivalAirport.countryCode; // 'DE'
const departDate = new Date(event.data.segments[0].departureDateTime);
const returnDate = new Date(event.data.segments[event.data.segments.length - 1].departureDateTime);
const tripDays = Math.ceil((returnDate.getTime() - departDate.getTime()) / 86400000);
const employeeId = event.data.traveler.employeeId;
const policy = await resolveCorporatePolicy(employeeId, arrivalCountry);
if (!policy.eligible) return new Response('policy: not eligible for eSIM');
const yonoRes = await fetch('https://api.yonosim.com/v1/orders', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.YONOSIM_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': `concur-${event.data.bookingId}`,
},
body: JSON.stringify({
planId: policy.planId, // e.g. 'eu_5gb_30d'
customerEmail: event.data.traveler.email,
metadata: {
tmc: 'concur',
bookingId: event.data.bookingId,
employeeId,
department: policy.department,
},
}),
});
const order = await yonoRes.json();
await storeTripEsim(event.data.bookingId, order);
await scheduleActivationEmail(event.data.traveler.email, order.activationUrl, departDate);
return new Response('ok');
}Step 3 — Corporate policy engine (~15 lines)
This is where you encode the policy from your hub buyer's-guide decisions: role tier, destination allowlist, data quota. Real implementations often live in a rules engine or feature flag service — this simplified form is the shape.
// corporate-policy.ts
const SANCTIONED = new Set(['IR', 'KP', 'SY', 'CU']);
const EU_EEA = new Set(['DE','FR','NL','BE','IT','ES','SE','DK','NO','FI','GB',/* ... */]);
const HIGH_QUOTA_DEPTS = new Set(['sales', 'field_services', 'customer_success']);
export async function resolveCorporatePolicy(employeeId: string, arrivalCountry: string) {
if (SANCTIONED.has(arrivalCountry)) {
return { eligible: false, reason: 'sanctioned destination' };
}
const employee = await getEmployee(employeeId); // your HRIS
const highQuota = HIGH_QUOTA_DEPTS.has(employee.department);
const quotaGb = highQuota ? 10 : 5;
const planId = EU_EEA.has(arrivalCountry)
? `eu_${quotaGb}gb_30d` // EU-regional
: `${arrivalCountry.toLowerCase()}_${quotaGb}gb_30d`; // country-specific
return {
eligible: true,
planId,
department: employee.department,
};
}Step 4 — Handle lifecycle webhooks from YonoSIM (~20 lines)
Same webhook handler pattern as the Travel apps integration walkthrough — just wire lifecycle events to your corporate dashboards + policy triggers.
// app/api/yonosim/webhook/route.ts
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':
// 80% quota reached — fire Slack for top-up approval
await notifySlackForTopUpApproval(event.data.metadata.employeeId);
break;
case 'order.refunded':
// Auto-refund on unused — credit corporate ledger, log for finance
await creditCorporateLedger(event.data.metadata.bookingId, event.data.amountUsd);
break;
}
return new Response('ok');
}TMC-specific field mapping
The only thing that changes across the four major TMCs is the incoming payload field names. Here's the mapping to save you the docs-reading:
| Field | SAP Concur | Navan / TripActions | TravelPerk | Egencia |
|---|---|---|---|---|
| Arrival country ISO | segments[0].arrivalAirport.countryCode | destination.country | flights[0].arrival.country | trip.arrivalCountryCode |
| Depart date | segments[0].departureDateTime | startDate | outbound_date | trip.startDate |
| Return date | Derive from last segment | endDate | return_date | trip.endDate |
| Employee ID | traveler.employeeId | user.externalId | traveler.employee_reference | traveler.corporateId |
| Booking ID (idempotency key) | bookingId | tripId | booking_reference | trip.recordLocator |
| Signature header | X-Concur-Signature | Navan-Signature | X-TravelPerk-Signature | Egencia-Sig |
End-to-end test in the sandbox
Before wiring the real TMC webhook, validate the YonoSIM path in the sandbox. Register a webhook.site URL as your yonosim-side listener, fire a test order, and observe the activation + refund lifecycle.
# Set the demo key
export KEY="sk_test_growth_daf7d4add37757f28cb4c9e020b41da3fd0aca463ee0af0b"
# 1. Register a webhook target (put your webhook.site URL here)
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. Simulate the policy engine picking an EU 5GB plan for a Frankfurt trip
curl -X POST "https://api.yonosim.com/v1/plans/search" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"region":"EU","minDataGb":5,"validForDays":30}'
# 3. Provision the corporate eSIM
curl -X POST "https://api.yonosim.com/v1/orders" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: concur-BK123456" \
-d '{"planId":"eu_5gb_30d","customerEmail":"[email protected]","metadata":{"tmc":"concur","bookingId":"BK123456","employeeId":"EMP001"}}'
# 4. Test the auto-refund path
curl -X POST "https://api.yonosim.com/v1/orders/ORD_ID/refund" \
-H "Authorization: Bearer $KEY"Total integration size
| Component | Lines of code | Time |
|---|---|---|
| TMC booking-completion webhook handler | 35 | 1–2 hr |
| Corporate policy engine | 15 | 45 min |
| YonoSIM lifecycle webhook handler | 20 | 1 hr |
| Activation email template (or TMC-inline) | ~10 | 30 min |
| Total | ~80 lines | Half a day |
FAQ
QWhich TMCs can integrate with a corporate eSIM API today?
AAny TMC that supports outbound booking-completion webhooks. This covers SAP Concur (via Concur App Center + webhooks), Navan (formerly TripActions, native webhooks + REST API), TravelPerk (Zapier + native events), Egencia (partner API + webhooks), and most custom in-house systems built on Amadeus or Sabre GDS. The integration pattern is identical across all of them — only the incoming booking payload's field names differ.
QWhat data does the TMC send that we use to pick the eSIM plan?
AThree fields matter: (1) arrival airport country (mapped to ISO code — e.g. FRA → DE), (2) trip duration in days (last flight departure date minus first flight arrival date), (3) employee ID (used for policy lookup — role, department, quota tier). Some TMCs also send purpose-of-trip codes (sales, engineering, executive) which map to different data quotas. Concur exposes all four in the standard travel-request webhook; Navan and TripActions include arrival country and dates but not purpose codes by default.
QDo we need to wait for the flight to be issued before provisioning the eSIM?
ANo — you want to provision immediately on booking confirmation, not flight issuance. The eSIM install typically takes travelers 5–15 minutes and they do it while still at home or in the taxi to the airport; if you wait until the boarding pass fires, they land without data. Under YonoSIM's flow, POST /v1/orders returns the ICCID + LPA string in ~180ms — provision it on the same webhook that confirms the booking and email the QR to the traveler with 24–72 hours of headroom.
QHow does the corporate policy engine hook into the provisioning call?
AThe webhook handler runs policy lookup before firing POST /v1/orders. Common shape: (1) look up the employee's tier from your HRIS or a static role map, (2) determine the data quota (5 GB / 10 GB / custom), (3) determine the plan region (EU-regional, US, APAC-regional, country-specific), (4) validate destination against the allowlist (block sanctioned jurisdictions), (5) fire POST /v1/orders with the resolved plan ID. Total policy engine overhead is ~15 lines of code and typically runs in under 50ms.
QHow does the traveler receive the eSIM?
ATwo paths — inline in the TMC's confirmation email, or via a separate itinerary attachment. Recommended pattern: your webhook handler stores the branded activation URL (api.yonosim.com/a/:orderId) against the trip record, then either injects it into the TMC's confirmation email template or triggers a follow-up email 24 hours before departure via your existing email service (SendGrid, Postmark, Resend). YonoSIM's white-label activation page renders your logo, primary color, and support email so the traveler never sees YonoSIM branding.
QWhat happens if the booking gets cancelled or modified?
AThe TMC fires a booking-cancelled webhook — your handler calls POST /v1/orders/:id/refund on YonoSIM, which cancels the eSIM upstream and credits the activation cost back to your corporate ledger in the same DB transaction. If the traveler already installed the eSIM but never used data, the automatic 30-day no-usage sweep would credit it back anyway. For destination changes (Frankfurt → Munich), no action needed — the EU-regional plan covers both. For continental changes (Frankfurt → Tokyo), refund the EU plan and issue a fresh Japan plan under the same webhook.
Bottom line
A production-grade TMC-to-eSIM integration is one webhook handler, one POST /v1/orders, and one lifecycle listener — ~80 lines of your code, half a day to write, one afternoon to test end-to-end. The pattern is identical across SAP Concur, Navan (TripActions), TravelPerk, and Egencia. The playground at api.yonosim.com/docs lets you validate the YonoSIM side before wiring the TMC webhook; the enterprise waitlist turns around a real sandbox key + $10 test credit in 24 hours.
Back to the Corporate travel hub. The compliance requirements the integration needs to satisfy are documented in the GDPR + data-reporting spoke.