Sending Transaction Data to DQC
This page is about when to send, and what to say about a respondent at each point in their journey. For the endpoint itself and every field it accepts, see POST /data.
📌 Overview
Send twice per respondent.
| Send | When | status |
|---|---|---|
| First | As soon as you have an id for them, before you know the outcome | Partial |
| Final | When they reach a terminal state, however it ends | Qualified / Terminated / Overquota |
The first send is the one people skip, and it is the one that matters most. If a respondent starts and never comes back, that record is the only evidence they existed — it becomes an Abandon rather than nothing at all.
Both sends carry the same customerTransactionID, so DQC upserts them into one transaction rather
than two rows.
The lifecycle
There is no separate "this is a partial" flag. status is the signal, and a record stays
Partial until you send a final one that replaces it.
Partial
The in-progress state, and the resting state of an abandon. Send it as soon as you have an id.
startDate and endDate are required. POST /data will happily accept a record without an
endDate and return 200 — and then the record is dropped later, during mapping, with nothing
surfaced to you.
Mid-flow you do not know the end time yet, so send startDate plus 100 ms. It is a
placeholder that keeps the record valid until the final send replaces it.
Two rules that go with that placeholder:
- The final send must carry a strictly later
endDatethan the partial.endDateis what we order the two sends by. Send the same value twice and which one wins is undefined. - Derive both timestamps from your server clock, not the browser's. Timestamps in the future are rejected, and a single bad row fails the batch it travels in.
- Send milliseconds. We order the two sends by comparing the
endDatestrings, so a 100 ms gap only survives if it is written down:2026-09-09T14:59:00.100Z, not2026-09-09T14:59:00Z. JavaScript'stoISOString()does this for you; if you format timestamps yourself — in C#, Python, anywhere — keep the.SSS.
A zero-length abandon does not distort your length-of-interview figures — duration only accrues for completes.
Qualified
They finished. Send the real endDate, plus any failures they tripped on the way through.
Terminated
Your screener ended it, or a DQC quality termination did. Send your own reason, in whatever form you already have it — see below.
Overquota
They qualified but the quota was already full.
Any other disqualification
Anything else your flow can produce — a hard stop, a fraud rule, a partner reject. Send it with your own code. You do not need to squeeze it into one of the four names above; we map it.
Example: a survey
When the Quality Tools run inside the survey itself, the two sends bracket the interview: one early, one at the end.
Send the first record after the first question, and the final one at the end of the interview
whichever way it ends. The abandon branch is the whole reason the first send exists: the respondent
answers a question and leaves, the final send never fires, and the record keeps the Partial status
it already had. DQC maps that to Abandon.
This is how the Decipher/Forsta integration works, if you want to see it wired up on a real platform.
Example: a router or a redirect
When the respondent only passes through your page on the way to a survey, you do not have an interview to bracket. You send the partial at entry and the final when they come back.
- The respondent lands on your router.
- You get a
requestIdfrom the Quality Tools. - You hand that to your server.
- Your server sends the first record with
status: "Partial", thestartDate, anendDateof start plus 100 ms, and the seller and buyer. - The respondent goes off to the survey.
- They come back, and your server sends the final record — same
customerTransactionID, the realendDate, your real status and your real reason.
Server-side or client-side is your call. We do not mind which, as long as the key that reaches
POST /data and the lookup stays on your server — the toolbox URL carries one by design.
➡️ Routers and Redirects in JavaScript — the same flow with diagrams and working code.
What the two records look like
The first one, mid-flow — note endDate is a placeholder 100 ms after the start:
{
"customerTransactionID": "4zd7stau0g6uba5j",
"status": "Partial",
"startDate": "2026-09-09T14:59:00.000Z",
"endDate": "2026-09-09T14:59:00.100Z",
"surveyName": "Southern Living Insiders",
"projectName": "Southern Living Insiders",
"buyerName": "Your Company",
"sellerName": "Newsletter",
"requestId": "1788965942580.aYDIIy",
"participantId": "0qN3RdAbZlAYWeqCpMA1",
"deviceScore": 75,
"dataTrustScore": 454,
"persona": "INCOGNITO OPERATOR",
"isDuplicate": false,
"deviceFailures": ["Privacy Settings"],
"country": "SV",
"subdivision": "Santa Ana Department"
}
The final one. This is the case worth studying, because it is the one integrations mis-handle: the respondent finished, but they answered before the tools could score them, so every quality field came back as the same message:
{
"customerTransactionID": "a5w8sq6bcdm5pe6t",
"status": "Qualified",
"startDate": "2026-08-24T14:51:00Z",
"endDate": "2026-08-24T14:51:10Z",
"surveyName": "Southern Living Insiders",
"projectName": "Southern Living Insiders",
"buyerName": "Your Company",
"sellerName": "Newsletter",
"requestId": "Submission too quick, data not processed",
"participantId": "Submission too quick, data not processed",
"deviceScore": 0,
"dataTrustScore": 0,
"persona": "NONE",
"isDuplicate": false,
"deviceFailures": ["Submission too quick, data not processed"],
"country": "Submission too quick, data not processed",
"subdivision": "Submission too quick, data not processed",
"failures": { "trapQuestion": 1, "honeyPot": 0 }
}
Send it exactly like that. Do not blank the message out, do not swap in a fake id, and do not
drop the record. The message is what tells us the response was never scored, and we normalise the
fields that need it. A 0 score here means "never measured", not "measured and terrible".
Decipher serialises every value, so the same records go over the wire with "75" instead of 75
and "False" instead of false. That is expected and we handle it. From JavaScript, send real
JSON types as shown above.
Three things that will bite you
Send the whole record every time — an omitted field is not "leave it as it was". Most columns
are written on every send, so a field the final record leaves out overwrites what the partial
stored with nothing. Seven of them behave the other way and keep their previous value: seller,
buyer, survey, startDate, endDate, the disposition and country — which also means you cannot
blank those by sending null.
Keep what you sent in the partial and send it back with the outcome filled in. If you keep only one
field, keep the requestId: it is how we tie the row to the rest of the data on our side.
The final record needs every required field, every time. The merge above only helps with
optional ones. If the final record is missing a seller, a survey name or a valid date it is
rejected outright — and the stored row silently keeps the partial's status. You will see a
Partial that should have been a complete, with nothing in your logs to explain it.
200 is not "in the dashboard". Both sends return 200 as soon as we have stored them.
Mapping runs on a schedule afterwards — see Integration phases.
You do not need to adapt to us
This is the part worth taking us up on. Send your own vocabulary and we write the translation.
- Your status values — numbers, your own words, whatever your platform emits.
- Your termination codes —
4,SCREENOUT_AGE,quota_full_wave2. All fine. - Your reason text, in whatever field you already keep it.
- Your quality-check names, as
failureskeys.
You do not have to match our names, our casing or our conventions. Send what you have, tell us what it means, and we build the mapping rules on our side — see Phase 2 and A custom set of rules.
A value you are not sure about is cheaper to send now and ignore later than to add once you are live. During Phase 1 we would rather receive too much than too little.
Next steps
- POST /data — the endpoint, every field, and what each value means.
- Integration phases — what happens after you start sending.
- Routers and Redirects in JavaScript — working code.
✅ Summary
- Two sends per respondent, both carrying the same
customerTransactionID. - There is no partial flag —
statusis the signal, andPartialis the resting state. - A Partial needs both dates. If you do not know the end yet, send
startDateplus 100 ms, and make sure the final send carries a strictly laterendDate. - Derive timestamps from your server clock; future timestamps are rejected.
- On a termination, send your own reason. We map it.