Record an offline consent
Not every person has a passkey. An elderly person who signs a paper form will
never go through the consent request approval
flow. For that case there is a separate path: the company records a consent
from a hand-signed PDF, under the attest scope. The result is a
company-attested consent that enforces exactly like a citizen-signed one.
You need an attest-scoped key.
Record the consent
You start from a hand-signed PDF and the published version of the disclosure
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 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 bytescurl -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 (one 0x-hex per
cell), assurance: "company_attested", and anchored: false (anchoring happens
afterward, asynchronously). Agreely.hashPdf is static and calls no network;
it produces exactly the shape evidence.pdfSha256 expects.
It enforces identically
A company-attested consent and a citizen-signed consent authorize identically
at /v1/check. Only the assurance tier differs: a check
returns assurance: "company_attested" instead of "citizen_signed". The code
that gates a feature changes nothing.
Bind the consent to the citizen later
The company can issue a single-use claim link so the person can later attach this offline consent to their own decentralized identifier.
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.
The honest boundary
An attested consent does not prove a human signed
A company_attested consent proves that the company attests it holds a signed PDF whose hash matches what was recorded. It does not prove that a human actually signed the document, nor who signed it: there is no passkey assertion from the person. It is a legitimate path for the no-passkey case, but its assurance tier is weaker than a citizen-signed consent, which is why the assurance field names it explicitly everywhere.
Next
- 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_attestedreceipt verifies fully offline.