FR EN

Record an offline consent

Not every person has a passkey. An elderly person who signs a paper form, or who answers the telephone, will never go through the consent request approval flow. For those cases, the organization records the consent itself, through one of two paths:

  • On paper: scope attest, tier manual, assurance company_attested. The proof is a hand-signed sheet, whose digest is recorded.
  • By telephone: scope attest_verbal, tier verbal, assurance company_documented. The proof is the organization's word: there is no document at all.

Both allow at /v1/check exactly like a citizen-signed consent; only tier and assurance differ, and your system decides what each tier unlocks. The attest_verbal scope is never ticked by default on a key: it must be a deliberate choice.

Choose the document version

Every offline consent is tied to the exact published version of a consent document, its documentVersionId. Read it with GET /v1/consent-documents, or with GET /v1/catalog?documentCode=..., which also returns the cells to present.

The person must receive that version's text. The information document downloads as a PDF, to print with the sheet or on its own:

curl -sS "https://api.agreely.ca/v1/consent-documents/versions/$VERSION_ID/pdf?locale=en" \
  -H "Authorization: Bearer $AGREELY_API_KEY" -o information-document.pdf

Downloading this PDF records nothing: it is not evidence that the person was informed.

On paper

You need an attest-scoped key.

POST /v1/customers/{customerRef}/consent-sheets produces the signature sheet of a version for a customer the organization knows: one Yes/No pair per consent ask, the information purposes with no box, the signature and a printed reference. The same call mints the claim link whose second factor that reference is.

curl -sS https://api.agreely.ca/v1/customers/cust_8812/consent-sheets \
  -H "Authorization: Bearer $AGREELY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: sheet-cust_8812-2026-10-09" \
  -d '{"documentVersionId":"e4d41dc3-c8a5-44b1-96c5-be740c45cc6d","locale":"en"}'
# { "signatureSheet": { "pdf": "<base64>", ... }, "printedReference": "ABCD-EFGH", "claim": { "claimUrl": "...", ... } }

The reference and the link are returned once: keep them at the time of the call. A new sheet for the same customer retires the previous link.

Two rules with a signature sheet

Never send the claim link in the same envelope as the sheet, nor in the same message. The printed reference only defends a link that travels separately.

Never send the blank sheet's digest as evidence.pdfSha256. The evidence is the signed sheet, scanned when it comes back: its digest is what you record in the next step.

Record the signed sheet

You start from the signed sheet, scanned to PDF, and the published version the person consented under. The SDK hashes the PDF locally (SHA-256); by default, only the pdfSha256 leaves your machine. The raw bytes leave only with an explicit escrow upload.

const m = await agreely.manualConsents.record({
  customerId: "cust_8812",
  documentVersionId: "doc_v3",     // the PUBLISHED disclosure version signed under
  effectiveDate: "2026-01-01",
  validUntil: "2031-01-01",
  items: [{ category: "Phone number", purpose: "Billing" }],
  evidence: {
    pdfSha256: Agreely.hashPdf(bytes), // "0x"+sha256, hashed LOCALLY, required
    // pdf: base64Pdf,                 // optional: only with an explicit upload
  },
});

m.assurance;   // "company_attested"
m.consentRefs; // one 0x ref per cell; m.anchored is false at record time
$m = $agreely->manualConsents()->record([
    'customerId'        => 'cust_8812',
    'documentVersionId' => 'doc_v3',
    'effectiveDate'     => '2026-01-01',
    'validUntil'        => '2031-01-01',
    'items'             => [['category' => 'Phone number', 'purpose' => 'Billing']],
    'evidence'          => [
        'pdfSha256' => Agreely::hashPdf($bytes), // "0x"+sha256, LOCAL, required
        // 'pdf'    => $base64Pdf,               // optional opt-in upload
    ],
]);

$m->assurance;   // "company_attested"
$m->consentRefs; // one 0x ref per cell
agreely manual-consent create \
  --customer cust_8812 --document-version doc_v3 \
  --effective-date 2026-01-01 --valid-until 2031-01-01 \
  --item "Phone number:Billing" \
  --pdf ./signed-consent.pdf --json
# the hash alone is sent; add --upload to also escrow the raw bytes
curl -sS https://api.agreely.ca/v1/manual-consents \
  -H "Authorization: Bearer $AGREELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customerId": "cust_8812",
    "documentVersionId": "doc_v3",
    "effectiveDate": "2026-01-01",
    "validUntil": "2031-01-01",
    "items": [{"category":"Phone number","purpose":"Billing"}],
    "evidence": {"pdfSha256":"0x…"}
  }'

The response carries consentId, merkleRoot, a consentRefs array (one 0x-hex per cell), assurance: "company_attested", and anchored: false (anchoring happens afterwards, asynchronously). Agreely.hashPdf is static and makes no network call; it produces exactly the shape evidence.pdfSha256 expects.

A few rules worth knowing:

  • items names the asks ticked "Yes". It may be empty when every ask got a "No". The lines the document gives for information are added by the server as acknowledged, never as consents; the response lists them in acknowledged.
  • effectiveDate is the date of the signature, never in the future.
  • A sensitive purpose resting on consent requires "sensitiveExpressAttested": true: the organization attests the consent was given expressly. Agreely records that attestation; it does not verify it.
  • A sheet signed before the chosen version was published requires "versionAttested": true: the organization attests the sheet asked consent for the same purposes, with the same information.
  • Send an Idempotency-Key header: a retry with the same body replays the first answer and records nothing new.
  • Every refusal carries a stable reason (predates_withdrawal, relationship_ended, sensitive_express_required, ...). Compare it, never the message. The full list is in the API reference, under "Record a paper consent".

