Terms and Conditions Hashes
Overview
When hash validation is enabled for your merchant (see the opt-in callout below), Bakkt requires hashes of the legal documents the end-user has accepted to be sent on every user/corporate create or update call. The hashes prove that a specific version of each document was shown to the user at the moment they consented, and they're retained in Bakkt's compliance records. Merchants who haven't enabled this feature can omit the hash fields entirely.
This guide explains:
- which fields you need to send and where,
- how to compute the hashes correctly,
- how to react when Bakkt re-versions a document and a previously verified user goes out of compliance.
This feature is opt-in per merchant. It is not active by default for all merchants — typically only merchants who collect T&C acceptance in their own UI (rather than relying on the Sumsub-hosted KYC flow) need it. To turn it on for your merchant account, contact your Bakkt account manager.
About the encoding. These fields are SHA-256 digests base64-encoded. Some example values in the OpenAPI spec are hex digests of an empty string (
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855); those are placeholder examples, not real produced hashes. The wire format is base64.
| Document | Document link |
|---|---|
| Terms and Conditions (EPMAP) | https://s3.eu-west-2.amazonaws.com/legal.getunblock.com/EPMAP+T%26Cs+Jan+30+2025.pdf |
| Terms of Service | https://s3.eu-west-2.amazonaws.com/legal.getunblock.com/Complycrypto_Terms_Conditions.pdf |
| Privacy Policy | https://s3.eu-west-2.amazonaws.com/legal.getunblock.com/complycrypto_privacy_policy.pdf |
Fields by entity
All three hashes are computed the same way (SHA-256 of the PDF bytes, base64-encoded). Only the field names differ between user and corporate flows.
Individual users — POST /user and PATCH /user
POST /user and PATCH /user| Field | Source document |
|---|---|
accepted_terms_and_conditions_hash | EPMAP Terms and Conditions (PDF) |
accepted_terms_of_service_hash | Terms of Service (PDF) |
accepted_privacy_policy_hash | Privacy Policy (PDF) |
The user-side endpoints accept both
accepted_terms_and_conditions_hash(plural) andaccepted_terms_and_condition_hash(singular) as aliases. Stick with the plural form for users — it's what the user-facing OpenAPI schema uses.
See User Management → Create a user.
Corporates — POST /corporate and PATCH /corporate/{corporate_uuid}
POST /corporate and PATCH /corporate/{corporate_uuid}| Field | Source document |
|---|---|
accepted_terms_and_condition_hash | EPMAP Terms and Conditions (PDF) |
accepted_terms_of_service_hash | Terms of Service (PDF) |
accepted_privacy_policy_hash | Privacy Policy (PDF) |
The corporate-side endpoints use the singular form
accepted_terms_and_condition_hashin the OpenAPI schema. Match the schema to keep your code unambiguous.
See Corporate Management → Create a corporate.
How to compute a hash
GETthe PDF URL and read the response body as raw bytes.- Compute the SHA-256 digest of those bytes.
- Base64-encode the digest.
- Send that string as the field value.
Node.js
const { createHash } = require('crypto');
async function hashLegalDocument(pdfUrl) {
const response = await fetch(pdfUrl);
if (!response.ok) {
throw new Error(`Failed to fetch ${pdfUrl}: ${response.status}`);
}
const pdf = Buffer.from(await response.arrayBuffer());
return createHash('sha256').update(pdf).digest('base64');
}
async function hashAllLegalDocuments() {
const [terms, tos, privacy] = await Promise.all([
hashLegalDocument('https://s3.eu-west-2.amazonaws.com/legal.getunblock.com/EPMAP+T%26Cs+Jan+30+2025.pdf'),
hashLegalDocument('https://s3.eu-west-2.amazonaws.com/legal.getunblock.com/Complycrypto_Terms_Conditions.pdf'),
hashLegalDocument('https://s3.eu-west-2.amazonaws.com/legal.getunblock.com/complycrypto_privacy_policy.pdf'),
]);
return {
accepted_terms_and_conditions_hash: terms,
accepted_terms_of_service_hash: tos,
accepted_privacy_policy_hash: privacy,
};
}Python
import base64
import hashlib
import requests
def hash_legal_document(pdf_url: str) -> str:
response = requests.get(pdf_url)
response.raise_for_status()
digest = hashlib.sha256(response.content).digest()
return base64.b64encode(digest).decode("ascii")
terms = hash_legal_document("https://s3.eu-west-2.amazonaws.com/legal.getunblock.com/EPMAP+T%26Cs+Jan+30+2025.pdf")
tos = hash_legal_document("https://s3.eu-west-2.amazonaws.com/legal.getunblock.com/Complycrypto_Terms_Conditions.pdf")
privacy = hash_legal_document("https://s3.eu-west-2.amazonaws.com/legal.getunblock.com/complycrypto_privacy_policy.pdf")Shell
hash_legal_document() {
pdf_url="$1"
curl -sSL "$pdf_url" | openssl dgst -sha256 -binary | base64
}
hash_legal_document https://s3.eu-west-2.amazonaws.com/legal.getunblock.com/EPMAP+T%26Cs+Jan+30+2025.pdf
hash_legal_document https://s3.eu-west-2.amazonaws.com/legal.getunblock.com/Complycrypto_Terms_Conditions.pdf
hash_legal_document https://s3.eu-west-2.amazonaws.com/legal.getunblock.com/complycrypto_privacy_policy.pdfManual / one-time
For a quick check, or for the very first setup, you can resolve the PDF URL by hand:
- Open the public landing page in a browser.
- Right-click the document download link on the rendered page and copy the URL.
- Run, for example:
curl -sSL "<PDF-URL>" | openssl dgst -sha256 -binary | base64.
That gives you the same value as the scripted approach above and lets you sanity-check what the automation produces.
When and how to refresh hashes
Bakkt may publish new versions of any of the three documents. Two signals tell you a refresh is needed:
- Advance notice. Bakkt notifies merchants before any document version goes live. Use that window to recompute the hashes and update your acceptance UI ahead of the rollover.
MISSING_TERMS_AND_CONDITIONS_SIGNEDwebhook. For users who are already verified but now hold acceptances for a superseded document version, Bakkt fires:- Event:
entityStatusUpdate subType:MISSING_TERMS_AND_CONDITIONS_SIGNED- What it means: the user passed KYC but is missing valid hashes for the latest document versions and cannot transact until they re-accept.
- What to do: re-resolve the PDFs, recompute the hashes, present the documents to the user again, and on acceptance call
PATCH /user(or the corporate equivalent) with the new hashes.
- Event:
See Webhooks → entityStatusUpdate for the full event payload structure.
Recommended workflow
- At onboarding time, resolve and hash the three PDFs, present the documents to the user in your UI for explicit acceptance, and only then send the create call. Store the hashes in your own records alongside the user/corporate ID, the resolved PDF URL, and the timestamp of acceptance.
- Cache aggressively. The resolved canonical PDF URL and resulting hash for a given document version don't change until Bakkt publishes a new version. Resolve and hash once per app boot (or once per day), cache the result, and only refresh when you receive advance notice or a
MISSING_TERMS_AND_CONDITIONS_SIGNEDwebhook. - On
MISSING_TERMS_AND_CONDITIONS_SIGNED, mark the affected user as "needs re-acceptance," gate transactions until they re-accept, and PATCH the new hashes once they do. - Don't share hashes across users. Even though the hash value will be the same for every user accepting the same version, you must collect explicit acceptance from each user — the hash is evidence of consent, not a substitute for collecting it.
Best practices
- Hash raw bytes, not parsed text. Don't try to extract text from the PDF, normalize whitespace, or convert encodings — SHA-256 over the exact bytes the S3 endpoint returned is the only thing that will match.
- Store the resolved PDF URL alongside the hash. It's a stable, versioned reference (the version date is in the filename) to the exact document the user accepted.
- Pin a copy of the PDF on your side. Save the resolved PDF (with its source URL and capture date) so you can show the exact document a user accepted in a compliance audit, even after Bakkt rotates to a newer version.
- Log the timestamp of acceptance. Bakkt's compliance workflows assume you can produce evidence that a specific user accepted a specific version at a specific moment.
- Surface re-acceptance prompts immediately when you receive
MISSING_TERMS_AND_CONDITIONS_SIGNED. The user is otherwise blocked from transacting and you'll see support traffic.
Next steps
- User Management → Create a user — where the user-side hashes are sent.
- Corporate Management → Create a corporate — where the corporate-side hashes are sent.
- Webhooks →
entityStatusUpdate— how Bakkt notifies you when a user falls out of T&C compliance.
Updated about 1 month ago
