Skip to content

REST API (Tool API & Server-to-Server)

The browser SDK talks to the Tool API. You can call the same API from your own backend — to record consents collected in your own UI, to mint one-time approval tokens, or to look up and verify records.

Base URL: https://system.hashentry.com

MethodPathPurposeAuth
GET/tool/v1/configBanner configuration, attached documents, detected regionAPI key (read)
GET/tool/v1/documents/{slug}/activeActive version of one document, including contentAPI key (read)
GET/tool/v1/documents/activeActive documents attached to the siteAPI key (read)
POST/tool/v1/consentsRecord one consent (browser / mobile SDK route)API key (write) + signature
POST/tool/v1/consents/batchRecord up to 100 consentsAPI key (write) + signature
POST/tool/v1/s2s/consentsRecord one consent from your backendAPI key (write) + JWT + signature
POST/tool/v1/s2s/consents/batchRecord up to 100 consents from your backendAPI key (write) + JWT + signature
POST/tool/v1/s2s/approval-tokensMint a one-time document approval tokenAPI key (write) + JWT + signature
GET/tool/v1/consents/statusCurrent consents of one visitorAPI key (read)
GET/tool/v1/consents/verify/{id}Integrity check of one recordAPI key (read)
GET POST/api/v1/public/consents/verifyPublic integrity check (no key)none

Every Tool API request carries the site’s key:

X-API-Key: he_live_…

A key has a scope: read allows the GET endpoints, write additionally allows POST. The site must be verified in the dashboard. If the site has an allowed-origins list, browser requests from other origins are rejected; if it has an IP allow-list, requests from other addresses are rejected.

X-Signature: <hex HMAC-SHA256>
X-Timestamp: <Unix time in seconds>
X-Nonce: <unique value per request>
body_hash = SHA-256(raw request body) (lowercase hex)
canonical = METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + body_hash
signature = HMAC-SHA256(key = API key, message = canonical) (lowercase hex)
  • METHOD is upper case. PATH is the request path including /tool/v1, without the query string, e.g. /tool/v1/s2s/consents.
  • The body hash is computed over the exact bytes you send. Serialise the JSON once and send that same string.
  • X-Timestamp must be within ±300 seconds of server time.
  • X-Nonce is 8–128 characters from A–Z a–z 0–9 . _ : - (a UUID works) and can be used only once per site; a repeat is rejected with NONCE_REPLAYED.

Worked example (the shared test vector of the SDK and the API):

API key : he_live_0123456789abcdef0123456789abcdef
Method : POST
Path : /tool/v1/consents
Timestamp : 1760000000
Nonce : 3f2b8c1e-5a47-4d0e-9b6a-7c1d2e3f4a5b
Body : {"consent_type":"cookie","decision":"accepted","language":"tr","idempotency_key":"9a1c2b3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d","metadata":{"note":"çerez ✓"}}
body_hash : 93316d8d6adb505a19668f4022e4e33b12aed9c0c4bb97d205b5064257090d34
canonical : POST\n/tool/v1/consents\n1760000000\n3f2b8c1e-5a47-4d0e-9b6a-7c1d2e3f4a5b\n93316d8d6adb505a19668f4022e4e33b12aed9c0c4bb97d205b5064257090d34
signature : e108683927badf35c36d5f467f63a9704c131d0102f14b06af7ed3046d44a8be

(\n stands for a single line-feed character; the body is UTF-8.) If your implementation produces this signature for these inputs, it is correct.

Node.js
const crypto = require('crypto');
function sign(apiKey, method, path, body) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomUUID();
const bodyHash = crypto.createHash('sha256').update(body, 'utf8').digest('hex');
const canonical = [method.toUpperCase(), path, timestamp, nonce, bodyHash].join('\n');
const signature = crypto.createHmac('sha256', apiKey).update(canonical, 'utf8').digest('hex');
return { 'X-Signature': signature, 'X-Timestamp': timestamp, 'X-Nonce': nonce };
}
const body = JSON.stringify({ consent_type: 'document_approval', decision: 'accepted', /* … */ });
const headers = {
'Content-Type': 'application/json',
'X-API-Key': apiKey,
Authorization: `Bearer ${jwt}`, // S2S routes only
...sign(apiKey, 'POST', '/tool/v1/s2s/consents', body),
};

The signature proves that the request was made with the API key and was not replayed. Because the browser SDK uses the same key, which is visible in page source, the signature alone does not prove that a request came from your backend — that is what the S2S routes are for.

Server-to-server routes (/tool/v1/s2s/…)

Section titled “Server-to-server routes (/tool/v1/s2s/…)”

S2S requests need three things: a write API key, the request signature, and a JWT of a user with the api_user role:

  1. Invite a team member with the role api_user in the dashboard. This role can only use the S2S API; it cannot sign in to the panel.
  2. Sign in with POST /api/v1/auth/login to obtain a JWT; renew it with POST /api/v1/auth/refresh.
  3. Send Authorization: Bearer <jwt> together with X-API-Key and the signature headers. The user must belong to the tenant that owns the site.

