Skip to main content

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 live API reference

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 volumeEndpointWhat you have to do
Under ~1M transactions a monthPOST https://api.dqco-op.com/dataNothing. Use your existing API key and start sending.
Over ~1M transactions a monthA dedicated endpoint, e.g. POST https://api.dqco-op.com/data/yourcompanyTalk 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.

A dedicated path is issued by DQC, not chosen by you

/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.

When to raise your hand

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
A scheme word and a space are required

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 your status and 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

FieldNotes
buyerName or buyerCodeIdentifies the buyer — usually you, the company hosting the survey. Required: exactly one of the two, never both.
sellerName or sellerCodeIdentifies the seller / supplier. Required: exactly one of the two, never both — see Company mapping.
customerTransactionIDYour own id for the response. This is the key we update on, so send a stable one.
surveyNameThe survey the response belongs to.
projectNameThe project the survey sits under. Send it even when it matches surveyName.
startDate / endDateISO 8601 timestamps, entered and exited, e.g. 2026-09-03T14:02:11Z.
statusYour own outcome code. DQC turns it into a disposition — see Dispositions.
countryISO 3166-1 alpha-2, e.g. US. Use ZZ if you genuinely do not know.
Project and survey are two different levels

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.

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.

FieldNotes
requestIdLinks the transaction to its Quality Tools session.
participantIdTies the respondent to their history across surveys — this is what powers the Data Trust Score over time. Required whenever you send a deviceScore.
deviceScoreThe 0-100 device quality score from the tools. Send participantId with it.
dataTrustScoreThe respondent's Data Trust Score.
personaThe respondent's assigned Persona, if you have it.
isDuplicateThe duplicate flag from the tools.
deviceFailuresWhich device signals contributed to the score, as an array of strings, e.g. ["Privacy Settings"]. Pass through whatever the tools returned.
Some requestId values are messages, not ids

Four 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.

FieldWhat it is
substatusYour own reason code behind the status, e.g. speeding. Feeds the failures mapping.
surveyStateWhich environment this response came from — live, testing, staging, dev. See Separating test traffic.
subdivisionRegion within the country, e.g. US-CA.
recruitmentSourceHow the respondent was sourced, e.g. Panel API.
projectTypeB2C, B2B, HEALTH CARE PROFESSIONALS, PATIENTS, MULTIPLE or OTHER. HCP still works but is deprecated.
cpiCost per interview — a number greater than zero, at most 2 decimals, e.g. 4.50. 0 is rejected; omit it instead.
groups / audiencesUp to 5 labels each, e.g. ["Wave1"], ["Gen Pop"].
brandsUp to 12 items shaped { brandId, categoryId } for brand studies. categoryId is optional.
recognizedBrands / brandsShownIntegers, 0 to 32767. They travel together, and recognizedBrands must not exceed brandsShown.
thirdPartyID / thirdPartyIDProviderAn external id and the provider that issued it.
thirdPartyQualityScore / thirdPartyQualityScoreProviderAn external score and its provider.
subSupplierCode / subSupplierNameA panel-within-a-panel under the seller. Send at most one of the two.
tagsFree-form labels for your own filtering.
sourceParticipantIDAn upstream participant id from your side.
failuresThe in-survey checks this respondent tripped — see Failures.
Your survey variablesAnything else. Extra keys are stored, not rejected.
Enum-style values

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

One respondent, one JSON object
{
"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:

GroupDispositionWhat it means
QualificationOverquotaQualified, but the quota was already full.
QualificationDid Not Qualify (DNQ)Screened out by your own criteria.
QualificationDuplicateAlready seen in this survey.
Data QualityAbandonStarted, answered something, left without finishing.
Data QualityOSQFailed quality before the survey — an out-of-survey fail.
Data QualityManual ISQFailed an in-survey check that a human flagged.
Data QualityAutomated ISQFailed an in-survey check that ran automatically.
CompletionFlagged CompleteFinished, but tripped at least one quality check.
CompletionQualified CompleteFinished clean. This is the one you are buying.
Full taxonomy

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.

Two rows that are easy to misread

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.

OptionSendWhen to use it
1. NamesellerName: "Acme Panel Co."Simplest. Use the exact company name, at least 3 characters.
2. CodesellerCode: "ACME01"You already store DQC's identifier code. Least ambiguous.
3. Your own indexsupplierIndex: 2 plus isSupplierIndex: "true"You would rather not put company names in the payload at all. Contact us before using this — see below.
A seller is required, and only one form of it

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.

Getting the names right

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 supplierIndexSupplier
1Company A
2Company B
3Company C
Do not switch this on before we have your table

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.

If you were offered an encrypted seller name

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 sendDQC records
a number, e.g. 2that many failures
true or any non-empty text1 failure
0, false, null, ""no failure

Which keys to use

  • Your own checkshoneyPot, trapQuestion, openEndQuestion and other are 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 checksdqcFraud and dqcDuplicate are 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.

The live list

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:

Your raw record - no reshaping
{
"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:

Rules DQC authors over your fields
# 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"
Send what you have

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:

The transaction DQC writes - you never send this
{
"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.

Confirming a record landed

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.

Disposition names above are the reader-facing ones

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.

This is mapped, not automatic

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.

Send us the whole list at once

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.

Coming from Decipher? terminationReason is not the field

Decipher 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.
Mapping is iterative, and that is by design

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

CodeBodyWhat it meansWhat 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.
403Deactivated-Key markerThe key was deactivated in the dashboard. Nothing is stored.Stop sending and contact us. Do not retry.
429 or 5xxvariesTransient.Back off and retry.
The error key is not always message

Body-shape problems answer with {"error": "..."}; auth and server problems answer with {"message": "..."}. If you are parsing the response, read both.

Body-shape checks run before authentication

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 three 401s are your bug. Retrying sends the same broken record again. Log it, alert on it, and fix the caller.
  • 403 means stop. The key is revoked; retrying will never succeed. Alert a human.
  • 429, 5xx, Authentication failed and 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.

On any non-200, keep the record
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:

dqc-server.js
// 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:

  1. curl returns 200 with your real key.
  2. A wrong key returns 401 — proves you are reading the response at all.
  3. One completed respondent produces exactly one POST (watch for React effects firing twice, and for a router that retries).
  4. requestId and participantId in the payload match what the Quality Tools returned for that respondent.
  5. Every terminal state fires — complete, terminate, quota full and abandon, not just complete.
  6. Killing the network mid-survey still lets the respondent redirect back to the panel.
Send us a sample before you scale

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:

contact@dataqualityco-op.com

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


✅ Summary

  1. POST https://api.dqco-op.com/data, one JSON object per respondent, header Authorization: apikey YOUR_DQC_API_KEY - a scheme word and a space are required.
  2. Over ~1M transactions a month, ask us for a dedicated endpoint.
  3. Send your own field names and values. DQC writes the mapping - you never send a disposition.
  4. A buyer and a seller are required on every record, and exactly one form of each.
  5. 400, 401 and 403 are final; 429, 5xx and a request that never got a reply are worth a retry.