Payment Integration Guide
Partner quick-start guide for embedding the TII payment widget and handling secure card-entry flows with Travel Insured hosted forms.
Payment Integration Guide
Quick-start for partner developers embedding the TII payment flow into a checkout experience.
For full deferred quote, staging, hosted payment, and policy binding sequences, see the Single Trip Quote to Purchase Flow and Annual Plan Quote to Purchase Flow.
How It Works
Card data never touches your servers. Your application:
- Creates a payment session server-to-server (your backend calls the TII payment app)
- Passes the
paymentSessionIdto your frontend - Mounts the payment widget or iframe using the session ID and
embedToken - Listens for safe status events
The widget renders the Travel Insured hosted card-entry form inside a secure iframe. Payment is verified server-side and policy binding happens automatically.
Prerequisites
- You have received a
partnerTokenandpartnerSubscriptionKeyfrom TII - Your server's origin is registered with Travel Insured
- You have a
planGuidfrom a staged policy purchase (stagePolicyPurchaseorstageAnnualPolicyPurchase)
Step 1: Create a Payment Session (Server-Side)
Call this from your backend, not from the browser. Both headers are required.
POST https://sb-pay.travelinsured.com/api/embedded-payment-sessions
Content-Type: application/json
Authorization: ApiKey {partnerToken}
x-api-key: {partnerSubscriptionKey}
Request body:
{
"planGuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"quoteNumber": "Q123456",
"agencyNumber": "AGN001",
"agentId": "AGT001",
"billingPartyInformation": {
"totalAmount": 99.99,
"billingAddress": {
"addressLine1": "123 Main St",
"city": "San Francisco",
"stateIsoCode": "CA",
"zipCode": "94107",
"countryIsoCode": "US"
},
"email": "customer@example.com",
"firstName": "Jane",
"lastName": "Smith"
}
}
Required fields:
| Field | Description |
|---|---|
planGuid | From stagePolicyPurchase. UUID. |
billingPartyInformation.totalAmount | Transaction amount. |
billingPartyInformation.billingAddress.* | Full billing address. |
billingPartyInformation.email | Customer email. |
billingPartyInformation.firstName | Cardholder first name. |
billingPartyInformation.lastName | Cardholder last name. |
Optional fields: quoteNumber, agencyNumber, agentId
Important: Do not send
Authorizationorx-api-keyfrom the browser. These credentials belong in your server-side code only.
Important: Your server's
Originheader is used to identify and validate your integration. Make sure your TII contact has registered your server origin with Travel Insured.
Response (201 Created):
{
"paymentSessionId": "3e4d9f2a-61bc-4e78-b012-7a5d8c9e1f03",
"status": "Created",
"expiresAt": "2026-06-08T18:30:00Z",
"embedToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Response fields:
| Field | Type | Description |
|---|---|---|
paymentSessionId | string (UUID) | Stable session identifier. Pass this to the widget or iframe. |
status | string | Always "Created" on success. |
expiresAt | string (ISO 8601) | Session expiry. Sessions are valid for 30 minutes by default. |
embedToken | string (JWT) | Short-lived signed token the payment page uses to verify the session load is legitimate. Required for widget integrations. See security note below. |
embedToken— required for all integrations
- Standard widget (
<tii-payment-widget>) integrations: Pass bothpayment-session-idandembed-tokenattributes. The widget forwards the token to the payment page URL automatically.- TypeScript / SDK users: Add
embedToken: stringto yourCreateSessionApiResponsetype definition.What
embedTokenis: A short-lived HS256 JWT (5-minute lifetime) containingpaymentSessionId,planGuid,environment,mode: "iframe", optional partner context, and standard JWT claims. The payment page validates it server-side before rendering.What
embedTokenis not: It is not the Authorize.net form token, not a payment credential, and does not contain transaction or cardholder data. It does not replace your partner API key or subscription key.Security requirements:
- Do not log
embedToken- Do not store it in cookies accessible to JavaScript
- Do not include it in analytics payloads
- Do not expose it to browser JavaScript — it is a server-side concern only
The session expires 30 minutes after creation. If the customer exceeds that time, create a new session and re-mount the widget.
Step 2: Add the Widget to Your Page
Include the widget script and mount the component with both payment-session-id and embed-token from Step 1.
<!DOCTYPE html>
<html>
<head>
<script type="module"
src="https://sb-pay.travelinsured.com/widget/tii-payment-widget.esm.js">
</script>
</head>
<body>
<!-- Inline mode: renders the form immediately without a button click -->
<tii-payment-widget
id="paymentWidget"
payment-session-id="3e4d9f2a-61bc-4e78-b012-7a5d8c9e1f03"
embed-token="{embedToken}"
payment-app-base-url="https://sb-pay.travelinsured.com"
mode="inline"
auto-open="true"
button-label="Complete Payment">
</tii-payment-widget>
<script>
// See Step 3 for event handling
</script>
</body>
</html>
Important: The
embed-tokenattribute value must be set server-side when rendering your page. Do not set it from browser JavaScript using a client-side fetch. The token is a server-to-frontend handoff only.
Widget attributes:
| Attribute | Required | Description |
|---|---|---|
payment-session-id | Yes | The paymentSessionId returned by Step 1. |
embed-token | Yes | The embedToken JWT from the same Step 1 response. Required to authenticate the session load. |
payment-app-base-url | Yes | Base URL of the TII payment app. Provided by TII. |
mode | inline - renders iframe directly in the page. modal - shows a Pay Now button first. Default: modal. | |
auto-open | When true and mode="inline", opens the payment form immediately without a button click. Default: false. | |
button-label | Custom text for the Pay Now button. Default: Pay Now. |
Step 3: Handle Payment Events
const widget = document.getElementById("paymentWidget");
widget.addEventListener("tii.payment.loaded", (event) => {
// Form is rendered and ready - hide any loading spinners
console.log("Payment form ready", event.detail);
});
widget.addEventListener("tii.payment.started", (event) => {
// Customer clicked Pay - show processing state
console.log("Payment started", event.detail);
});
widget.addEventListener("tii.payment.processing", (event) => {
// Card is being submitted to TravelInsured payments - keep spinner visible
console.log("Processing payment", event.detail);
});
widget.addEventListener("tii.payment.verified", (event) => {
// Server confirmed payment - policy binding in progress
console.log("Payment verified", event.detail);
});
widget.addEventListener("tii.policy.bound", (event) => {
// SUCCESS - policy is bound and documents are available
const { planNumber, cobDownloadLink, eobDownloadLink } = event.detail;
console.log("Policy bound:", planNumber);
// Redirect to confirmation page, show documents, etc.
});
widget.addEventListener("tii.payment.failed", (event) => {
// Payment was declined or an error occurred
console.error("Payment failed", event.detail);
// Show an error message to the customer
});
widget.addEventListener("tii.payment.cancelled", (event) => {
// Customer closed the widget without completing payment
console.log("Payment cancelled", event.detail);
});
widget.addEventListener("tii.payment.closed", (event) => {
// Widget modal/iframe was closed
console.log("Widget closed", event.detail);
});
Security Note: Events are UX signals only. Never trigger fulfillment logic on
tii.payment.processingalone. Wait fortii.policy.boundand confirm the final state via your server pollingGET /api/embedded-payment-sessions/{id}/status.
Step 4: Confirm Completion (Server-Side Poll)
As a belt-and-suspenders confirmation, poll the session status from your server:
// Your server-side polling
async function pollSessionStatus(paymentSessionId) {
const response = await fetch(
`https://sb-pay.travelinsured.com/api/embedded-payment-sessions/${paymentSessionId}/status`,
{
headers: {
Authorization: `ApiKey ${process.env.PARTNER_TOKEN}`,
},
}
);
return response.json();
}
// Check for completion
const session = await pollSessionStatus(paymentSessionId);
if (session.status === "Bound" && session.bindStatus === "Bound") {
// Confirmed - retrieve planNumber and document links
const { planNumber, cobDownloadLink, eobDownloadLink } = session;
}
Terminal states: Bound, BindFailed, Failed, Cancelled, Expired.
Testing with Test Credit Cards
Use these test credit card numbers in your sandbox environment to verify payment flows without processing real transactions.
| Card Brand | Card Number |
|---|---|
| Visa | 4007000000027 |
| Visa | 4012888818888 |
| Visa | 4111111111111111 |
| Mastercard | 5424000000000015 |
| Mastercard | 2223000010309703 |
| Mastercard | 2223000010309711 |
For all test cards:
- Expiration Date: Any future date (e.g., 12/25)
- CVV: Any 3-digit number (e.g., 123)
- Cardholder Name: Any name
Declined/Failed Transaction: Use zip code
46282with any of the test credit cards above to generate a declined transaction.
These cards will process as successful transactions in sandbox. All transactions will be declined in production if these test card numbers are used.
Validate Embed Token (Optional - Server-to-Server)
POST /api/embedded-payment-sessions/{paymentSessionId}/validate-token
This endpoint allows callers to verify that an embedToken is still valid for a specific session. It is primarily used internally by the payment page before rendering.
Partners with custom iframe shells or server-side session orchestration may optionally call this endpoint to confirm the token is still valid before loading the payment page. It is not required for standard widget integrations.
Request
POST /api/embedded-payment-sessions/{paymentSessionId}/validate-token
Authorization: ApiKey {partnerToken}
Content-Type: application/json
{
"embedToken": "<embedToken from session creation response>"
}
Responses
| Status | Body | Meaning |
|---|---|---|
200 OK | { "valid": true } | Token is valid, not expired, and matches the session. |
401 Unauthorized | { "error": "Invalid or expired token" } | Token is invalid, expired, or does not match the session. |
401 Unauthorized | { "error": "Unauthorized" } | Authorization header is missing or invalid. |
400 Bad Request | { "error": "Invalid request body" } | embedToken field is missing or malformed. |
When to use this endpoint
- You maintain a custom iframe shell and want to verify the token before initiating the iframe load
- You perform server-side session orchestration and need to confirm the token has not expired before handing off to the frontend
- Standard
<tii-payment-widget>integrations do not need to call this endpoint
Security Rules
| Do | Do Not |
|---|---|
| Create sessions from your server | Create sessions from browser JavaScript |
Keep partnerToken and x-api-key server-side | Include credentials in HTML or client-side JS |
Pass embedToken to the widget or iframe URL server-side | Expose embedToken to browser JavaScript |
| Confirm completion via server status poll | Treat tii.payment.processing as success |
| Let the payment widget handle card entry | Create your own card number/CVV inputs |
Validate postMessage origin before processing | Trust inbound iframe messages without origin check |
Troubleshooting Quick Reference
| Symptom | Check |
|---|---|
401 Unauthorized | Authorization: ApiKey {value} is valid and in PARTNER_API_KEYS |
400 Missing required auth headers | Both Authorization and x-api-key are present |
403 Request origin is not in the partner allowlist | Your server origin is in PARTNER_ALLOWED_ORIGINS |
400 Missing Origin/Referer header | Your server-side HTTP client is sending an Origin header |
| Widget renders but payment page rejects the session | Verify embed-token attribute is set and matches the paymentSessionId |
| Session expires before customer pays | Increase PAYMENT_SESSION_TTL_MINUTES or recreate session |
| Widget shows blank iframe | Check NEXT_PUBLIC_ACCEPT_HOSTED_FORM_URL and browser CSP |