Only S2S requests may set ip_address, user_agent, otp_verified and otp_reference. On the browser route the IP address and user agent are always taken from the connection, and the OTP fields are ignored.


POST /tool/v1/consents (or /tool/v1/s2s/consents)

{
"consent_type": "cookie",
"decision": "partial",
"document_version_id": "0190a1b2-…",
"language": "en",
"categories": { "necessary": true, "analytics": true, "marketing": false },
"idempotency_key": "9a1c2b3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
"visitor_id": "user-4711",
"visitor_region": "DE",
"compliance_framework": "gdpr",
"metadata": { "order_id": "ORD-54321" }
}
FieldRules
consent_typeRequired. Up to 50 characters (A–Z a–z 0–9 _ - .). In use: cookie, document_approval, do_not_sell_opt_out.
decisionRequired. accepted, rejected, partial or withdrawn.
document_version_idUUID of an active version of a document attached to the site. Without it — or with a version that is not active — the request is rejected with 409 CONFIG_OUTDATED. A cookie consent references the site’s cookie_policy document.
languageIf given, it must be the language of that version.
categoriesObject of category key → boolean. Only categories defined in the banner configuration are stored.
idempotency_key8–64 characters (A–Z a–z 0–9 . _ : -). See below.
visitor_idYour reference for the person, up to 100 characters.
visitor_region, visitor_countryUp to 10 / 2 characters.
compliance_frameworkkvkk, gdpr, uk_gdpr, ccpa, lgpd, popia, pipeda, pipl, pipa or none.
sourceUp to 20 characters, e.g. client, server.
metadata, extra_fieldsObjects with at most 30 keys and about 16 KB each.
approval_tokenOne-time token for a document_approval (see below).
Signature / custom document fieldssignature_image, signature_mime_type, signer_name, signer_email, signer_phone, signer_id_number, declaration_text, drawing_duration_ms, stroke_count, strokes_data, input_type, screen_resolution, custom_document_content, custom_document_mime_type — all top-level. See Document Approval & Signature.
ip_address, user_agent, otp_verified, otp_referenceS2S only.

Get the document_version_id from GET /tool/v1/config?language=en (data.documents[].version.id) or from GET /tool/v1/documents/{slug}/active?language=en.

{
"message": "…",
"data": {
"consent_token": "ct_…",
"proof_hash": "…64 hex characters…",
"created_at": "2026-10-10T10:00:00+00:00",
"duplicate": false
}
}

202 means the request passed all checks and the record was accepted for writing. The token, the proof hash and the timestamp are final at that point, but the row is written by a background worker, so status and verify may not find it for a short moment.

Send one key per user action and reuse it on every retry of that action. A repeated request with the same key does not create a second record and does not consume quota; it returns the original response with "duplicate": true. Keys are remembered for 24 hours. The browser SDK generates one UUID per decision and uses it for the first attempt, client retries and replays from its offline queue.

POST /tool/v1/consents/batch with { "consents": [ { … }, … ] } (1–100 items, same fields as above).

{
"data": {
"accepted": 2,
"rejected": 1,
"results": [
{ "index": 0, "status": "accepted", "consent_token": "ct_…", "proof_hash": "…", "created_at": "…", "duplicate": false },
{ "index": 1, "status": "accepted", "consent_token": "ct_…", "proof_hash": "…", "created_at": "…", "duplicate": true },
{ "index": 2, "status": "rejected", "error": "…", "error_code": "CONSENT_LIMIT_EXCEEDED", "code": 429 }
]
}
}

The status is 202 when the batch was processed; check each entry of results. Validation errors and an item with an unknown or inactive document version reject the whole batch (422 / 409).


POST /tool/v1/s2s/approval-tokens — S2S only.

{
"document_slug": "service-contract",
"subject": "user-4711",
"signer_name": "Jane Doe",
"signer_email": "[email protected]",
"expires_in": 600
}
FieldRules
document_slug / document_version_idAt least one. With a version id the token is valid for that version only.
subjectRequired. Your reference for the person (max. 100 characters); stored as the record’s visitor_id.
signer_name, signer_email, signer_phone, signer_id_numberOptional. Identity details you vouch for.
otp_verified, otp_referenceOptional. Declare an OTP check you performed.
custom_document_hashOptional. SHA-256 (hex) of a document generated by your system; the approval must then carry exactly that content.
expires_inSeconds, 30–3600. Default 600.

Response 201:

{
"data": {
"approval_token": "hat_…",
"approval_token_id": "atk_…",
"expires_in": 600,
"expires_at": "2026-10-10T10:10:00+00:00",
"document_id": "…",
"document_slug": "service-contract",
"document_version_id": null
}
}