It enforces identically

A company-attested consent and a citizen-signed consent allow identically at /v1/check. Only the tier differs: a check will return assurance: "company_attested" and tier: "manual" instead of "citizen_signed" and "full". The code gating a feature does not change.

The person can later bind this offline consent to her own decentralized identifier through a single-use claim link. The signature sheet already mints one; for a consent recorded without a sheet produced by the API, mint it separately:

const link = await agreely.manualConsents.createClaimLink({
  customerId: "cust_8812",
  reference: "order-4471", // optional
});
// { claimUrl, token, expiresAt } - single-use

On the CLI: agreely manual-consent claim-link --customer cust_8812 --reference order-4471.

By telephone

You need an attest_verbal-scoped key.

The script to read

The script to read to the person is in the Agreely web app: on the customer's record, on the Verbal consent page, choose the document, then show the script. It carries the version's information, each ask one at a time, and the read-back of the answers.

Through the API there is no script to download. Two fields tie the call to what was read:

  • documentVersionId ties the consent to the exact version of the document read on the call;
  • scriptVersion is your organization's own label for the script it read (for example intake-v3). Agreely records it as is in the receipt.

Every ask put on the call gets an explicit answer, "yes" or "no": there is no shorthand that records a list of purposes as accepted.

curl -sS https://api.agreely.ca/v1/verbal-consents \
  -H "Authorization: Bearer $AGREELY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: call-client-7-2026-10-08" \
  -d '{
    "customerId": "client-7",
    "documentVersionId": "e4d41dc3-c8a5-44b1-96c5-be740c45cc6d",
    "answers": [
      {"category": "Photo", "purpose": "Publication", "answer": "yes"},
      {"category": "Address", "purpose": "Meal delivery", "answer": "no"}
    ],
    "obtainedAt": "2026-10-08T14:05:00-04:00",
    "obtainedBy": "Luc Gagnon",
    "scriptVersion": "intake-v3",
    "respondent": {"consentedBy": "self"},
    "validUntil": "2027-10-31",
    "paperExpected": true
  }'
# { "consentId": "...", "tier": "verbal", "assurance": "company_documented", "consentRefs": ["0x..."], ... }

obtainedAt is the instant of the call, the consent date: at most 7 days old and never in the future, not even by one second (obtained_at_in_future). A stronger consent already in force for a purpose, on paper or by passkey, is never replaced by a call (stronger_consent_active), except in the renewal window described below. For a minor under 14, the respondent must be the person having parental authority or the tutor.

Receive the signed paper that confirms the call

With paperExpected: true, the organization sends the person a sheet that repeats the asks. When it comes back signed, record it with an attest-scoped key:

curl -sS https://api.agreely.ca/v1/verbal-consents/$CONSENT_ID/paper \
  -H "Authorization: Bearer $AGREELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "signedAt": "2026-10-20T10:30:00-04:00",
    "answers": [{"category": "Photo", "purpose": "Publication", "answer": "yes"}],
    "evidence": {"pdfSha256": "0x..."}
  }'

The verbal consent is never rewritten: each ticked purpose moves into a new manual consent, which the check follows from then on. The consent still dates from the call; the signature is its evidence date. A box left without a "Yes" on the paper is recorded as a withdrawal dated at the signature. If the paper never comes back, the consent stays verbal: Agreely does not lapse it for that. GET /v1/verbal-consents/{consentId} gives the full history of the call and the paper.

Renew by telephone a paper that is about to end

A consent signed on paper whose end falls within 30 calendar days may be renewed by telephone. Those 30 days are an Agreely product rule, not a period set by law. The request is an ordinary POST /v1/verbal-consents:

  • it is a new consent; the paper is never rewritten and stays recorded as it was;
  • /v1/check at once answers tier: "verbal" with the new validUntil, then manual when the new signed paper comes back. If your system requires manual for a use, it will see verbal between the call and the paper's return;
  • the renewal may not end before the paper (renewal_ends_before_current) nor be dated before its signature (renewal_predates_current);
  • a "no" at renewal withdraws nothing: the paper stays in force until its own end.

Outside the window, the call is refused as usual. A passkey-signed consent is never renewed by telephone: renew it with a new online request.

Two routes, depending on what you withdraw:

  • POST /v1/manual-consents/{consentRef}/revoke (scope attest, or attest_verbal for a verbal consent) withdraws a consent the organization recorded. Its response says in gate what /v1/check now answers for the purpose.
  • POST /v1/customers/{customerRef}/consents/{consentRef}/withdrawal (scope withdraw) records the withdrawal the person asked for on any consent, including the one she signed online with her passkey. This route stays open during a billing lapse.

Either way, a withdrawal attaches to the purpose: withdrawing through any reference in the chain (a renewal and the paper it renews, a call and the paper that confirmed it) withdraws the running consent. A sheet or a call dated before a recorded withdrawal never reopens it (predates_withdrawal).

The honest boundary

A consent the organization recorded does not prove a human consented

A company_attested consent documents that the organization attested it holds a signed sheet whose digest matches what was recorded. It does not prove that a human actually signed, nor who signed: there is no passkey assertion from the person. A company_documented consent is weaker still: it records that the organization stated, at a recorded instant (the receipt's digest is then anchored), that the person consented by telephone to these purposes under this script; it does not prove the call happened. These are legitimate paths for the no-passkey cases, which is why assurance and tier name them explicitly everywhere.

Next

  • API reference: every route, its bodies, its responses and its refusal reasons.
  • Consent requests: the citizen-signed path, and the company-side context for offline consent.
  • Citizen portal: what the person sees when they claim and confirm their consent.
  • Verify a receipt: a company_attested receipt verifies fully offline.