Payment Integration Guide

Partner quick-start guide for embedding the TII payment widget and handling secure card-entry flows with Travel Insured hosted forms.

Audience: Partner development teams integrating payment flowsLast Updated: July 26, 2026

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:

  1. Creates a payment session server-to-server (your backend calls the TII payment app)
  2. Passes the paymentSessionId to your frontend
  3. Mounts the payment widget or iframe using the session ID and embedToken
  4. 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 partnerToken and partnerSubscriptionKey from TII
  • Your server's origin is registered with Travel Insured
  • You have a planGuid from 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:

FieldDescription
planGuidFrom stagePolicyPurchase. UUID.
billingPartyInformation.totalAmountTransaction amount.
billingPartyInformation.billingAddress.*Full billing address.
billingPartyInformation.emailCustomer email.
billingPartyInformation.firstNameCardholder first name.
billingPartyInformation.lastNameCardholder last name.

Optional fields: quoteNumber, agencyNumber, agentId

Important: Do not send Authorization or x-api-key from the browser. These credentials belong in your server-side code only.

Important: Your server's Origin header 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:

FieldTypeDescription
paymentSessionIdstring (UUID)Stable session identifier. Pass this to the widget or iframe.
statusstringAlways "Created" on success.
expiresAtstring (ISO 8601)Session expiry. Sessions are valid for 30 minutes by default.
embedTokenstring (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 both payment-session-id and embed-token attributes. The widget forwards the token to the payment page URL automatically.
  • TypeScript / SDK users: Add embedToken: string to your CreateSessionApiResponse type definition.

What embedToken is: A short-lived HS256 JWT (5-minute lifetime) containing paymentSessionId, planGuid, environment, mode: "iframe", optional partner context, and standard JWT claims. The payment page validates it server-side before rendering.

What embedToken is 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-token attribute 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:

AttributeRequiredDescription
payment-session-idYesThe paymentSessionId returned by Step 1.
embed-tokenYesThe embedToken JWT from the same Step 1 response. Required to authenticate the session load.
payment-app-base-urlYesBase URL of the TII payment app. Provided by TII.
modeinline - renders iframe directly in the page. modal - shows a Pay Now button first. Default: modal.
auto-openWhen true and mode="inline", opens the payment form immediately without a button click. Default: false.
button-labelCustom 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.processing alone. Wait for tii.policy.bound and confirm the final state via your server polling GET /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 BrandCard Number
Visa4007000000027
Visa4012888818888
Visa4111111111111111
Mastercard5424000000000015
Mastercard2223000010309703
Mastercard2223000010309711

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

StatusBodyMeaning
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

DoDo Not
Create sessions from your serverCreate sessions from browser JavaScript
Keep partnerToken and x-api-key server-sideInclude credentials in HTML or client-side JS
Pass embedToken to the widget or iframe URL server-sideExpose embedToken to browser JavaScript
Confirm completion via server status pollTreat tii.payment.processing as success
Let the payment widget handle card entryCreate your own card number/CVV inputs
Validate postMessage origin before processingTrust inbound iframe messages without origin check

Troubleshooting Quick Reference

SymptomCheck
401 UnauthorizedAuthorization: ApiKey {value} is valid and in PARTNER_API_KEYS
400 Missing required auth headersBoth Authorization and x-api-key are present
403 Request origin is not in the partner allowlistYour server origin is in PARTNER_ALLOWED_ORIGINS
400 Missing Origin/Referer headerYour server-side HTTP client is sending an Origin header
Widget renders but payment page rejects the sessionVerify embed-token attribute is set and matches the paymentSessionId
Session expires before customer paysIncrease PAYMENT_SESSION_TTL_MINUTES or recreate session
Widget shows blank iframeCheck NEXT_PUBLIC_ACCEPT_HOSTED_FORM_URL and browser CSP