Skip to main content

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​

  1. Your page loads the toolbox with your public key and gets a requestId.
  2. The page sends the requestId to your server.
  3. Your server reads the result with your private key.
  4. Your server sends the transaction record to POST /data with 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​

dqc.js
// 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 Origin header. If your server copies headers from a browser request, remove Origin.
  • Call the lookup once, right after your page hands you the id. It waits for the data to land and answers 200 either way: if the data never arrived, you get the Could not process payload.
  • 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 this requestId.
  • false: this requestId was looked up before with a different Idempotency-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, false means 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 reads false, so lookups made for reporting say nothing.
  • null: the check could not run this time. The result is still valid. If you sent an Idempotency-Key, a second lookup with the same key can still give you the answer. If you sent none, a second lookup reads false because of the first one, so it tells you nothing.
  • Missing: special values such as Could not process come back without the field, and never count as the first lookup: a retry after one is still true.

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 a requestId sends that same value, so with the requestId as the key every replay reads true and 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 requestId can send the first respondent's id and read true again.
  • 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 Origin header gives it away.
  • Put the private key in a URL, a request body, or a VITE_ or NEXT_PUBLIC_ variable. A key in a URL ends up in proxy logs, browser history and HAR files, and a VITE_ or NEXT_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​

  1. Create a public key and set its allowed domains. Create a private key and store it on your server. See Create a key.
  2. Change the toolbox URL in your pages to the public key.
  3. Change your server's lookup and sends to the private key.
  4. Deploy, and check that the toolbox loads, the lookup returns a payload and POST /data answers 200.
  5. 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.
  6. When it has stopped changing for a few days, deactivate the general key.
While a key is deactivated

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​