Skip to content

NewThe DPDP Rules phase in by May 2027.Check your website’s consent readiness

Home

Server API

Use the server API when you collect consent in your own sign-up flow or app, and to check consent before you send a marketing message. It is a JSON REST API at https://api.optproof.in/v1.

Starter and above

Authentication#

Server calls use a secret key in the Authorization header:

bash
curl https://api.optproof.in/v1/consents/check?property_id=prp_...&purpose=whatsapp_updates&phone=%2B919876543210 \  -H "Authorization: Bearer sk_live_..."
  • Secret keys are shown once when you create them, and stored by OptProof only as an argon2id hash. If you lose one, rotate it.
  • Keep secret keys on your server. Never put them in a web page or mobile app; those use the publishable key.
  • Rotation gives you an overlap window, so you can deploy the new key before the old one stops working.

Call POST /v1/consents after the person has made their choice in your UI.

bash
curl https://api.optproof.in/v1/consents \  -H "Authorization: Bearer sk_live_..." \  -H "Content-Type: application/json" \  -H "Idempotency-Key: signup-8f2c1d" \  -d '{    "property_id": "prp_01JBTT8000E008000000000002",    "principal": { "phone": "+919876543210" },    "decisions": [      { "purpose_key": "appointments", "granted": true },      { "purpose_key": "whatsapp_reminders", "granted": true },      { "purpose_key": "health_tips", "granted": false }    ],    "notice_version": 3,    "language": "en",    "context": { "ui_variant": "signup_v2", "app_version": "4.12.0" }  }'
property_idstringrequired
The property this consent belongs to (prp_…).
principalobjectrequired
Exactly one of phone, email or external_id (your own stable user id). OptProof turns it into a pseudonymous principal_ref straight away; the raw value never enters the ledger.
decisionsarrayrequired
At least one { purpose_key, granted }. Granted and refused purposes in one call become a grant event and a deny event.
notice_versionintegerrequired
The version of the notice the person saw. It is recorded on the event and in the receipt.
languagestring
Language of the notice shown. Default en.
channelstring
Default server. Use app for mobile apps.
occurred_atdate-time
When the person actually decided, for example the sign-up time. Defaults to now and cannot be in the future.
contextobject
Non-personal context such as ui_variant, page_url or app_version. Values are strings, numbers or booleans.
store_contactboolean
Default true. Stores the phone or email encrypted, so the person can later sign in to the preference centre by OTP.

The response (201) contains the principal_ref and one receipt per event written:

json
{  "principal_ref": "3f9a0c...e1",  "receipts": [    {      "event_id": "evt_01JBTT8000E008000000000081",      "receipt_code": "OP-7K3M-9Q2X",      "receipt_jws": "eyJhbGciOiJFZERTQSIs...",      "action": "grant",      "purpose_keys": ["appointments", "whatsapp_reminders"],      "seq": "1042",      "occurred_at": "2026-10-20T06:12:45.120Z"    }  ]}

Give the receipt code to the person (on screen, by SMS or email). With it they can check the record themselves on verify.optproof.in.

Call GET /v1/consents/check before any marketing email, SMS or WhatsApp message. Identify the person with exactly one of phone, email, external_id or principal_ref.

bash
curl "https://api.optproof.in/v1/consents/check?property_id=prp_...&purpose=whatsapp_reminders&email=asha%40example.in" \  -H "Authorization: Bearer sk_live_..."
json
{  "granted": false,  "status": "withdrawn",  "notice_version": 3,  "event_id": "evt_01JBTT8000E008000000000093",  "updated_at": "2026-10-22T11:02:09.004Z"}

status is granted, denied, withdrawn, expired, or null if OptProof has no record for this person and purpose. Treat anything other than granted: true as "do not send".

The widget identifies visitors by a random device_ref until they tell you who they are. When someone logs in or submits a form, link the two so their device history follows them:

bash
curl https://api.optproof.in/v1/principals/link \  -H "Authorization: Bearer sk_live_..." \  -H "Content-Type: application/json" \  -d '{ "property_id": "prp_...", "device_ref": "0b7e4c1a-9f3d-4c2e-8a51-2d6f0e9b7c43", "principal": { "phone": "+919876543210" } }'

The response is { "principal_ref": "…", "merged_events": 4 }. The inline form block puts the device_ref in a hidden optproof_device field for exactly this call. In the browser, optproof.identify() does the same thing.

Other server endpoints#

EndpointWhat it does
GET /v1/consents/eventsLedger events, newest first, cursor-paginated. Filter by property, action, time, principal or receipt code.
GET /v1/principals/{ref}/consentsCurrent state of every purpose for one person.
GET /v1/principals/{ref}/historyEvery event for one person.
GET /v1/requests, GET /v1/requests/{id}Rights requests and their timelines.
PATCH /v1/requests/{id}Change status, assignee or resolution.
POST /v1/requests/{id}/eventsAdd a note, a reply, or the outcome in one of your systems.
POST /v1/access-logsRecord that someone viewed a person’s record (Business and above).
POST /v1/importsImport existing consents from CSV with evidence (Growth and above).
POST /v1/exports, GET /v1/exports/{id}Start an evidence export and download it.

All fields are in the API reference.

Idempotency#

Send an Idempotency-Key header on every POST. If a request times out, retry it with the same key: OptProof returns the original result instead of writing a second event. Reusing a key with a different body returns 409 idempotency_conflict. Use something stable from your side, such as signup-<user id> or the id of the form submission.

Pagination#

List endpoints return { "data": [...], "next_cursor": "..." }. Pass cursor to get the next page and limit (1 to 200, default 50) to change the page size. next_cursor is null on the last page.

Errors#

Every error has the same shape, with a request_id to quote when you contact support:

json
{  "error": {    "code": "validation_failed",    "message": "decisions must contain at least one item",    "request_id": "req_01JBTT9F2K3M4N5P6Q7R8S9T0V"  }}

Branch on code, not on message. The codes are:

CodeMeaning
bad_requestThe request could not be read, for example malformed JSON.
validation_failedA field is missing or invalid. details lists the paths.
unauthorizedNo key, or the key is wrong or revoked.
forbiddenThe key is valid but cannot do this.
not_foundThe resource does not exist, or belongs to another workspace.
conflictThe request conflicts with the current state.
idempotency_conflictThis Idempotency-Key was already used with a different body.
rate_limitedToo many requests. Wait and retry; see the Retry-After header.
origin_not_allowedA publishable-key call came from an origin not on the property’s list.
notice_not_publishedThe notice version you sent is not published for this property.
unknown_purposeA purpose key does not exist on this property.
otp_invalidThe one-time code is wrong.
otp_expiredThe one-time code has expired. Request a new one.
otp_lockedToo many wrong attempts. Wait before trying again.
sms_unavailable
plan_feature_unavailableYour plan does not include this feature.
internalSomething failed on our side. Retry with the same Idempotency-Key.

Rate limits#

CredentialLimit
Secret key (sk_)1,000 requests a minute per key; higher on Business
Publishable key (pk_)1,000 requests a minute per key
Public calls per device60 requests a minute

Over the limit you get 429 rate_limited with a Retry-After header. Consents are never dropped for being over your plan's yearly consent limit: they are always recorded, you are notified, and the extra consents are billed as overage.

Test and live chains#

Test keys write to a sandbox chain that is separate from your live ledger. Test events never count toward usage, are marked is_test in the ledger, and arrive in webhooks with "test": true. Use them in staging and CI, then switch the key in production.