Using a key pair
If part of your integration runs on a server, you can use a public key in your pages and a private key on your server. The key in your pages then can't look up results or send data. Integrations that run entirely in the browser keep using a general key: see Which key for each integration.
How it works
- Your page loads the toolbox with your public key and gets a
requestId. - The page sends the
requestIdto your server. - Your server reads the result with your private key.
- Your server sends the transaction record to
POST /datawith the same private key.
In the page: the public key
<script type="module">
// Your public key (recommended) or your general key. Visible in the page by design.
const GENERAL_OR_PUBLIC_DQC_KEY = 'YOUR_GENERAL_OR_PUBLIC_KEY';
const { DQCToolBox } = await import(
`https://api.dqco-op.com/tools/toolbox/${GENERAL_OR_PUBLIC_DQC_KEY}`
);
const { requestId } = await DQCToolBox.getIdentityRequestId('YOUR_SURVEY_ID');
// Hand the requestId to your server, however your page already talks to it.
await fetch('/dqc/lookup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ requestId }),
});
</script>
Set the key's allowed domains to your survey sites: see Allowed domains.
On your server: the private key
// Node 18+. Your private key (recommended) or your general key. Never ship it to a browser.
const GENERAL_OR_PRIVATE_DQC_KEY = process.env.GENERAL_OR_PRIVATE_DQC_KEY;
const DQC_BASE = 'https://api.dqco-op.com';
const auth = { Authorization: `apikey ${GENERAL_OR_PRIVATE_DQC_KEY}` };
// respondentId: an id your server issued for this person (never one the browser sent), so your
// own repeat reads stay first reads. Optional.
export async function lookup(requestId, respondentId) {
const response = await fetch(
`${DQC_BASE}/tools/request/${encodeURIComponent(requestId)}`,
{
headers: {
...auth,
Accept: 'application/json',
...(respondentId ? { 'Idempotency-Key': String(respondentId) } : {}),
},
}
);
if (!response.ok) {
throw new Error(`DQC lookup ${response.status}: ${await response.text()}`);
}
return response.json();
}
export async function send(record) {
const response = await fetch(`${DQC_BASE}/data`, {
method: 'POST',
headers: { ...auth, 'Content-Type': 'application/json' },
body: JSON.stringify(record),
});
if (!response.ok) {
throw new Error(`DQC send ${response.status}: ${await response.text()}`);
}
}
The same two calls with curl ($RESPONDENT_ID is that same id from your server):
curl "https://api.dqco-op.com/tools/request/$REQUEST_ID" \
-H "Authorization: apikey $GENERAL_OR_PRIVATE_DQC_KEY" \
-H "Idempotency-Key: $RESPONDENT_ID" \
-H "Accept: application/json"
curl -X POST "https://api.dqco-op.com/data" \
-H "Authorization: apikey $GENERAL_OR_PRIVATE_DQC_KEY" \
-H "Content-Type: application/json" \
-d @record.json
- Send the key in the header only, never in the URL or the body.
- Server-side HTTP clients send no
Originheader. If your server copies headers from a browser request, removeOrigin. - Call the lookup once, right after your page hands you the id. It waits for the data to land and
answers
200either way: if the data never arrived, you get theCould not processpayload. - For production, the complete server helper on Routers and Redirects adds fallbacks, so a respondent is never blocked by a failed call, and the in-progress and final sends. The record itself is described on POST /data.
Detect a reused requestId
A requestId travels from the respondent's browser to your server, and nothing ties it to the
person who produced it. Someone can take a clean survey once, keep its requestId, and send it
again from sessions that never ran the toolbox. Every lookup of that id returns the same clean
result.
So every lookup with a private or general key also says whether it is the first one. It is a flag: DQC never refuses the lookup, and you always get the full result. What to do with it is up to you. It only means something when your server does the lookup: a lookup from the respondent's browser can be skipped or replayed by the respondent.
The first lookup is counted once per requestId across all your keys. A lookup from the browser
with a general key counts too, so if your page reads the result and your server then looks the same
id up, the server reads false for every respondent. Look each requestId up from one place: your
server.
{
"requestId": "1793102400000.kXq3Lm",
"deviceScore": 87,
"isDuplicate": false,
"isFirstTimeRetrieved": true,
"firstRetrievedAt": "2026-10-27T12:00:02.000Z"
}
true: the first lookup of thisrequestId.false: thisrequestIdwas looked up before with a differentIdempotency-Key(or none), it was issued more than 24 hours ago, or its issue time can't be read or is in the future. If your server looks it up right after your page hands it over, sending the key every time,falsemeans someone else used the id first: the same signal as a duplicate. A later lookup without the key, or more than 24 hours after the survey, also readsfalse, so lookups made for reporting say nothing.null: the check could not run this time. The result is still valid. If you sent anIdempotency-Key, a second lookup with the same key can still give you the answer. If you sent none, a second lookup readsfalsebecause of the first one, so it tells you nothing.- Missing: special values such as
Could not processcome back without the field, and never count as the first lookup: a retry after one is stilltrue.
The Idempotency-Key header
It is optional. Without it, the flag still works for a server that looks each requestId up once.
Send it if your server ever looks up the same respondent again, so those lookups stay true:
- Your own id for the respondent (or the session), the same on every lookup for that person,
retries included. The same key on different
requestIds is fine. - Not the
requestId. Anyone replaying arequestIdsends that same value, so with therequestIdas the key every replay readstrueand the flag never catches one. - One the respondent can't choose: an id your server issued and stored, never one taken from
the URL or a request body the browser sends. Otherwise someone reusing a
requestIdcan send the first respondent's id and readtrueagain. - At most 256 characters (a longer key gets
400), plain ASCII (HTTP clients reject some other characters before sending).
A repeat lookup with the same key stays true until 24 hours after the requestId was issued. The
key is stored only as a keyed hash and does not need to be secret. Unlike the usual
Idempotency-Key, this lookup has nothing to repeat, so it never replays a stored response or
answers 409: every lookup returns the current result.
The 24-hour limit is the most we allow. Most lookups come seconds after the survey page, so you can
apply a stricter limit of your own: the requestId starts with the time it was issued in epoch
milliseconds (1793102400000 above), and firstRetrievedAt tells you when it was first looked up.
What not to do
Each of these gets a 403, and retrying won't help:
- Call the lookup from the browser with a private key. The
Originheader gives it away. - Put the private key in a URL, a request body, or a
VITE_orNEXT_PUBLIC_variable. A key in a URL ends up in proxy logs, browser history and HAR files, and aVITE_orNEXT_PUBLIC_variable ships to the browser. - Forward a browser request to DQC without removing
Origin.
The message for each case is under private key errors.
Switching an existing integration
- Create a public key and set its allowed domains. Create a private key and store it on your server. See Create a key.
- Change the toolbox URL in your pages to the public key.
- Change your server's lookup and sends to the private key.
- Deploy, and check that the toolbox loads, the lookup returns a payload and
POST /dataanswers200. - Watch Last used on the general key. Surveys already in the field keep using it until they
close. While pages still read results in the browser, your server's lookups of the same ids read
false(see Detect a reused requestId), so don't act on the flag until they stop. - When it has stopped changing for a few days, deactivate the general key.
Anything sent with it to POST /data is rejected and not stored, and surveys still using it get the
Deactivated-Key response, so those respondents aren't scored.
Reactivating the key doesn't bring that data back.
Next steps
- Server-only results: once your server reads the results, keep the scores off the page.