Approval requests
Live on
api.zyton.ai. The SDK methods come in@zyton/pip0.2.0, which is not on npm yet (npm has 0.1.0): until it is, use the plain HTTP calls on this page.
With an approval request, you don't capture consent in your own app. You ask one of your end users, and they approve or deny on a Zyton-hosted screen, in their own Zyton account. An approval becomes a consent grant that you check the same way as a developer-attested grant, and the end user can revoke it in their Zyton account at any time.
Approval requests need an API key. Every request lives in your organisation and in your key's environment, like grants: a sandbox key never sees a live request, and the other way round.
How it works
- Your backend sends an approval request: your own id for the end user (
subjectRef), their email address, the purpose, the data categories and the operations. - Live keys: Zyton emails the end user a one-time link. You never see the link. Sandbox keys: Zyton sends no email. The response carries the link (
approvalUrl), so you can try the flow yourself. - The end user opens the link and signs in to their Zyton account, or creates one, with the email address you sent. Their Zyton account must have confirmed that address (Zyton sends the confirmation link); until it has, the request stays pending. They see your organisation's registered name (or a neutral label if we can't find it) with a "via Zyton" label, and approve or deny.
- You poll the request until it is decided. There are no decision webhooks yet.
- An approval is a grant with
attestation: 'end_user_approved'.checkConsentand the consent gate honour it. - The first time the end user opens one of your links signed in to their Zyton account, your
subjectRefis linked to that account, whatever they then decide. Later requests for the samesubjectRefgo straight to their Zyton inbox, soemailbecomes optional.
You never see the end user's Zyton account ID or the email address on their account. Zyton stores the address you send encrypted and never returns it, and no error message repeats it.
1. Send a request
import { PipClient } from '@zyton/pip';
const pip = new PipClient({ apiKey: process.env.PIP_API_KEY });
const request = await pip.consent.requestApproval({
subjectRef: 'user_8412', // your own id for the user
email: '[email protected]', // where a live key's link is sent
purpose: 'Share your order history so we can recommend products',
dataCategories: ['order_history'],
operations: ['read'],
});
console.log(request.requestId, request.status); // "…" "pending"
console.log(request.approvalUrl); // sandbox keys: the link to try it yourself; live keys: null| SDK field | HTTP field | Rules |
| --- | --- | --- |
| subjectRef | subject_ref | Required. Your own id for the end user: 1-128 characters from A-Z a-z 0-9 . _ - : \|, never an email address. |
| email | email | The end user's email address (ASCII, at most 254 characters). Required until the subjectRef is linked to their Zyton account. |
| purpose | purpose | Required. What you ask for, in your words, 1-1000 characters. No links: web addresses, domain names, IP addresses and email addresses are refused, and so are control and invisible formatting characters. |
| dataCategories | data_categories | Required. 1-32 names such as email or order_history. |
| operations | operations | Required. Any of read, process, derive, share, store, delete. |
| expiresInSeconds | expires_in_seconds | How long the end user has to decide: 300 (5 minutes) to 2592000 (30 days). Default 7 days. |
| grantExpiresAt | grant_expires_at | When the grant ends, 60 seconds to 10 years ahead and after the request's own deadline. Omit it for a grant that lasts until it is revoked. |
| jurisdiction | jurisdiction | { countryCode, region?, legalFramework? }, ISO 3166-1 numeric (36 = Australia). |
| evidence | evidence | Your own references, free of personal data: consentTextVersion (1-128 characters) and reference, such as an order id (1-256). |
The answer is 201 with created: true, the requestId, status: 'pending' and expiresAt, the end user's deadline. Sending the same request again while it is pending returns it with created: false (HTTP 200) and doesn't email the end user again, so retries are safe.
approvalUrl is set for sandbox keys only. Sending the same sandbox request again returns a fresh link, and the previous one stops working. The link carries a one-time token, so treat it as a secret and never log it. It is always null for live keys, and when the end user's account is already linked.
2. Wait for the decision
const decided = await pip.consent.waitForApproval(request.requestId, {
timeoutMs: 10 * 60_000, // the default; at most 30 days
});
if (decided.status === 'approved') {
// The approval is a grant: the same check as for any other grant.
await pip.consent.checkConsent({
subjectRef: 'user_8412',
operation: 'read',
dataCategories: ['order_history'],
}); // true
}waitForApproval(requestId, { timeoutMs, signal }) polls GET /api/v1/consent/approval-requests/{request_id}, at once and then every 2 seconds, backing off to every 10 seconds. It returns the request as soon as its status is final:
| status | What happened |
| --- | --- |
| pending | Waiting for the end user. A pending request past its expiresAt is reported expired. |
| approved | The end user approved. grantId is the grant, and decidedAt is when. |
| denied | The end user said no. Don't ask again straight away. |
| expired | The end user didn't decide in time. |
| cancelled | You withdrew the request. |
- After
timeoutMs(default 10 minutes, at most 30 days) it throwsApprovalWaitTimeoutError. The request itself stays pending: wait again, or read it later withgetApprovalRequest(requestId), which returnsnullfor a request that isn't in your organisation and environment. - Abort the
signalto stop waiting at once: the wait rejects with the signal's reason. - A network failure or timeout, or an HTTP
408,429,502,503or504, is retried with the same backoff (afterRetry-Afterwhen the API sends one). Any other error, including a404for an unknown request, ends the wait.
3. Use the grant
An approval creates a grant in your organisation and in the key environment of the request. getGrant, listGrants({ subjectRef }), checkConsent, hasConsent and withConsentGate all include it, and revokeGrant revokes it. Approvals made with a sandbox key create sandbox grants, which live checks never honour.
A grant from an approval has attestation: 'end_user_approved' and source: { type: 'end_user', approvalRequestId, decidedAt, channel }. It has no attestedByKeyId and no evidence (both null), because no key attests it: the end user's decision is the evidence. A grant your backend recorded has attestation: 'developer_attested' and source.type: 'developer'. In TypeScript, narrow on attestation to read the fields of each kind.
The end user can revoke the grant in their Zyton account at any time. Checks then answer false, so check before each use.
List and cancel requests
// One end user's pending requests, newest first.
const { requests } = await pip.consent.listApprovalRequests({ subjectRef: 'user_8412', status: 'pending' });
// Withdraw one: it leaves the end user's inbox and can no longer be approved.
const { cancelled } = await pip.consent.cancelApprovalRequest(requests[0].requestId);listApprovalRequests({ subjectRef?, status?, limit?, offset? }) lists your requests, newest first (limit 1-200, default 50). cancelApprovalRequest(requestId) withdraws a pending request; a request that is no longer pending comes back unchanged with cancelled: false.
Limits
- Daily cap. Each organisation can create 200 requests a day with live keys and 1,000 with sandbox keys. The count is per key environment and resets at 00:00 UTC. Over the cap,
requestApprovalthrowsApprovalRateLimitError(HTTP429, codeCONSENT_RATE_LIMITED) and nothing is created. Sending an identical request again at the cap is refused too: list the end user's requests (listApprovalRequests({ subjectRef })) to find the one you already created. - Alert emails. One end user gets at most 3 alert emails from your organisation in 24 hours. Past that, the request is still created, with
alertStatus: 'throttled'.alertStatus: 'failed'means the email couldn't be sent. Either way the end user got no link: cancel the request and create it again later. There is no resend yet. Sandbox requests never send one (skipped). - Busy service.
503with codeCONSENT_SERVICE_UNAVAILABLEmeans the limits couldn't be checked. Nothing was created: try again shortly.
import { ApprovalRateLimitError } from '@zyton/pip';
try {
await pip.consent.requestApproval(input);
} catch (err) {
if (err instanceof ApprovalRateLimitError) {
// 429 CONSENT_RATE_LIMITED: nothing was created. The daily cap resets at 00:00 UTC.
console.log(err.resetsAt, err.retryAfterMs);
} else {
throw err;
}
}What your end user sees
The link opens the request on consumer.zyton.ai. To see it, the end user signs in with, or creates, a Zyton account for the address you sent. A sandbox approvalUrl works the same way, so test it with a Zyton account for an address of your own. When something stops them, they see a message for one of these answers:
| When | HTTP | Code |
| --- | --- | --- |
| The account's email address isn't confirmed yet | 403 | CONSENT_EMAIL_NOT_VERIFIED (they're asked to confirm it, then the same link works) |
| The account's email isn't the address you sent | 409 | CONSENT_APPROVAL_EMAIL_MISMATCH (with a masked hint of the address) |
| Your subjectRef is already linked to a different Zyton account | 409 | CONSENT_APPROVAL_BOUND_TO_OTHER_ACCOUNT |
| The same account opens the link again | 409 | CONSENT_APPROVAL_ALREADY_CLAIMED, with the request's current status: pending, approved, denied, or cancelled or expired if that happened after they opened it |
| The request expired, or you cancelled it, before they opened it | 410 | CONSENT_REQUEST_EXPIRED or CONSENT_APPROVAL_CANCELLED |
| Anyone else uses a link that was already used, or a wrong link | 404 | CONSENT_REQUEST_NOT_FOUND |
CONSENT_APPROVAL_ALREADY_DECIDED is listed in the API reference but isn't returned in practice: a request can only be decided after it is opened, so whoever opened it gets CONSENT_APPROVAL_ALREADY_CLAIMED and everyone else 404.
Errors
| HTTP | Code | What to do |
| --- | --- | --- |
| 400 | CONSENT_INVALID_SCOPE, CONSENT_INVALID_VALIDITY, CONSENT_INVALID_JURISDICTION | Fix the input. The message names the field; the SDK refuses most of these before sending anything. |
| 403 | CONSENT_FORBIDDEN | Only an API key's token can create and read approval requests, not a signed-in session or another token. |
| 404 | CONSENT_REQUEST_NOT_FOUND | The request isn't in your organisation and key environment, or it no longer exists: a request is deleted 90 days after it ended without an approval, and when the end user's Zyton account is erased. waitForApproval throws this code too. |
| 409 | CONSENT_CONFLICT | The same request was being changed at the same moment. Retry. |
| 429 | CONSENT_RATE_LIMITED | Your daily cap is reached. Retry after 00:00 UTC. |
| 503 | CONSENT_SERVICE_UNAVAILABLE | Nothing was created. Retry shortly. |
Errors carry the HTTP status and the API's code in err.context, as everywhere in the SDK. The HTTP API overview explains the shared error format.
Your responsibilities
Send approval requests only to your own end users, at an address they gave you, and only for something you need their consent for. Section 4 of the Terms sets out the rules, and its section on your end users' data explains who is responsible for what.