Public widget API
The DSAR widget and the hosted request page both submit requests through a public HTTP API. If you want to build your own request form, for example inside a mobile app or a design system that cannot use the widget, you can call the same API directly.
The API has two endpoints: one that issues a short-lived submission token and one that creates the request. It is meant to be called from a visitor's browser or from your server. It does not read or change requests that already exist. That is done in the Dashboard.
Base URL
https://dash.cookiehub.com/api/v1/dsarWhat you need
- The account key and domain key of the website the request is for. They are the
data-account-keyanddata-domain-keyvalues in the embed code from Publish form in the Dashboard. See Installing the widget. - An account with the DSAR add-on.
- A
Refererheader that matches the website. See Origin check.
The keys are public: they are visible in every page that embeds the widget. They identify the website; they do not authenticate you.
The flow
- Your form calls
GET /initand receives asubmission_token. - When the visitor submits, your form calls
POST /submitwith the token and the fields. - CookieHub creates the request as unverified and emails the data subject a verification link. If the request is on behalf of someone else, the requester gets a separate link too.
- The data subject opens the link and confirms. The request becomes pending and appears in your Dashboard. See After the request is submitted.
GET /init
Returns a submission token.
GET /api/v1/dsar/init?ak=<account key>&dk=<domain key>| Query parameter | Meaning |
|---|---|
ak | The account key. |
dk | The domain key. |
Success:
{ "submission_token": "..." }Treat the token as an opaque string. It is valid for 30 minutes and tied to the website it was issued for. A token is single use: the first POST /submit that presents it consumes it, even when that submit is then rejected. Fetch a fresh token before every submit.
Failures:
| Status | error_key | When |
|---|---|---|
| 422 | err_inv | A key is missing (API keys are missing.), or the keys are not valid for the same website (Invalid API keys.). |
| 403 | err_unauth | The account does not have DSAR enabled, or the origin check failed. |
| 429 | Too many requests from this address. |
POST /submit
Creates the request. Send JSON with the headers Content-Type: application/json and Accept: application/json. Without Accept: application/json, a validation failure is answered with a redirect instead of the JSON described below.
{
"account_key": "ak_xxxxxxxx",
"domain_key": "dk_yyyyyyyy",
"submission_token": "...",
"name": "Jane Doe",
"email": "jane@example.com",
"relationship": "customer",
"request_types": ["access", "erasure"],
"request_details": "Please send me everything you hold about me.",
"country_code": "DE",
"locale": "de",
"source_url": "https://www.example.com/privacy-requests",
"consent_token": "",
"is_on_behalf": false
}Fields
| Field | Required | Rules |
|---|---|---|
account_key | Yes | The account key. |
domain_key | Yes | The domain key. |
submission_token | Yes | A token from GET /init. |
name | Yes | The data subject's name, up to 255 characters. |
email | Yes | The data subject's email address, up to 255 characters. |
relationship | Yes | One of customer, visitor, employee, former_employee, applicant, other. |
request_types | Yes | A list with at least one of access, erasure, portability, rectification, restriction, object, automated, other. |
request_details | Yes | The message, up to 2,000 characters. |
country_code | No | A two-letter country code. The country decides the jurisdiction suggested for the request. If omitted, CookieHub uses the country detected from the request where it can. |
locale | No | A lowercase language code such as en, pt-br or zh-tw, up to 10 characters. It selects the language of every email the data subject receives. Unsupported codes fall back to English. |
source_url | No | The address of the page the request comes from: http or https, up to 255 characters. If omitted, the Referer header is used. Shown as the Source URL on the request. |
consent_token | No | The visitor's consent ID, up to 64 characters. Links the request to the consent log. |
is_on_behalf | No | true if someone submits for another person. Accepts true, false, 1, 0, on, yes. |
requester_name | If is_on_behalf | The requester's name, up to 255 characters. |
requester_email | If is_on_behalf | The requester's email address. Must differ from email. |
requester_relationship | If is_on_behalf | One of parent, lawyer, agent, family, other. |
subject_phone | No | Leave this out, or send it empty. See Spam protection. |
Leading and trailing spaces are trimmed from text fields, and a field that is empty after trimming counts as not sent.
The keys and the token can also be sent as the headers X-Account-Key, X-Domain-Key and X-Submission-Token. A browser calling the API from your website cannot send these headers, because the API's CORS policy only allows Content-Type and Accept. Put them in the body.
Success
{
"success": true,
"message_key": "ok_received",
"message": "Your request has been received. Please check your inbox, including your spam folder, to verify your identity."
}Status 201. The response does not contain a request ID: the data subject receives the link to their request in the verification email.
Failures
| Status | Body | When |
|---|---|---|
| 422 | error_key: "err_keys" | account_key or domain_key is missing. |
| 422 | error_key: "err_inv" | The keys are invalid or belong to different websites. |
| 422 | message and errors | A field failed validation. errors maps each field name to a list of messages, and message is the first of them. |
| 403 | error_key: "err_unauth" | DSAR is not enabled for the account, or the origin check failed. |
| 403 | error_key: "err_token" | The token is missing, malformed, expired or already used. Reload and fetch a new one. |
| 429 | message | Too many requests. See Rate limits. |
| 500 | error_key: "err_server" | The request could not be stored. Nothing was created; it is safe to try again later. |
Error responses with an error_key also carry a human-readable message in English. If you show your own text, key it on error_key.
If the verification email cannot be sent after the request is stored, the response is still 201 and the failure is recorded on the request. You can resend the email from the request page in the Dashboard.
Origin check
Both endpoints check where the call comes from, to stop another website from using a visitor's browser to submit on your behalf.
- On
GET /initthe origin is theRefererheader. - On
POST /submitthe origin issource_urlif you send it, otherwise theRefererheader.
It must be an http or https address of the website itself, or of one of its subdomains. A leading www. is ignored on both sides. For a website registered as example.com, https://www.example.com/privacy, https://example.com/ and https://help.example.com/ pass, and https://example.org/ does not. The address can be at most 255 characters.
The address of the website's active hosted request page also passes, for that website only.
Browsers send the Referer header themselves, so a form on your website works as long as your site's referrer policy does not remove it (for example no-referrer). Calling the API from your server is possible, but you must set a matching Referer header yourself.
Rate limits
Limits apply per minute, counted from the first request:
| Endpoint | Limit |
|---|---|
GET /init | 20 per IP address |
POST /submit | 10 per IP address, and 60 per website across all addresses |
In addition, each email address, whether the data subject's or the requester's, can receive at most 5 verification emails per hour from submissions, across all your websites. A submission that would exceed it is answered with 429 and creates nothing.
A request over a per-minute limit is answered with 429, the body {"message":"Too Many Attempts."} and a Retry-After header with the seconds to wait.
Spam protection
subject_phone is a hidden decoy field. A person never sees it, but a bot that fills in every field does. If it is present and not empty, the API answers as if the request succeeded (201) and stores nothing. Do not render this field in your form.
CORS
Every response carries these headers, so a browser can call the API from any website:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Accept
Access-Control-Max-Age: 86400OPTIONS requests to any path under /api/v1/dsar/ are answered with 204.
After the request is submitted
The rest of the flow happens on pages that CookieHub hosts and that people open from their email. They are web pages, not JSON endpoints, and you do not need to call them, but it helps to know what the data subject will see:
| Address | What it does |
|---|---|
/dsar/verify/<token> | The link in the verification email. Opening it shows a confirmation button; pressing the button completes the verification. Email scanners that open links therefore cannot verify a request by accident. The link is valid for 30 days. |
/dsar/request/<id> | The data subject's status page: the request's status and deadline, replies to your questions, the download of the disclosure file and, where the law requires it, the appeal form. |
All of these live on https://dash.cookiehub.com. Verification, status and download pages are limited to a few requests per minute per address, and "send the verification email again" on the status page is limited to 3 per hour.
An on-behalf request needs both the data subject and the requester to verify before it becomes pending. Opening a link that is expired, already used or unknown shows a page that says so. A new verification link can be sent from the status page, or from the request in the Dashboard.
Next steps
- Install the widget if you would rather not build the form yourself
- Hosted request page for a ready-made page with a link
- Manage incoming requests