The POST /data endpoint
One endpoint takes everything: POST /data. Send one JSON object per respondent and DQC stores it,
then maps it onto a transaction on your behalf.
This page is the reference - the endpoint, what you can send, and what each value means. For when to send, see Sending Data to DQC. For code, see the JavaScript pages.
The generated spec for every DQC endpoint, including this one, is at api.backend.dqco-op.com/docs. This page is the narrative version - what the fields mean and how we map them. Use the spec for exact schemas.
Choose an endpoint
There are two shapes, and the only thing that decides between them is your volume.
| Your volume | Endpoint | What you have to do |
|---|---|---|
| Under ~1M transactions a month | POST https://api.dqco-op.com/data | Nothing. Use your existing API key and start sending. |
| Over ~1M transactions a month | A dedicated endpoint, e.g. POST https://api.dqco-op.com/data/yourcompany | Talk to us first — we provision it. |
Both endpoints take the same auth, the same payload and return the same responses. A dedicated path exists for isolation, not speed: at high volume we can give your traffic its own processing queue, so a burst or a backlog from another client never sits in front of your records and your failed messages are triaged on their own. Whether a named path also gets its own queue is a call we make from your volume — some named paths share the default one.
/data/yourcompany is not a wildcard — each one is a real endpoint we stand up for a
named customer. Posting to a path we have not provisioned for you will not work. Start
on POST /data; moving to a dedicated endpoint later is a one-line change on your side.
Contact us before you go live if you expect to pass ~1M transactions a month, if your traffic is spiky (a tracker that fires a month of interviews in three days), or if you want to lock ingestion down to a specific domain. All three are easier to set up before launch than after.
Authenticating
One header, on every request:
Authorization: apikey YOUR_DQC_API_KEY
Content-Type: application/json
apikey, ApiKey and Bearer are all accepted — we read the token after the first
space. What does not work is the bare key: Authorization: abc123 returns 401 Api key not provided. If you are seeing that with a key you know is good, the missing space
is almost always why.
What to send
The endpoint takes any JSON object. There is no field whitelist to satisfy — one respondent per request, as flat as you can make it. The more you include, the richer the dashboard.
The tables below are what we look for when we build a transaction out of your data. They are the target we map onto, not a required request format — send your own field names and we write the translation.
Two fields belong to the transaction but never to your request:
disposition— the end result. DQC computes it from yourstatusand the quality signals. See Dispositions; you never send one.customerID— your DQC customer scope. We set it from your API key, so it is never in a payload.
Identity and structure
| Field | Notes |
|---|---|
buyerName or buyerCode | Identifies the buyer — usually you, the company hosting the survey. Required: exactly one of the two, never both. |
sellerName or sellerCode | Identifies the seller / supplier. Required: exactly one of the two, never both — see Company mapping. |
customerTransactionID | Your own id for the response. This is the key we update on, so send a stable one. |
surveyName | The survey the response belongs to. |
projectName | The project the survey sits under. Send it even when it matches surveyName. |
startDate / endDate | ISO 8601 timestamps, entered and exited, e.g. 2026-09-03T14:02:11Z. |
status | Your own outcome code. DQC turns it into a disposition — see Dispositions. |
country | ISO 3166-1 alpha-2, e.g. US. Use ZZ if you genuinely do not know. |
A project can hold many surveys. If today they are the same thing for you, send the same value in both — then nothing has to change the first time a project spans several waves.
Quality Tools fields (strongly recommended)
These are the outputs of the tools you already run. Without at least the first two, a transaction cannot be tied back to its quality result.
| Field | Notes |
|---|---|
requestId | Links the transaction to its Quality Tools session. |
participantId | Ties the respondent to their history across surveys — this is what powers the Data Trust Score over time. Required whenever you send a deviceScore. |
deviceScore | The 0-100 device quality score from the tools. Send participantId with it. |
dataTrustScore | The respondent's Data Trust Score. |
persona | The respondent's assigned Persona, if you have it. |
isDuplicate | The duplicate flag from the tools. |
deviceFailures | Which device signals contributed to the score, as an array of strings, e.g. ["Privacy Settings"]. Pass through whatever the tools returned. |
requestId values are messages, not idsFour values come back in place of a real id when the tools could not score the
respondent: Request-Blocked, Wrapper Tampering, Could not process and
Submission too quick, data not processed. Send them through as-is — do not blank
them out or substitute a fake id. They are recognised on our side, they bypass the
normal id validation, and they change how the response is dispositioned.
Deactivated-Key is different: it means the API key itself was revoked, and ingestion
rejects those requests outright. See
Deactivated Key.
Optional fields
None of these are required. Send whatever you already have; a few travel in pairs.
| Field | What it is |
|---|---|
substatus | Your own reason code behind the status, e.g. speeding. Feeds the failures mapping. |
surveyState | Which environment this response came from — live, testing, staging, dev. See Separating test traffic. |
subdivision | Region within the country, e.g. US-CA. |
recruitmentSource | How the respondent was sourced, e.g. Panel API. |
projectType | B2C, B2B, HEALTH CARE PROFESSIONALS, PATIENTS, MULTIPLE or OTHER. HCP still works but is deprecated. |
cpi | Cost per interview — a number greater than zero, at most 2 decimals, e.g. 4.50. 0 is rejected; omit it instead. |
groups / audiences | Up to 5 labels each, e.g. ["Wave1"], ["Gen Pop"]. |
brands | Up to 12 items shaped { brandId, categoryId } for brand studies. categoryId is optional. |
recognizedBrands / brandsShown | Integers, 0 to 32767. They travel together, and recognizedBrands must not exceed brandsShown. |
thirdPartyID / thirdPartyIDProvider | An external id and the provider that issued it. |
thirdPartyQualityScore / thirdPartyQualityScoreProvider | An external score and its provider. |
subSupplierCode / subSupplierName | A panel-within-a-panel under the seller. Send at most one of the two. |
tags | Free-form labels for your own filtering. |
sourceParticipantID | An upstream participant id from your side. |
failures | The in-survey checks this respondent tripped — see Failures. |
| Your survey variables | Anything else. Extra keys are stored, not rejected. |
disposition, projectType, recruitmentSource and country draw on DQC's /values/*
lookups. You do not have to memorise them — we align on the exact values during setup,
and you can keep sending your own codes.
A full payload
{
"customerTransactionID": "abc-123",
"projectName": "Q3 Brand Tracker",
"surveyName": "Wave 1",
"buyerName": "Your Company",
"sellerName": "Acme Panel Co.",
"subSupplierName": "Acme Sub-panel A",
"requestId": "1757416517972.rWZN1D",
"participantId": "q6VCgbAXzpZCtZNCL0LF",
"deviceScore": 85,
"dataTrustScore": 780,
"persona": "Skeptic",
"isDuplicate": false,
"status": "3",
"substatus": "speeding",
"startDate": "2026-09-03T14:02:11Z",
"endDate": "2026-09-03T14:14:52Z",
"country": "US",
"subdivision": "US-CA",
"projectType": "B2C",
"recruitmentSource": "Panel API",
"cpi": 4.5,
"groups": ["Wave1"],
"audiences": ["Gen Pop"],
"tags": ["pilot"],
"brandsShown": 8,
"recognizedBrands": 5,
"brands": [{ "brandId": 42, "categoryId": 7 }],
"thirdPartyID": "RD-99182",
"thirdPartyIDProvider": "Research Defender",
"thirdPartyQualityScore": 0.92,
"thirdPartyQualityScoreProvider": "Research Defender",
"failures": { "trapQuestion": 1, "honeyPot": 0 },
"q1": "Yes",
"q2": "18-24",
"q3": "Weekly"
}
Dispositions
A disposition is the end result of a response. DQC computes it — you never send one. You send your status, your substatus, and the quality signals; the rules above decide the disposition.
There are nine, in three groups:
| Group | Disposition | What it means |
|---|---|---|
| Qualification | Overquota | Qualified, but the quota was already full. |
| Qualification | Did Not Qualify (DNQ) | Screened out by your own criteria. |
| Qualification | Duplicate | Already seen in this survey. |
| Data Quality | Abandon | Started, answered something, left without finishing. |
| Data Quality | OSQ | Failed quality before the survey — an out-of-survey fail. |
| Data Quality | Manual ISQ | Failed an in-survey check that a human flagged. |
| Data Quality | Automated ISQ | Failed an in-survey check that ran automatically. |
| Completion | Flagged Complete | Finished, but tripped at least one quality check. |
| Completion | Qualified Complete | Finished clean. This is the one you are buying. |
The definitive definitions, including exactly what pushes a response from Qualified Complete to Flagged Complete, live in Dispositions. The Decipher decision map is a worked example of the same rules applied to one platform's status values — useful to read even if you are not on Decipher.
A isDuplicate flag from the Quality Tools is not automatically the Duplicate
disposition — that depends on how your rules treat it. And a response that genuinely
completed is never OSQ; the worst it gets is Flagged Complete.
Company mapping — buyer and seller
Every response DQC receives is a transaction between two companies: the buyer (you, hosting the survey) and the seller (the panel or supplier the respondent came from).
The buyer is usually one static value:
buyerName: 'Your Company',
The seller changes per respondent, and you have three ways to identify it. Send exactly one of them.
| Option | Send | When to use it |
|---|---|---|
| 1. Name | sellerName: "Acme Panel Co." | Simplest. Use the exact company name, at least 3 characters. |
| 2. Code | sellerCode: "ACME01" | You already store DQC's identifier code. Least ambiguous. |
| 3. Your own index | supplierIndex: 2 plus isSupplierIndex: "true" | You would rather not put company names in the payload at all. Contact us before using this — see below. |
Every record needs one. A response with no identifiable seller cannot become a
transaction. Sending both sellerName and sellerCode fails the record just as
surely as sending neither — it is exactly one or the other. Same rule for the buyer. If a
respondent's source is genuinely unknown, tell us during setup and we will agree on how
to represent it.
DQC identifies companies by a canonical name and code. Look up the exact records with the Company Names and Codes search tool. If a name you send does not match, we do not drop it silently — it is flagged so we can follow up and map it correctly.
If you cannot send company names
Option 3 solves this with a plain lookup: you send a stable id of your own (1, 2,
3, or your internal supplier key), and DQC keeps the lookup table on our side, so no
company name ever leaves your systems.
This option starts with a conversation. You share the table with us once, out of band, and we load it before you send anything:
Your supplierIndex | Supplier |
|---|---|
1 | Company A |
2 | Company B |
3 | Company C |
isSupplierIndex: "true" takes precedence over every other seller field. Until your
lookup table is loaded, a record carrying it is rejected rather than stored — including
one that also carries a perfectly good sellerName. Contact us first.
Some earlier integrations agreed a scheme where you encrypt each supplier name once,
store the ciphertext, and send that instead of the plaintext. That still works and we
still decrypt it — but supplierIndex reaches the same place with less on your side:
no key management, no ciphertext column, no re-encryption when a name changes. If you
have already built the encrypted-name path, keep it; if you are choosing now, choose the
index. Either way, tell us which one you are sending during setup.
Failures
A failure is an in-survey quality check that a respondent tripped. They are what turn a finished response into Flagged Complete and what drive the ISQ dispositions.
Send them as an object keyed by check name:
{
"failures": {
"honeyPot": 1,
"trapQuestion": 0,
"openEndQuestion": 2
}
}
How a value is read:
| You send | DQC records |
|---|---|
a number, e.g. 2 | that many failures |
true or any non-empty text | 1 failure |
0, false, null, "" | no failure |
Which keys to use
- Your own checks —
honeyPot,trapQuestion,openEndQuestionandotherare the common ones, and any key you run can be mapped. The catch is that keys are matched verbatim against the list we agreed, so tell us your names during mapping: a key we are not expecting is dropped rather than stored, and a typo costs you that check. - DQC's own checks —
dqcFraudanddqcDuplicateare computed on our end from the Quality Tools signals. Do not send these.
You can also skip the failures object entirely and let the mapping derive failures from
your substatus, as shown in the rules above. Both work; the
explicit object is clearer if you already track checks individually.
Accepted quality check names are rendered from our database rather than written down, so they are never out of date: Quality Checks and Failures.
Data mapping — your fields to a transaction
DQC writes a custom rule set over your raw fields. Your field names, your status codes, your quality-check columns. We translate them.
The data you send
Whatever your app already produces. If your outcome lives under a differently-named attribute, or uses your own numeric codes, send it as-is:
{
"customerTransactionID": "abc-123",
"projectName": "Q3 Brand Tracker",
"surveyName": "Wave 1",
"sellerName": "Acme Panel Co.",
"requestId": "1757416517972.rWZN1D",
"participantId": "q6VCgbAXzpZCtZNCL0LF",
"deviceScore": 85,
"dataTrustScore": 780,
"status": "3",
"substatus": "speeding",
"startDate": "2026-09-03T14:02:11Z",
"endDate": "2026-09-03T14:14:52Z",
"country": "US"
}
A custom set of rules
If you already have a status / substatus column — terminations, in-survey quality checks, quota logic — just send its value. We map it on our end, and more than one rule can hang off a single status. Illustrative:
# seller - send a name, a code, or your own index; DQC resolves it to a Company
sellerName == "Acme Panel Co." -> Company: Acme Panel Co.
# your status -> a DQC disposition (send your existing status value)
status == 3 -> disposition = "Qualified Complete" # a complete
status == 2 -> disposition = "Overquota"
status == 1 -> disposition = "Did Not Qualify"
status == 4 -> disposition = "Abandon"
# your substatus -> a quality-check failure
substatus == "speeding" -> failure "Speeding"
substatus == "straightline" -> failure "Straight Lining"
substatus == "open_end" -> failure "Open End Question"
substatus == 4 -> failure "Trap Question" # numeric codes are fine too
# you can attach MULTIPLE rules to one record - status + substatus together
status == 5 (terminate) + substatus == "speeding"
-> disposition = "OSQ" + failure "Speeding"
# quality signals, applied on top of your status. NOTE: neither of these
# overrides a completed response - a respondent who finished is never OSQ,
# and the bare isDuplicate flag is not a Duplicate disposition. On a
# complete they attach the failure, which makes it a Flagged Complete.
deviceScore <= 10 -> failure "DQC Fraud" (disposition = OSQ only on a terminate)
isDuplicate -> failure "DQC Duplicate" (your status still decides the disposition)
# timing and country normalisation
no endDate -> endDate = startDate + duration
country -> upper-case ISO alpha-2; default "ZZ"
Do not reshape your data to fit us. You do not need to match our vocabulary, our field names, or our conventions. Send your values and we write the rules that translate them.
What we write
Applying those rules to the record above produces the transaction
DQC writes on your behalf. Buyer Your Company, seller Acme Panel Co.; status: "3" with a
deviceScore of 85 becomes a Qualified Complete:
{
"customerTransactionID": "abc-123",
"buyerName": "Your Company",
"sellerName": "Acme Panel Co.",
"projectName": "Q3 Brand Tracker",
"surveyName": "Wave 1",
"startDate": "2026-09-03T14:02:11Z",
"endDate": "2026-09-03T14:14:52Z",
"disposition": "Qualified Complete",
"country": "US",
"requestId": "1757416517972.rWZN1D",
"participantId": "q6VCgbAXzpZCtZNCL0LF",
"deviceScore": 85
}
This is the record that reaches your dashboard. You never build it and you never call the
endpoint that stores it — DQC writes it through the Transaction API. It matters because it
is what you check against when you confirm the mapping did what you expected: your
status became the disposition you agreed, your seller resolved to the right company, and
your quality signals came along with it.
Mapping runs on a schedule after ingestion, so a record does not appear the moment
POST /data returns 200. Send a handful, wait for the next cycle, then check the
dashboard — or ask us and we will confirm what arrived and how it mapped.
They match Dispositions. The literal values written to
the Transaction API differ in places — Overquota is stored as QUOTA FULL, for
instance. You never send these, so it only matters if you are reading exports; we confirm
the exact values during mapping.
Separating test traffic
If you run more than one environment — a staging copy, a QA pass, a soak test — send a field
saying which one a response came from. Call it surveyState, or whatever you already call it.
{
"surveyName": "Surveys 2027",
"surveyState": "testing"
}
We append it to the survey name, so the same survey in two environments lands as two entries instead of one mixed pile:
Surveys 2027 (live)
Surveys 2027 (testing)
Surveys 2027 (staging)
That gives you something to filter on in the dashboard, and it keeps a week of QA traffic out of your completion and failure rates.
The Decipher integration already works this way — it sends decipherSurveyState and the pipeline
builds identifier (state) from it. For your own integration the field is yours to name, and we
wire it up during
phase 2.
Tell us the
values you use — we do not assume live and testing unless you say so.
Values Decipher uses, if you want a set to copy: dev, testing, live, closed.
Tell us why it ended
Whatever ended a respondent's session — a speeding check, a weak open end, a trap question, a full quota, your own screener, or nothing at all because they walked away — tell us, in your own words.
Put the reason in substatus and any in-survey checks they tripped in failures:
{
"status": "Terminated",
"substatus": "failed_attention_check",
"failures": { "speeding": 1, "openEnd": 0 }
}
You do not need our vocabulary. "speeding", "quota_full_wave2", "OE_GIBBERISH" and 4 all
work, because we write the rules that translate your values into DQC dispositions and quality-check
failures — see A custom set of rules.
If you already have a fixed set of reason codes, send the list at the start rather than one value at a time. We map it in one pass during phase 2, instead of coming back to you each time a new code shows up in the data.
terminationReason is not the fieldDecipher payloads carry a terminationReason, but it is specific to that integration and
recognises exactly four values — duplicate, devicescore, datatrustscore and persona — the
terminations the Decipher script fires on its own.
It is not a place for your own codes. An unrecognised value is not an error; it quietly lands as
Did Not Qualify. Use substatus.
Who provides what
- You provide: your variable list or a sample of real records, the seller names (or codes, or your index), your status and substatus values, what the fields mean, and any rules only you know.
- DQC provides: the Company, Project and Survey records, the mapping rules, storage, and the dashboard.
Not every field maps on day one. We start collecting first and map second, against real records. Expect a round or two of clarifying questions — a value we expected and did not receive, or one that turns out to mean two different things. That back-and-forth is normal.
Responses and error handling
| Code | Body | What it means | What to do |
|---|---|---|---|
200 | {"message":"Integration data stored successfully"} | Stored. | Nothing. |
400 | {"error":"Request body is required and must be a valid JSON value."} | Empty body. | Fix the caller. Do not retry. |
400 | {"error":"Request body is not valid JSON."} | The body did not parse. | Fix the caller. Do not retry. |
400 | {"error":"Request body must be a JSON object (got array or primitive)."} | You sent an array, string or number. | Send one object per request. |
401 | {"message":"Missing Authorization header"} | No header and no apiKey in the body. | Add the header. |
401 | {"message":"Api key not provided"} | Header present, but nothing after the scheme word. | Check for the apikey prefix. |
401 | {"message":"Invalid ApiKey"} | The key is not recognised. | Check the key, and that it is the right environment. |
401 | {"message":"Authentication failed ..."} | We could not complete the key lookup. | Transient. Back off and retry. |
403 | Deactivated-Key marker | The key was deactivated in the dashboard. Nothing is stored. | Stop sending and contact us. Do not retry. |
429 or 5xx | varies | Transient. | Back off and retry. |
messageBody-shape problems answer with {"error": "..."}; auth and server problems answer with
{"message": "..."}. If you are parsing the response, read both.
An array body with no Authorization header returns 400, not 401. If you are
debugging a missing header, fix the body shape first or you will chase the wrong error.
Retry policy
- The three body-shape
400s and the first three401s are your bug. Retrying sends the same broken record again. Log it, alert on it, and fix the caller. 403means stop. The key is revoked; retrying will never succeed. Alert a human.429,5xx,Authentication failedand a request that never got a reply are worth retrying. Two or three attempts with exponential backoff and a little jitter is plenty. Anything that still fails belongs in a small local queue or a log you can replay, not in a tight retry loop.- Never block the respondent. A short timeout, a logged failure, and the redirect fires anyway. Losing one record is an operational problem; a respondent stuck on a spinner is a dead interview.
Log what you could not send
Whenever the response is not a 200, write the payload you tried to send to your own logs.
That one habit is what makes a bad day recoverable. If a key expires overnight, a deploy breaks a field, or we have an incident, the records are still sitting in your logs - you replay them, or you send them to us and we reconcile on our side. Without them the responses are simply gone, and neither of us can reconstruct what a respondent did.
const ok = await sendToDQC(payload);
if (!ok) {
// Keep the whole payload, not just the error. This is what gets replayed.
logger.error("DQC send failed - queued for replay", {
customerTransactionID: payload.customerTransactionID,
payload: payload,
});
}
The reference implementation
This is the server-side helper the JavaScript integration uses. It covers the lookup and both sends, and it is the same file rendered on Routers and Redirects:
// Node 18+ (needs global fetch), ESM. In a CommonJS project either add
// "type": "module" to package.json or save this as dqc-server.mjs.
// Not Node? The three calls below are plain HTTPS - port them anywhere.
const DQC_API_KEY = process.env.DQC_API_KEY; // from your own secret store
const DQC_BASE = "https://api.dqco-op.com";
// DQC's own marker for "we could not score this respondent". Reused below as
// the default whenever a lookup does not give us real values.
const COULD_NOT_PROCESS = "Could not process";
/**
* The exact values DQC returns when it could not score someone. Your server
* answers with the same shape when the lookup itself fails, so there is one
* payload shape downstream and nothing has to tell the two cases apart.
*
* The marker sits on participantId on purpose - that is the field our
* termination rules check. The scores stay 0, and 0 passes every threshold,
* so the marker is what stops a real respondent being terminated as fraud.
*/
function couldNotProcess() {
const noData = "No data";
return {
participantId: COULD_NOT_PROCESS,
surveyId: COULD_NOT_PROCESS,
country: COULD_NOT_PROCESS,
subdivision: COULD_NOT_PROCESS,
deviceFailures: [COULD_NOT_PROCESS],
deviceScore: 0,
dataTrustScore: 0,
persona: "NONE",
isDuplicate: false,
averageDeviceScore: 0,
lowestDeviceScore: 0,
totalSurveys: 0,
completionRate: noData,
duplicationRate: noData,
failureRate: noData,
qualificationRate: noData,
manualISQRate: noData,
automatedISQRate: noData,
osqRate: noData,
lastSurveyTaken: noData,
brandFamiliarity: 0,
openEnd: 0,
speeding: 0,
honeyPot: 0,
straightlining: 0,
distinctSupplierCount: 0,
suppliers: [],
};
}
/**
* Exchanges a requestId for the quality payload.
*
* Never returns null and never throws. If DQC cannot be reached you get the
* "Could not process" payload back - the same shape DQC sends when it could
* not score someone. Send whatever comes back through unchanged.
*/
export async function fetchQualityPayload(requestId) {
try {
const response = await fetch(
`${DQC_BASE}/tools/request/${encodeURIComponent(requestId)}`,
{
method: "GET",
headers: {
// A scheme word plus a space. "Bearer" and "ApiKey" work too.
Authorization: `apikey ${DQC_API_KEY}`,
Accept: "application/json",
},
}
);
if (!response.ok) {
console.error("DQC lookup failed:", response.status);
return couldNotProcess();
}
return await response.json();
} catch (err) {
console.error("DQC lookup failed:", err);
return couldNotProcess();
}
}
/**
* Sends one record. Returns true if DQC stored it. Never throws.
*
* Both failure paths log the payload. A rejected record is gone unless you
* kept it, and the log line is the cheapest place to keep it - you can replay
* from your logs without having stored anything else.
*
* It is respondent data, though. No API key (that rides in the header, never
* the body), but a participantId, a country and the quality scores are in
* there - so give these logs whatever retention and access rules the rest of
* your respondent data already has.
*/
export async function sendToDQC(payload) {
try {
const response = await fetch(`${DQC_BASE}/data`, {
method: "POST",
headers: {
Authorization: `apikey ${DQC_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
console.error(
"DQC rejected the record:",
response.status,
await response.text(),
JSON.stringify(payload)
);
return false;
}
return true;
} catch (err) {
console.error("DQC send failed:", err, JSON.stringify(payload));
return false;
}
}
/**
* The first record. Send it as soon as you have an id for the respondent.
*
* STORE THE RECORD THIS RETURNS against your own id for this respondent.
* sendFinal has to send all of it again - see the note there - and nothing
* else in your system has it.
*/
export async function sendPartial({ requestId, customerTransactionID, session }) {
const quality = await fetchQualityPayload(requestId);
// Mid-flow you do not know when they finish, but a record with no endDate is
// dropped during mapping - silently, after POST /data has already said 200.
// A placeholder 100 ms after the start keeps it valid. The final send
// replaces it, and must carry a strictly later endDate than this one.
const startDate = new Date().toISOString();
const endDate = new Date(Date.parse(startDate) + 100).toISOString();
const record = {
// Everything the lookup gave us, passed straight through. Spreading beats
// picking fields by hand: the endpoint grows, and a field you forgot to
// copy is a field we never see. Extra keys are stored as-is and cost you
// nothing.
...quality,
// Yours. Listed after the spread so they always win.
customerTransactionID: customerTransactionID,
projectName: session.projectName, // dynamic - from your DB or query params
surveyName: session.surveyName, // dynamic - from your DB or query params
sellerName: session.sellerName, // dynamic - the panel this respondent came from
buyerName: "Your Company", // static - this one is you
status: "Partial",
startDate: startDate,
endDate: endDate,
requestId: requestId,
};
const ok = await sendToDQC(record);
// Store this whole record. sendFinal takes it back and resends it with the
// outcome filled in.
return { ok: ok, record: record };
}
/**
* The final record, when the respondent reaches a terminal state.
*
* stored - the record sendPartial returned, looked up by your own
* customerTransactionID.
* outcome - your result: status, substatus, failures.
*
* Send the stored fields AGAIN. An omitted field is not "leave it as it was":
* most columns are written on every send, so leaving one out overwrites what
* we already had with nothing. If you can only keep one thing, keep the
* requestId - it is how we tie the row back to the rest on our side.
*/
export async function sendFinal({ customerTransactionID, stored, outcome }) {
// The partial already reserved startDate + 100 ms. Ours has to beat that, or
// a respondent who bounces in under 100 ms sends a final that looks OLDER
// than its own partial - and the two can no longer be ordered.
const partialEnd = Date.parse(stored.startDate) + 100;
const endDate = new Date(Math.max(Date.now(), partialEnd + 100)).toISOString();
const record = {
// Everything the partial sent, sent again unchanged.
...stored,
customerTransactionID: customerTransactionID,
status: outcome.status, // your own outcome code
substatus: outcome.substatus, // your own reason code, if any
failures: outcome.failures, // e.g. { trapQuestion: 1, honeyPot: 0 }
endDate: endDate, // the real one now
// Add anything else you already track. We store the record as you send it
// and map your names later, so third-party scores, your own quality flags,
// panel ids, an isTest flag - all fine, and none of them have to be ours.
};
const ok = await sendToDQC(record);
return { ok: ok, record: record };
}
200 does not mean "in the dashboard"It means we accepted and stored your record. Mapping into transactions runs on a
schedule afterwards, so the dashboard is near-real-time rather than instant. Do not poll
for a record immediately after a 200 and conclude it was lost.
Testing your integration
Smoke-test the endpoint and the key before you wire up any application code:
curl -i -X POST https://api.dqco-op.com/data \
-H "Authorization: apikey $DQC_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customerTransactionID": "smoke-test-1",
"projectName": "Integration Test",
"surveyName": "Integration Test",
"buyerName": "Your Company",
"sellerName": "Acme Panel Co.",
"status": "3",
"startDate": "2026-09-10T12:00:00Z",
"endDate": "2026-09-10T12:04:00Z",
"country": "US"
}'
A working setup returns:
HTTP/2 200
content-type: application/json
{"message":"Integration data stored successfully"}
Then work through this before you launch:
-
curlreturns200with your real key. - A wrong key returns
401— proves you are reading the response at all. - One completed respondent produces exactly one
POST(watch for React effects firing twice, and for a router that retries). -
requestIdandparticipantIdin the payload match what the Quality Tools returned for that respondent. - Every terminal state fires — complete, terminate, quota full and abandon, not just complete.
- Killing the network mid-survey still lets the respondent redirect back to the panel.
Once a handful of real records have landed, tell us. We will look at what actually arrived, start the mapping, and come back with the questions that only real data surfaces. That is much cheaper than discovering a field means something else after 50,000 interviews.
Getting in touch
Several things on this page need a word with us first — a dedicated endpoint, a supplierIndex
mapping, a deactivated key. All of them go to the same place:
If you are already working with someone at DQC — whoever set up your account, or ran your onboarding call — write to them directly instead. They have your account context and will be faster.
Next steps
- API reference - the generated spec for every DQC endpoint.
- Sending Data to DQC - the lifecycle: which status to send, when, and the two worked examples.
- Integration phases - what happens after you start sending.
- Dispositions - the full taxonomy.
- Company Names and Codes - look up exact buyer and seller records.
✅ Summary
POST https://api.dqco-op.com/data, one JSON object per respondent, headerAuthorization: apikey YOUR_DQC_API_KEY- a scheme word and a space are required.- Over ~1M transactions a month, ask us for a dedicated endpoint.
- Send your own field names and values. DQC writes the mapping - you never send a
disposition. - A buyer and a seller are required on every record, and exactly one form of each.
400,401and403are final;429,5xxand a request that never got a reply are worth a retry.