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.
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 (stagePolicyPurchase)
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
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 |