Give approval_token to the page and pass it to the SDK (approvalToken) or send it as top-level approval_token of a document_approval consent. The server consumes it once, takes the identity from the token and records identity_assurance: tenant_attested together with the non-secret approval_token_id. A request that is rejected for another reason (rate limit, quota) does not consume the token. HashEntry stores only a hash of the token.


GET /tool/v1/consents/status?visitor_id=…

Section titled “GET /tool/v1/consents/status?visitor_id=…”

Returns up to 20 unexpired consents of one visitor_id, newest first. Optional filters: consent_type, document_slug, document_version_id.

{id} is a consent UUID, a consent_token (ct_…) or a proof_hash.

{
"valid": true,
"data": {
"consent_type": "cookie",
"decision": "accepted",
"proof_hash": "…",
"hash_version": 2,
"hash_valid": true,
"merkle_valid": null,
"is_anonymized": false,
"is_tsa_stamped": false,
"created_at": "2026-10-10T10:00:00+00:00"
}
}

What this answer means — and what it does not:

  • valid: true means: the record belongs to your tenant, HashEntry recomputed the proof hash from the stored record with your tenant’s secret key and it matches, and — if the record is part of a Merkle batch — its inclusion proof is correct. The existence of a row is not enough.
  • merkle_valid is null when the record has no Merkle proof.
  • is_tsa_stamped says that a timestamp token exists for the record (directly or through its Merkle batch). It does not say that the token’s signature was verified in this call.
  • This is a check performed by HashEntry on HashEntry’s data. To verify without trusting the server’s answer, export an evidence package and verify it yourself: Verifying evidence offline.
  • An unknown id returns 404 with "valid": false and code: "CONSENT_NOT_FOUND".

No API key; rate limited. Parameters: token (a ct_… token or the value of the SDK’s _he_consent cookie), proof_hash or id. It returns the decision, categories, site domain, timestamps, hash status and timestamp information, and never the IP address, user agent or visitor id.

  • A record that exists: 200 with valid as described above.
  • A browser cookie value that cannot be matched to a server record: 200 with "valid": false, "source": "client_token", "server_record_found": false and the decoded cookie fields. The cookie is not signed, so it is never reported as valid on its own.
  • Nothing found: 404 with "valid": false. No identifier: 422.

Error responses are JSON and carry a stable machine code in code (also error_code and error.code; some authentication errors only use error.code).

HTTPCodeMeaning
401MISSING_API_KEY, INVALID_API_KEYNo or unknown API key.
401MISSING_SIGNATURE_HEADERS, TIMESTAMP_EXPIRED, INVALID_SIGNATURE, INVALID_NONCE, NONCE_REPLAYEDRequest signature problems.
401MISSING_BEARER_TOKEN, INVALID_BEARER_TOKENS2S: JWT missing or invalid.
403SITE_NOT_VERIFIEDVerify the site in the dashboard.
403ORIGIN_NOT_ALLOWED, IP_NOT_ALLOWEDNot in the site’s allow-list.
403INSUFFICIENT_ROLE, TENANT_MISMATCHS2S: user is not an api_user of this tenant.
403—API key scope does not allow the method.
404DOCUMENT_NOT_FOUND, VERSION_NOT_FOUND, CONSENT_NOT_FOUND
409CONFIG_OUTDATEDdocument_version_id missing, not active, wrong language, or the server’s configuration cache is cold. Re-read /tool/v1/config; resend only if the version is still the active one.
422VALIDATION_ERRORField errors are in errors.
422APPROVAL_TOKEN_REQUIRED, APPROVAL_TOKEN_INVALID, APPROVAL_TOKEN_CONFLICTSee one-time approval tokens.
422INVALID_CATEGORIES, METADATA_TOO_MANY_KEYS, METADATA_TOO_LARGE, EXTRA_FIELDS_TOO_MANY_KEYS, EXTRA_FIELD_NAME_TOO_LONG, EXTRA_FIELDS_TOO_LARGE, STROKES_DATA_TOO_LARGE, INVALID_SIGNATURE_IMAGE, SIGNATURE_IMAGE_TOO_LARGE, INVALID_CUSTOM_DOCUMENT, UNSUPPORTED_CUSTOM_DOCUMENT_TYPE, CUSTOM_DOCUMENT_TOO_LARGEPayload limits.
429RATE_LIMIT_EXCEEDEDPer-plan rate limit. See Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining.
429CONSENT_LIMIT_EXCEEDED, NO_CONSENT_SUBSCRIPTIONThe plan’s consent quota is used up / there is no active plan.
429TOO_MANY_INVALID_API_KEYSToo many requests with invalid keys from one IP address.
503QUEUE_OVERLOADED, ADMISSION_UNAVAILABLE, API_KEY_LOOKUP_UNAVAILABLETemporary. Retry after Retry-After with the same idempotency_key.

Retry 408, 425, 429 and 5xx responses with the same body and the same idempotency_key (and a new timestamp, nonce and signature). Do not retry other 4xx responses unchanged.