API reference
The Agreely /v1 API is the backend the TypeScript,
PHP, and CLI clients wrap. It is small,
stateless, and server-to-server. This page is the orientation; the
interactive /v1 reference has the full request/response
schemas, rendered from the OpenAPI spec.
Authentication
Every call is a Bearer API key:
Authorization: Bearer agr_live_...
The key alone identifies the tenant. There is no session and no CSRF. Keys carry scopes:
checkauthorizesPOST /v1/check.issueauthorizes the consent-request endpoints.attestauthorizes the manual-consent endpoints.relationshipauthorizes both customer-relationship lifecycle actions (ending it, art. 23; undoing a mistaken end, art. 11 and 28).- The
checkorissuescope may readGET /v1/catalog. - Any valid key (any scope) may call
GET /v1/whoami.
A missing or invalid key is 401; a valid key lacking the required scope is
403.
Endpoints
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET |
/v1/whoami |
any | The scopes the presented key carries (least disclosure). |
POST |
/v1/check |
check |
Synchronous consent decision for one cell. |
POST |
/v1/check/batch |
check |
Synchronous decisions for many cells in one call (500-item cap). |
GET |
/v1/catalog |
check or issue |
The company's active (category, purpose) catalog. |
POST |
/v1/consent-requests |
issue |
Issue a consent request. |
GET |
/v1/consent-requests |
issue |
List requests, newest first, cursor-paged. |
GET |
/v1/consent-requests/{id} |
issue |
One request by its protocol requestId. |
POST |
/v1/consent-requests/{id}/cancel |
issue |
Cancel a pending request (idempotent). |
POST |
/v1/manual-consents |
attest |
Record an offline, company-attested consent. |
POST |
/v1/manual-consents/claim-links |
attest |
Mint a single-use citizen claim link. |
POST |
/v1/manual-consents/{consentRef}/revoke |
attest |
Revoke a manual consent. |
POST |
/v1/manual-consents/{consentRef}/erase |
attest |
Erase (crypto-shred) a manual consent. |
POST |
/v1/customers/{customerRef}/relationship/end |
relationship |
End a customer relationship (art. 23, idempotent). |
POST |
/v1/customers/{customerRef}/relationship/revert |
relationship |
Undo a mistaken end of relationship (a correction, art. 11 and 28). |
Listing consent requests
GET /v1/consent-requests lists only the key's own company requests, newest
first. Four query parameters, all optional:
| Parameter | Role |
|---|---|
status |
Filter on a request status. An unknown status is the API's only 400. |
customerId |
Your own customer ref, the one supplied at issuance. Narrows the list to the requests issued for that subject, within your own requests. A ref that is not yours matches nothing. |
cursor |
The requestId to page after (keyset pagination, never offset). |
limit |
Page size, clamped to [1, 100], defaulting to 50. An out-of-range value is clamped rather than rejected; a non-numeric value falls back to the default. |
Key identity (whoami)
GET /v1/whoami confirms, server-side, the scopes the presented key actually
carries. Any valid key reaches it (any scope). The response is deliberately
minimal: only the key's scopes, nothing else. No company id, no key name, no
counters.
{ "scopes": ["check", "issue"] }
A missing or invalid key is 401. Like the other endpoints, it is rate-limited
(429).
Cancelling a consent request
POST /v1/consent-requests/{id}/cancel cancels a pending request (the
company-side "revoke before action" path): it flips to the terminal
revoked_before_action state, so the citizen deep link no longer leads to an
approval. Scope: issue. The request is identified by its protocol requestId and
is company-scoped from the key (never a body value).
The call is idempotent: an already-terminal request (approved, refused,
expired, or revoked_before_action) is not an error; it returns its current
status with cancelled: false. Only the pending-to-cancelled transition returns
cancelled: true.
{ "requestId": "0x...", "status": "revoked_before_action", "cancelled": true }
An unknown, foreign, or malformed requestId is 404 (never a cross-tenant
write).
The check: deny is a 200
A consent decision, allow or deny, is always a successful 200. Deny is not
an error.
{ "decision": "deny", "status": "revoked", "consentRef": "0x...", "assurance": "citizen_signed", "checkedAt": "2026-06-26T15:04:05Z" }
Always branch on decision (allow or deny). The status tells you why,
and the vocabulary has eight values:
status |
decision |
What it means |
|---|---|---|
active |
allow |
An active enforcement record covers the cell. |
necessity |
allow |
No per-subject record, but the active catalog cell declares a non-consent lawful basis (contract, necessary_for_service, security_fraud, legal_obligation, professional_contact). The response then carries basis and neither consentRef nor assurance: there is no signed proof. The call creates nothing. |
none |
deny |
No record on a consent-basis cell. |
revoked |
deny |
The consent was withdrawn. |
expired |
deny |
The consent's lifespan elapsed (art. 14 al. 3). |
erased |
deny |
The cell was erased (crypto-shredded). |
relationship_ended |
deny |
The company attested the relationship is over (art. 23). This is a prospective, relationship-level stop; the per-cell consent stays truthfully active (it was never withdrawn). |
sensitive_requires_consent |
deny |
No record, and the cell is declared sensitive by the company: it requires express consent and never opens on a bare necessity basis (art. 12 al. 1 in fine and art. 13). |
consentRef and assurance are present for every status backed by a record, and
absent for none and for necessity. assurance is citizen_signed
(citizen-signed) or company_attested (manual consent): the assurance tier of the
backing consent.
Agreely records the declared basis; it does not validate it
A necessity allow rests on the lawful basis your company declared on the catalog cell. Agreely records that declaration and enforces it consistently; it does not certify that it is legally sound. Note: professional_contact (art. 1 al. 5) is a scope carve-out, not a necessity ground under art. 12 or art. 9; it appears here because it also produces an allow without consent.
Send the raw human category and purpose; the server resolves them.
Resolution is case- and whitespace-insensitive; a French label resolves with or
without accents, and an English label resolves with or without accents only if
the company has disclosed an English label for that cell. An ambiguous or
undisclosed label matches nothing and denies (none). Do not normalize
client-side.
The check reads only the status of a single indexed enforcement record and never touches the chain or the indexer, so a revoke denies on the very next call. It is fail-closed: there is no allow without a positive record or a declared basis. An Agreely outage should be treated as a deny too; that is what the SDKs do.
The batch check
POST /v1/check/batch evaluates many cells in one round-trip instead of N calls
to /v1/check. Same scope (check), same decision engine, same fail-closed rule,
and the same per-decision access log (Loi 25 G3): one batched, per-decision-granular
write. It is scoped to the api-key's company. category / purpose are sent
raw; the server normalizes them.
Two body shapes are accepted. The flat form:
{ "items": [ { "customerRef": "cust_8812", "category": "Phone number", "purpose": "Billing" } ] }
And the grouped form (expanded server-side into a customer x fields product):
{ "checks": [ { "customerRef": "cust_8812", "fields": [ { "category": "Phone number", "purpose": "Billing" }, { "category": "Email address", "purpose": "Newsletter" } ] } ] }
Batch uses customerRef, the single call uses customerId
A compatibility detail worth knowing: POST /v1/check names the subject field customerId, while POST /v1/check/batch names it customerRef. Both mean your own customer reference, resolved within the api-key's company. The SDKs hide this difference.
The response is a decisions array aligned to the submitted cells. Each decision
carries the same fields as a single /v1/check, plus the echoed-back customerRef,
category, and purpose:
{ "decisions": [
{ "customerRef": "cust_8812", "category": "Phone number", "purpose": "Billing",
"decision": "allow", "status": "active", "consentRef": "0x...",
"assurance": "citizen_signed", "checkedAt": "2026-06-26T15:04:05Z" }
] }
As with the single call, consentRef and assurance are absent for statuses
none and necessity, the status vocabulary is identical (including necessity
with its basis, relationship_ended, and sensitive_requires_consent), and a
cell with no record and no declared basis returns deny / none (fail-closed). An
empty batch returns 422, and a batch of more than 500 cells returns 422
(invalid_request: "Batch exceeds the 500-item cap. Split into smaller batches.");
split it into smaller batches.
Use case: gating a client-list display
Rendering a table of N customers x M fields is N x M uses of personal information. Under Loi 25, each field you display is a use gated by a check (use-limitation, art. 12; accountability, art. 3.1). The batch collapses the whole grid into one round-trip while keeping a decision and an access-log entry per cell. Batching is an optimization of the same per-decision accountability, never a weakening: every cell is still decided and logged individually.
Manual consents (attest)
The attest scope covers consents collected offline and attested by the company.
Four endpoints:
POST /v1/manual-consentsrecords a company-attested consent. The body carriescustomerId,documentVersionId,effectiveDate,validUntil,items(catalog ids or{category, purpose}pairs), and anevidencewhosepdfSha256(a SHA-256 digest,0x+ 64 hex) is required; the base64pdfis optional, for escrow storage. The201response carriesconsentId,merkleRoot, aconsentRefsarray (one0x-hex per cell),assurance: "company_attested", andanchored(false at record time).POST /v1/manual-consents/claim-linksmints a single-use citizen claim link: body{ customerId }(required) and optionalreference;201response{ claimUrl, token, expiresAt }.POST /v1/manual-consents/{consentRef}/revokewithdraws the consent: optional{ reason? }body;200response{ consentRef, revoked: true, alreadyRevoked }. An unknown reference is404.POST /v1/manual-consents/{consentRef}/eraseperforms the erasure (crypto-shred): optional{ reason? }body;200response{ consentRef, erased: true, alreadyErased }. An unknown reference is404.
A manual consent and a citizen-signed consent allow identically; only the
assurance tier differs (company_attested versus citizen_signed). Manual
consents do not implement idempotency.
Ending a customer relationship (relationship)
POST /v1/customers/{customerRef}/relationship/end attests, from your own code
(e.g. an offboarding flow), that a customer relationship is over, in the sense of
Law 25 art. 23 ("les fins sont accomplies"). Scope: relationship. This is the
/v1 exposure of the company-UI action: it performs the same terminal
lifecycle write, so the required justification, the endedBy origin, the
active/ending to ended transition, and any downstream obligation behave
identically. It is a pure lifecycle overlay: it never revokes, erases, or
hides any per-cell consent.
The customer is identified by your own customerRef (the same one used by
/v1/check), resolved within the key's company. An unknown or foreign
reference is a clean 404 with nothing written. A missing or blank reason
is 422 (never a silent end). The call is idempotent: re-ending an
already-ended relationship is a success; the original endedAt and endedBy
origin stand. The endpoint never accepts or returns a citizen DID.
{ "customerRef": "cust_8812", "status": "ended", "endedAt": "2026-06-26T15:04:05Z", "endedBy": "company" }
Undoing a mistaken end
POST /v1/customers/{customerRef}/relationship/revert undoes a premature or
mistaken company-attested end. It is a correction of an inaccurate record
(art. 11 and 28), never a resurrection of dead consent. Scope: relationship.
It is the /v1 twin of the undo offered in the company UI, behind exactly the
same eligibility gate.
The undo is allowed only if all of these hold: the end was
company-attested, nothing was destroyed (no consent withdrawn, no erasure
executed), and the call lands within the 30-day correction window. An
ineligible relationship (a citizen-driven end, past the window, or after any
destruction) is a clean 404 with nothing written: a returning person is a
new relationship needing a fresh consent request (art. 14). A missing or blank
reason is 422: the correction must carry its justification.
On success the relationship returns to active and the still-active cells, which
were never withdrawn, allow again on their own: no re-consent to request, nothing
lapsed.
{ "customerRef": "cust_8812", "status": "active", "reverted": true }
The error envelope
Every error (never a deny) shares one shape:
{ "error": { "code": "invalid_request", "message": "…", "field": "category" } }
| HTTP | code |
When |
|---|---|---|
401 |
unauthorized |
Missing, invalid, or revoked key. |
402 |
billing_inactive |
The workspace has no active subscription (its trial ended unpaid, or billing lapsed). Every /v1 endpoint returns this until the account owner picks a plan in the app. |
403 |
forbidden |
Valid key, missing scope. |
422 |
invalid_request |
Malformed input (field names the offender). The only 400 is the unknown status filter on the request list. |
404 |
not_found |
No such resource. |
429 |
rate_limited |
Per-company window exceeded (see Retry-After). |
The unavailable code is reserved in the enum, but no endpoint emits it yet.
Rate limits
Limits are per-company, in a fixed window (120 requests per minute by default).
Every /v1 endpoint is limited. A 429 carries the rate_limited code and a
Retry-After header (integer seconds until the window resets). There is no other
rate-limit header. The SDKs surface this as a typed error with retryAfter.
Idempotency
Only POST /v1/consent-requests honors an Idempotency-Key header (255
characters at most). A retry with the same key for the same company replays the
original 201 byte-for-byte and creates nothing new, so there is no
double-issue and no double-email. The SDKs attach a unique key per call
automatically; pass your own to make a retry safe. The manual-consent endpoints do
not implement idempotency.
Idempotency-Key: order-4471
Next
- The interactive
/v1reference for full schemas. - TypeScript SDK, PHP SDK, and CLI wrap all of this.