Skip to main content

Intake Integration

This page walks through embedding Triple intake end-to-end: create a session, render the iframe, listen for completion, fetch the evaluation.

Beta

Endpoints, request bodies, response shapes, and event names on this page are under active development and may change before general availability. The flow — session → iframe → completion event → evaluation — is stable.

Step 1: Create a session

Your backend calls the session creation endpoint. Don't do this from the browser — your API key must stay server-side.

curl -X POST https://api.disputes.sandbox.tripledev.app/api/intake/sessions/ \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "form",
"transaction": {
"transaction_id": "txn_12345",
"transaction_amount": 299.99,
"transaction_currency": "USD",
"transaction_timestamp": "2026-04-18T10:30:00Z",
"merchant_name": "ACME Store"
},
"card": {
"bin": "54133388",
"last_4": "1234"
},
"cardholder": {
"external_id": "cust_9f3a2",
"locale": "en-US"
},
"return_url": "https://issuer.example.com/disputes/done"
}'

Request fields

FieldRequiredDescription
modeNo"form" (default) or "chatbot"
transactionYesThe transaction being disputed. Same shape as the evaluations request — the more you send, the fewer questions the cardholder answers
cardYesMasked card snapshot
cardholderNoYour internal cardholder identifier and optional locale (en-US, es-ES, etc.)
category_hintNo"fraud", "service_not_received", "processing_duplicate", or "cancelled". Used as a starting point; may be overridden
return_urlNoWhere to send the cardholder after they finish (shown as a button at the end)

Response

{
"id": "is_a1b2c3",
"url": "https://intake.triple.app/s/is_a1b2c3",
"expires_at": "2026-04-23T16:30:00Z",
"status": "pending",
"mode": "form"
}
  • id — use this to track the session server-side and cross-reference with webhooks.
  • url — the short-lived URL to embed in the iframe. Treat it as single-use.
  • expires_at — the URL stops working after this time. If the cardholder doesn't start within the window, create a new session.

Step 2: Embed the iframe

Return the url from your backend to your frontend and embed it.

<iframe
id="triple-intake"
src="https://intake.triple.app/s/is_a1b2c3"
width="480"
height="640"
allow="clipboard-write"
style="border: 0; border-radius: 12px;"
></iframe>

Sizing

Intake is responsive and works in any container at least 360 px wide. For best results we recommend:

  • Desktop: 480 × 640 or 520 × 720
  • Mobile: full width × 100 vh (or your app's content area height)

If you need a full-screen modal, wrap the iframe and set width: 100%; height: 100%.

Step 3: Listen for completion

Triple communicates back to your page via window.postMessage. Always check event.origin before trusting the payload.

window.addEventListener('message', (event) => {
if (event.origin !== 'https://intake.triple.app') return;

const msg = event.data;
if (!msg || typeof msg !== 'object') return;

switch (msg.type) {
case 'triple.intake.opened':
// Cardholder has loaded the flow
break;

case 'triple.intake.step_changed':
// Useful for analytics: msg.step_id
break;

case 'triple.intake.completed':
// Hand off to evaluation lookup
fetchEvaluation(msg.evaluation_id);
break;

case 'triple.intake.abandoned':
// Cardholder closed or stalled; you may offer to resume later
break;
}
});

See Events for the full list of events and their payload shapes.

Step 4: Fetch the evaluation result

When the completed event fires you have an evaluation_id. From here it's the same as the direct path:

curl https://api.disputes.sandbox.tripledev.app/api/evaluations/{evaluation_id}/result \
-H "Authorization: Bearer $API_KEY"

Or — strongly recommended — let a webhook push the result to your server as soon as it's ready, so your frontend doesn't have to poll.

Full example

<!doctype html>
<html>
<body>
<div id="intake-container"></div>

<script>
async function startIntake() {
// 1. Ask your backend to create a session
const res = await fetch('/api/disputes/intake-session', {
method: 'POST',
body: JSON.stringify({ transactionId: 'txn_12345' }),
headers: { 'Content-Type': 'application/json' },
});
const { url } = await res.json();

// 2. Embed the returned URL
const iframe = document.createElement('iframe');
iframe.src = url;
iframe.width = 480;
iframe.height = 640;
iframe.style.border = 0;
iframe.allow = 'clipboard-write';
document.getElementById('intake-container').appendChild(iframe);
}

// 3. Listen for completion
window.addEventListener('message', async (event) => {
if (event.origin !== 'https://intake.triple.app') return;
if (event.data?.type === 'triple.intake.completed') {
// Send the evaluation_id back to your backend,
// which will fetch the full result from /evaluations/{id}/result
await fetch('/api/disputes/intake-complete', {
method: 'POST',
body: JSON.stringify({
intake_session_id: event.data.intake_session_id,
evaluation_id: event.data.evaluation_id,
}),
headers: { 'Content-Type': 'application/json' },
});
window.location.href = '/disputes/done';
}
});

startIntake();
</script>
</body>
</html>

Server-to-server alternative

If you'd rather not listen on the frontend, subscribe to the intake.session.completed webhook instead. The webhook payload carries the same intake_session_id and evaluation_id. See Webhooks.