Intake Integration
This page walks through embedding Triple intake end-to-end: create a session, render the iframe, listen for completion, fetch the evaluation.
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
| Field | Required | Description |
|---|---|---|
mode | No | "form" (default) or "chatbot" |
transaction | Yes | The transaction being disputed. Same shape as the evaluations request — the more you send, the fewer questions the cardholder answers |
card | Yes | Masked card snapshot |
cardholder | No | Your internal cardholder identifier and optional locale (en-US, es-ES, etc.) |
category_hint | No | "fraud", "service_not_received", "processing_duplicate", or "cancelled". Used as a starting point; may be overridden |
return_url | No | Where 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.