FR EN

SDK TypeScript

@agreely/sdk est le client Node fin et entièrement typé pour l' API /v1. Un seul appel protège un usage de données par une vérification de consentement en direct et faisant autorité. Il n'y a aucune copie locale et aucune mémoire-cache : chaque check() est un appel frais, car mettre en cache une autorisation pendant qu'une révocation arrive est une faute d'exactitude.

  • DX à un seul appel, typée de bout en bout (ESM + CJS, .d.ts complet).
  • Refus par défaut : une panne refuse.
  • Node 18+ (fetch global), avec un repli undici paresseux et optionnel.

Cette page documente la version 0.6.1, celle publiée sur npm. Les membres de l'API que 0.6.1 n'envoie pas ou ne type pas sont énumérés dans Ce que 0.6.1 ne couvre pas.

Installer

npm install @agreely/sdk

Construire

import { Agreely } from "@agreely/sdk";

const agreely = new Agreely({ apiKey: process.env.AGREELY_API_KEY! });

Toutes les options du constructeur :

Option Défaut Rôle
apiKey (requis) (aucun) une valeur vide lève AgreelyConfigError
baseUrl https://api.agreely.ca racine de l'API
timeout 800 (ms) budget total par appel, nouvelles tentatives comprises
degradeOnOutage (désactivé) adhésion explicite à l'ouverture en cas de panne
onBreakGlass (aucun) collecteur d'audit (event) => void du bris de glace
maxDegradeWindow "24h" plafond ferme du ttl de bris de glace et de la fenêtre de panne
fetch fetch global injectez votre propre implémentation
maxRetries 0 tentatives supplémentaires sur un 429 rate_limited, lectures et vérifications seulement
respectRetryAfter true honore le Retry-After du serveur

Les ressources sont des propriétés en lecture seule : agreely.consentDocuments, agreely.consentRequests, agreely.manualConsents, agreely.verbalConsents, agreely.withdrawals, agreely.relationships, agreely.customers, agreely.catalog, agreely.retention, agreely.inventory, agreely.breakGlass.

check() renvoie un booléen

allow est le seul true. Envoyez les libellés bruts lisibles; le SDK les transmet tels quels et ne normalise jamais la catégorie ni la finalité. Le serveur les reconnaît en français ou en anglais, avec ou sans accents, sans égard à la casse ni aux espaces; un libellé ambigu ou non déclaré refuse.

if (await agreely.check("cust_8812", "Phone number", "Billing")) {
  // ...you may use the phone number for billing
}

checkDetailed() renvoie la décision

const d = await agreely.checkDetailed("cust_8812", "Phone number", "Billing");
// { decision: "allow" | "deny",
//   status: Status,                // the nine values below
//   consentRef?: "0x…",            // absent for "none" and "necessity"
//   checkedAt: "2026-…Z",
//   assurance?: "citizen_signed" | "company_attested" | "company_documented",
//   tier?: "full" | "manual" | "verbal",   // present exactly when assurance is
//   basis?: Basis,                 // ONLY on "necessity"
//   validUntil?: "2027-…Z",        // the end of the consent behind the answer
//   revokedAt?: "2026-…Z",         // ONLY on "revoked"
//   degraded?: boolean, mode?: string }  // present only on a degraded allow

Un refus est un 200 normal : checkDetailed le renvoie, il ne lève pas d'exception. Seuls l'authentification, la validation, la facturation, la limite de débit et les pannes lèvent une exception. validUntil et revokedAt sont absents, pas nuls, quand aucun enregistrement ne fonde la réponse. Ils datent la réponse; ce n'est pas une durée de vie : ne mettez jamais une autorisation en cache jusqu'à son validUntil, un retrait peut arriver à tout moment.

Deux statuts autorisent, sept refusent :

status decision Ce que cela veut dire
active allow un consentement en vigueur fonde la cellule
necessity allow aucun consentement : la cellule du catalogue déclare un fondement autre que le consentement, prévu par la loi de l'organisation. Porte basis, sans consentRef, assurance ni tier
none deny aucun enregistrement sur une cellule fondée sur le consentement; c'est aussi ce que donne une cellule effacée
revoked deny le consentement a été retiré
expired deny la durée du consentement est écoulée
relationship_ended deny l'entreprise a consigné la fin de la relation; le consentement lui-même n'a jamais été retiré
requires_depersonalization deny le fondement déclaré n'existe que pour des renseignements dépersonnalisés, ce qu'un client nommé ne peut pas remplir
basis_not_in_regime deny le fondement déclaré n'existe pas dans la loi de l'organisation (souvent après un changement de secteur)
erased deny prévu au contrat, non émis à ce jour : une cellule effacée donne none

Traitez un statut inconnu comme un refus : lisez d.decision. Une autorisation necessity n'est pas un consentement : elle repose sur un fondement que l'entreprise a déclaré à son catalogue, sans preuve signée derrière, et Agreely ne juge pas de sa validité juridique. Lisez company.sector dans identity() avant d'aiguiller sur basis, dont le vocabulaire dépend de la loi.

Une autorisation fondée sur un consentement dit la force de sa preuve, et votre système décide de ce que chaque palier peut débloquer :

tier assurance Le consentement
full citizen_signed la personne l'a signé avec une clé d'accès
manual company_attested l'organisation a consigné un papier signé à la main
verbal company_documented la personne a consenti par téléphone; l'organisation l'a documenté

Une ligne qu'un document donne à titre d'information est consignée comme une prise de connaissance, jamais comme un consentement : la vérification y répond comme en l'absence d'enregistrement (necessity ou un refus), sans consentRef, assurance ni tier.

Vérification par lot : checkBatch() et checkFields()

Pour évaluer plusieurs cellules en un seul aller-retour au lieu de N appels à check(). Les décisions renvoyées sont alignées sur les items soumis. Les category / purpose partent bruts (le serveur normalise). En cas de panne, ces méthodes lèvent AgreelyUnavailableError (pas de dégradation ici).

const decisions = await agreely.checkBatch([
  { customerRef: "cust_8812", category: "Phone number", purpose: "Billing" },
  { customerRef: "cust_8813", category: "Email address", purpose: "Newsletter" },
]);
// BatchDecision[]: { customerRef, category, purpose, decision, status,
//   consentRef?, assurance?, tier?, basis?, validUntil?, revokedAt?, checkedAt }

// checkBatch([]) court-circuite vers [] côté client, sans aucun aller-retour.

checkFields() est la forme ergonomique en produit cartésien : donnez une liste de clients et une liste de champs, elle construit toutes les paires, appelle checkBatch() une fois, et renvoie une barrière isAllowed prête à l'emploi.

const grid = await agreely.checkFields(
  ["cust_8812", "cust_8813"],
  [ { category: "Phone number", purpose: "Billing" },
    { category: "Email address", purpose: "Newsletter" } ],
);
// CheckFieldsResult: { decisions, isAllowed(customerRef, category, purpose): boolean }

if (grid.isAllowed("cust_8812", "Phone number", "Billing")) {
  // ...you may display this field for this customer
}

Un lot compte au plus 500 cellules (BATCH_CAP), refusé côté client avant l'appel. checkFields multiplie les clients par les champs : le plafond arrive plus vite qu'il n'y paraît, et son erreur indique la taille de page qui passe.

Cas d'usage : protéger l'affichage d'une liste de clients

Afficher un tableau de N clients x M champs, c'est N x M utilisations de renseignements personnels. La loi limite l'utilisation aux fins de la collecte (P-39.1, art. 12; A-2.1, art. 65.1) et l'accès aux membres du personnel pour qui le renseignement est nécessaire à leurs fonctions (P-39.1, art. 20; A-2.1, art. 62). Agreely encadre chaque champ affiché d'une vérification et en consigne la décision, pour documenter cette limitation. checkFields regroupe la grille entière en un seul aller-retour et vous rend un isAllowed(customerRef, category, purpose) à appeler par cellule. Le regroupement est une optimisation de la même responsabilité par décision (chaque cellule reste décidée et journalisée par le serveur), pas un affaiblissement.

Identité de la clé

identity() interroge GET /v1/whoami et renvoie les portées que porte réellement la clé, vérifiées côté serveur (n'importe quelle portée y accède), et le minimum qu'il faut pour découvrir l'organisation plutôt que de la coder en dur : son nom, son secteur, la loi qui la régit et l'adresse absolue de sa page publique de confidentialité (null quand cette page ne se résoudrait pas). La réponse reste à divulgation minimale : aucun identifiant d'entreprise, aucun DID, aucun nom de clé, rien sur le forfait, l'usage ou la facturation.

const me = await agreely.identity();
// Identity: {
//   scopes: Scope[],   // "check" | "issue" | "attest" | "attest_verbal" | "relationship"
//                      // | "registry" | "retention" | "inventory" | "withdraw" | "holds"
//   company: { name: string, slug: string, sector: "private" | "public",
//              statute: "P-39.1" | "A-2.1", publicPolicyUrl: string | null }
// }
if (me.scopes.includes("issue")) {
  // ...this key may issue consent requests
}
me.company.statute;         // "P-39.1": the act whose vocabulary /v1/catalog uses
me.company.publicPolicyUrl; // link your public form to it instead of hard-coding it

// The configured endpoint is also exposed client-side:
agreely.baseUrl; // "https://api.agreely.ca"

company.slug est l'identifiant public de l'organisation, celui que porte l'adresse de sa page publique de confidentialité. L'API peut aussi lister une onzième portée, diffusion, que 0.6.1 ne type pas mais laisse passer.

Appelée une fois par minute depuis un seul planificateur, elle sert aussi de sonde : sur un 401 ou un 402, videz tout cache tiré d'Agreely et refusez par défaut.

Documents de consentement

Les consentements sur papier, les consentements par téléphone et les feuilles de consentement prennent un documentVersionId. Retenez le code stable du document et résolvez par lui la version publiée aujourd'hui, plutôt que de copier un identifiant depuis un navigateur (portée check ou issue) :

const documents = await agreely.consentDocuments.list();
// [{ code, documentVersionId, version, name, nameEn, effectiveDate, publishedAt, items }]
const doc = await agreely.consentDocuments.get("conditions-marketing");
// ...plus disclosure, responsable, automatedDecision, profiling, integrity

// The information document of one version, to print and hand to the person
// (scope check, issue, attest or attest_verbal):
const info = await agreely.consentDocuments.getInformationPdf(doc.documentVersionId, { locale: "fr" });
await writeFile(info.filename ?? "information.pdf", info.pdf); // Uint8Array

Le PDF est servi par identifiant de version, jamais par code, et une version remplacée reste servie (elle le dit). L'anglais est refusé (english_text_missing) quand une section n'a pas de texte anglais. Les octets ne sont pas figés : gardez votre propre copie. L'obtenir ne consigne rien : ce n'est pas une preuve que quiconque a été informé. Un rendu dispose d'un budget d'au moins 15 s, modifiable avec { timeout }.

Demandes de consentement

create envoie un courriel au destinataire. Il nomme le document de consentement publié sous lequel la demande est émise, par identifiant de version ou par code (l'un ou l'autre, pas les deux); les catégories et finalités demandées découlent de ce document.

const r = await agreely.consentRequests.create({
  customerId: "cust_8812",
  recipientEmail: "[email protected]",
  documentCode: "conditions-marketing",   // or: consentDocumentId: "<documentVersionId>"
  validUntil: "2031-01-01",
});
// IssuedRequest: { requestId: "0x…64hex", status: "pending", deepLink, emailDelivered, items, document }

validUntil (ici comme sur les consentements sur papier et par téléphone) est une date simple, qui veut dire la fin de ce jour dans le fuseau de l'organisation, ou un instant RFC 3339 avec décalage. Une expression relative est refusée, comme une fin à plus de 10 ans du début (un plafond du produit Agreely, pas un plafond prévu par la loi).

create n'est jamais ré-essayé automatiquement (il envoie un courriel). Le SDK attache un Idempotency-Key unique par appel; passez le vôtre pour qu'une nouvelle tentative rejoue l'original :

await agreely.consentRequests.create(input, { idempotencyKey: "order-4471" });

Avant d'émettre, hasPending vous dit si une demande est déjà en attente pour ce client (au besoin pour un document), pour ne pas écrire deux fois à la personne. Elle signale une demande en attente; elle ne dit rien du consentement.

if (!(await agreely.consentRequests.hasPending("cust_8812", "conditions-marketing"))) {
  await agreely.consentRequests.create(input);
}

Lister, obtenir, parcourir toutes les pages, et attendre le règlement

const page = await agreely.consentRequests.list({
  customerId: "cust_8812", // optional
  status: "pending",       // optional
  limit: 50,               // default 50, max 100
  cursor,                  // a prior nextCursor
});
// { items, nextCursor }: newest first, nextCursor is null when exhausted
const one = await agreely.consentRequests.get("0x…");  // the requestId, NOT a uuid
// ConsentRequestRecord: { requestId, status, validUntil, expiresAt, createdAt,
//   settledAt (nullable), customerId, documentCode, items, document }

status vaut pending, approved, asks_declined, refused, expired ou revoked_before_action. approved veut dire que la personne a accepté au moins une demande de consentement; une demande dont toutes les demandes ont été déclinées donne asks_declined.

listAll est un générateur asynchrone (défaut maxPages 1000); collect réunit tout en un tableau; waitForSettlement interroge get jusqu'à ce que la demande soit réglée (asks_declined compte comme réglée).

for await (const rec of agreely.consentRequests.listAll({ status: "pending" })) {
  // one ConsentRequestRecord at a time
}
const all = await agreely.consentRequests.collect({ status: "pending" });

// Poll until settled. Defaults: intervalMs 2000, timeoutMs 120000.
const settled = await agreely.consentRequests.waitForSettlement("0x…", {
  intervalMs: 2000,
  timeoutMs: 120000,
  signal: controller.signal, // TS only: an AbortSignal
});

waitForSettlement lève AgreelyTimeoutError si le budget client s'écoule (ce n'est pas une panne serveur), et AgreelyAbortError si l'AbortSignal fourni se déclenche.

Annuler une demande en attente

cancel annule une demande en attente par son requestId de protocole (la voie « révocation avant action »). Elle est idempotente : une demande déjà terminale n'est pas une erreur, elle renvoie cancelled: false. Un requestId inconnu lève AgreelyNotFoundError.

const c = await agreely.consentRequests.cancel("0x…");
// CancelledRequest: { requestId, status: "revoked_before_action", cancelled: true }
// Already terminal -> { requestId, status, cancelled: false } (no error)

Consentements sur papier (portée attest)

Pour un consentement recueilli sur papier (un PDF signé à la main). Vous consignez la preuve : le hachage SHA-256 du PDF signé, et facultativement le PDF lui-même. items nomme les demandes de consentement cochées sur la feuille, et peut être vide quand chacune a reçu un « non ». Les lignes que le document donne à titre d'information sont ajoutées par le serveur comme prises de connaissance et reviennent dans acknowledged.

const m = await agreely.manualConsents.record({
  customerId: "cust_8812",
  documentVersionId: doc.documentVersionId,
  effectiveDate: "2026-06-01",
  validUntil: "2031-01-01",
  items: ["<catalogEntryId>", { category: "Email address", purpose: "Newsletter" }],
  evidence: {
    pdfSha256: Agreely.hashPdf(bytes), // "0x"+sha256 of the SIGNED scan, required
    pdf: base64Pdf,                    // optional opt-in upload
  },
  sensitiveExpressAttested: true, // when a ticked purpose is sensitive and rests on consent
  versionAttested: true,          // when the paper was signed before this version's publication
});
// ManualConsentResult: { consentId, merkleRoot, consentRefs[], assurance: "company_attested",
//   anchored: false, acknowledged: [{ category, purpose, consentRef }], asksDeclined }

record n'est jamais ré-essayé automatiquement. Il porte un Idempotency-Key comme consentRequests.create : une nouvelle tentative avec la même clé et le même corps rejoue la réponse d'origine et ne consigne rien de nouveau; la même clé avec un autre corps est une nouvelle demande. Une clé compte de 1 à 255 caractères ASCII imprimables.

Refus, par leur e.reason stable : 422 notice_only, pdf_hash_mismatch, valid_until_too_far, effective_date_in_future, sensitive_express_required, version_attestation_required; 409 stronger_consent_active, relationship_ended, verbal_awaits_paper. Rien n'est consigné.

// A link for the person to claim the consent in their own space.
const link = await agreely.manualConsents.createClaimLink({ customerId: "cust_8812" });
// { claimUrl, token, expiresAt }

// Revoke or erase by consentRef.
const w = await agreely.manualConsents.revoke("0x…", { reason: "customer request" });
// { consentRef, revoked: true, alreadyRevoked, gate: "denied" | "superseded" | "unchanged" }
const e = await agreely.manualConsents.erase("0x…", { reason: "erasure request" });
// { consentRef, erased: true, alreadyErased, gate }

// Keep in force a consent the person signed online (see the refusal below).
const k = await agreely.manualConsents.revoke("0x…", { otherConsents: "keep" });
// { ..., gate: "superseded", stillInForce: true }

gate dit ce que fait maintenant /v1/check pour cette finalité : denied (ce consentement la fondait), superseded (un autre consentement la fonde, et la vérification peut encore autoriser) ou unchanged (une répétition idempotente). Lisez gate, et non revoked, avant de dire à quiconque que l'usage a cessé. Une révocation ou un effacement est refusé en 409 citizen_consent_at_gate quand un consentement que la personne a signé en ligne tient la finalité : ces routes n'y mettent jamais fin, consignez plutôt son retrait avec withdrawals.record. otherConsents: "keep" met fin à cet enregistrement et laisse ce consentement en vigueur, et la réponse porte stillInForce: true; sans lui (ou avec "withdraw", que ces routes traitent de la même façon), le 409 tient. Les deux routes n'acceptent que reason et otherConsents.

Imprimer une feuille de consentement

createConsentSheet renvoie la feuille de signature d'une version publiée pour un client, avec un nouveau lien de réclamation et la référence imprimée qui le protège :

const sheet = await agreely.manualConsents.createConsentSheet("cust_8812", {
  documentVersionId: doc.documentVersionId,
  locale: "fr",
});
const pdf = Buffer.from(sheet.signatureSheet.pdf, "base64"); // print this
// sheet.printedReference: "ABCD-EFGH" (on the sheet)
// sheet.claim: { claimUrl, token, expiresAt } (send it SEPARATELY)

N'envoyez jamais le lien de réclamation dans la même enveloppe ni le même message que la feuille : remis ensemble, la référence imprimée ne protège rien. Et ne consignez jamais le hachage de la feuille vierge comme preuve : la preuve est le papier signé, numérisé à son retour. La référence et le jeton ne sont renvoyés qu'une fois et ne sont conservés nulle part; une nouvelle feuille retire la réclamation précédente.

Consentements par téléphone (portée attest_verbal)

Un consentement que la personne a donné par téléphone, documenté par votre organisation. Il n'y a aucun document : la preuve est votre parole. Chaque résultat dit tier: "verbal" et assurance: "company_documented". La portée attest_verbal n'est jamais accordée par défaut.

const v = await agreely.verbalConsents.record({
  customerId: "cust_8812",
  documentVersionId: doc.documentVersionId,
  answers: [                             // every ask put on the call, answered explicitly
    { category: "Email address", purpose: "Newsletter", answer: "yes" },
    { category: "Email address", purpose: "Surveys", answer: "no" },
  ],
  obtainedAt: callEndedAt,               // at most 7 days old, never in the future
  obtainedBy: "Julie, customer service",
  scriptVersion: "script-2026.09",
  respondent: { consentedBy: "self" },   // or a representative and its capacity
  validUntil: "2027-09-29",
  paperExpected: true,
});
// { consentId, merkleRoot, consentRefs, tier: "verbal", assurance: "company_documented",
//   anchored: false, paperExpected, acknowledged, asksDeclined }

// The signed paper came back (scope attest): each "yes" moves into a new paper consent.
const p = await agreely.verbalConsents.confirmWithPaper(v.consentId, {
  signedAt: "2026-10-02T09:00:00-04:00",
  answers: [{ category: "Email address", purpose: "Newsletter", answer: "yes" }],
  evidence: { pdfSha256: Agreely.hashPdf(bytes) },
});

// What was said, what the paper said, what is in force:
const h = await agreely.verbalConsents.get(v.consentId);
// h.state: "awaiting_paper" | "verbal_only" | "paper_received"

Pour un mineur de moins de 14 ans, passez isMinor: true et un représentant en qualité de titulaire_autorite_parentale ou tuteur_mineur. Une limite quotidienne s'applique (429 AgreelyVerbalDailyCapError, jamais ré-essayé automatiquement). Retirez avec manualConsents.revoke; une fois le papier revenu, retirez plutôt la référence du consentement sur papier.

Les retraits consignés au nom de la personne (portée withdraw)

Quand une personne vous demande, par n'importe quel canal, de retirer un consentement, consignez-le ici. Le retrait atteint tout consentement que vous détenez pour elle, y compris un consentement qu'elle a signé en ligne, et la vérification refuse aussitôt. La route fonctionne même si votre facturation est échue : donner suite à un retrait est une obligation légale, pas un service payant.

const w = await agreely.withdrawals.record("cust_8812", "0x…consentRef", {
  channel: "phone",        // phone | email | mail | in_person | other
  operator: "agent-042",   // YOUR opaque staff id, never an email address
  requestedAt: "2026-10-08T14:05:00-04:00", // optional: when they asked
  reason: "Asked on the phone", // optional, at most 1000 characters
  otherConsents: "keep",   // see below: "withdraw" | "keep", or alsoWithdraw: ["0x…"]
});
// { consentRef, withdrawn: true, alreadyWithdrawn, recordedOnBehalf: true,
//   assurance: "company_documented", gate: "denied" | "superseded" | "unchanged",
//   alsoWithdrawn: ["0x…"], stillInForce: ["0x…"] }
  • La réponse dit qui parle. recordedOnBehalf: true, et le serveur envoie assurance: "company_documented" : Agreely documente que votre organisation dit avoir reçu la demande et y avoir donné suite, jamais que la personne a signé une révocation.
  • Un autre consentement de la même finalité. Quand un autre consentement de la finalité est en vigueur pour ce client (deux personnes peuvent chacune en détenir un), l'appel dit ce qu'il en advient : otherConsents: "withdraw" les retire tous, alsoWithdraw: ["0x…"] nomme ceux qu'il couvre (au plus 50, WITHDRAWAL_SCOPE_MAX; [] ne déclare rien), et otherConsents: "keep" retire ce consentement seul. Jamais les deux ensemble : le SDK refuse avant tout appel (AgreelyConfigError), comme tout membre inconnu. Sans déclaration, la réponse est 422 other_consents_running, une AgreelyOtherConsentsRunningError dont otherConsentRefs liste ces consentements, rien d'écrit : demandez à la personne lesquels, puis réessayez. Ceux retirés au même instant reviennent dans alsoWithdrawn, ceux encore en vigueur dans stillInForce.
  • Sa propre portée, jamais accordée par défaut, et un plafond quotidien (429 AgreelyDailyCapError, withdrawal_daily_cap, sans Retry-After); le propriétaire du compte est avisé.
  • Refus : 404 unknown_consent, 409 consent_lapsed, 422 not_revocable, requested_at_in_future, requested_at_before_grant, not_running_for_purpose (une entrée d'alsoWithdraw qui n'est pas un autre consentement en vigueur pour la finalité). Accepté après la fin de la relation. Jamais ré-essayé automatiquement; une nouvelle tentative avec la même clé et le même corps rejoue la première réponse.

Fin d'une relation client

Consignez, depuis votre propre code, qu'une relation client est terminée (P-39.1, art. 23; A-2.1, art. 73). Portée relationship. Le customerRef est votre propre référence (la même que pour check), rattachée à l'entreprise de la clé. Le reason est requis (un motif vide lève une AgreelyError). L'appel est idempotent : re-finir une relation déjà terminée renvoie le endedAt et l'origine endedBy d'origine. C'est une surcouche de cycle de vie : elle ne révoque ni n'efface aucun consentement.

const r = await agreely.relationships.end({ customerRef: "cust_8812", reason: "Compte fermé, fins accomplies" });
// RelationshipEnded: { customerRef, status: "ended", endedAt, endedBy: "company" | "citizen_request" }

0.6.1 consigne la fin à l'instant de l'appel; l'API accepte aussi un endedAt antérieur, que 0.6.1 n'envoie pas.

Une fin enregistrée par erreur s'annule, dans la fenêtre de correction de 30 jours (une règle de produit d'Agreely, pas un délai prévu par la loi) et tant que rien n'a été détruit (P-39.1, art. 11; A-2.1, art. 72). Le reason est requis. Une relation non admissible lève une AgreelyNotFoundError sans rien écrire : une personne qui revient est une nouvelle relation, qui exige une nouvelle demande de consentement.

const u = await agreely.relationships.revert({ customerRef: "cust_8812", reason: "Fin enregistrée par erreur" });
// RelationshipReverted: { customerRef, status: "active", reverted: true }

Le registre des clients (portée registry)

L'identité que votre propre système détient pour l'un de ses clients, un customerRef à la fois (il n'existe aucune route de liste, par construction).

const r = await agreely.customers.upsert("cust_8812", {
  displayName: "Marie Tremblay",
  email: null,          // null or "" CLEARS a value
  noticeLocale: "fr",
});                     // a member you leave out is left as it stands
// { customerRef, registered, source, hasDisplayName, hasEmail, noticeLocale,
//   createdAt, updatedAt, relationship, created }

const meta = await agreely.customers.get("cust_8812"); // metadata only
  • Le registre ne porte plus de fondement par client. legalBasis et basisNote ont quitté l'API : une valeur envoyée est ignorée, et la réponse ne porte plus legalBasis ni hasBasisNote. 0.6.1 ne les envoie ni ne les type. La finalité appartient au traitement, documentée par cellule du catalogue.
  • Une fusion, jamais un remplacement. created vaut vrai quand l'appel a créé la fiche (HTTP 201), faux quand il a fusionné (200).
  • La lecture ne rend que des métadonnées : hasEmail: true, jamais l'adresse.
  • 409 porte sa chaîne stable dans code : identity_erased (une destruction a retiré l'identité; un effacement de valeur passe encore) ou identity_held (une suspension la garde telle quelle).

Catalogue

const catalog = await agreely.catalog.list();
// CatalogEntry[]: { id, category, purpose, description (nullable), legalBasis, sensitive }

const form = await agreely.catalog.forDocument("conditions-marketing");
// { regime, document: { code, documentVersionId }, catalog }: only the cells that document groups

const { cells } = await agreely.catalog.listCells(); // each cell with its retentionRuleKey

Le catalogue dit ce qui a été déclaré, jamais si cette personne a consenti : seule une vérification le dit.

Conservation (portées retention, holds, registry)

Agreely décide des règles de conservation et consigne ce que vos systèmes déclarent. Votre système lit les règles, mène ses propres purges et les déclare. Agreely n'observe rien dans vos systèmes et n'en vérifie rien : chaque écriture répond status: "declared".

// A nightly purge job. Its key carries 'retention' AND 'holds'.
const run = await agreely.retention.rulesForPurge({ snapshot: await loadSnapshot() });
if (run.source === "abstain") return; // Agreely unreachable: do NOT purge this run
await saveSnapshot(run.snapshot);

const held = await agreely.retention.syncHolds(); // every active hold; any error throws: do not purge

for (const rule of run.rules) {
  const purged = await purgeExpired(rule, held.holds); // your code: skip what a hold covers
  const idempotencyKey = `${jobRunId}:${rule.key}`;    // durable, never random per attempt
  if (purged.count > 0) {
    await agreely.retention.declarePurge(rule.key, {
      ranAt: new Date(),
      recordsAffected: purged.count,   // at least 1
      method: "destroyed",             // or "anonymized" + anonymizationProcessKey
      coveredFrom: "2025-01-01",
      coveredUntil: "2025-01-31",
      hostSystem: "billing",           // a STABLE name, never a pod name
      hostCategory: "invoices",
    }, { idempotencyKey });
  } else {
    await agreely.retention.declareSweep(rule.key, { sweptAt: new Date(), hostSystem: "billing" }, { idempotencyKey });
  }
}
  • Une tâche de purge échoue fermée en ne purgeant pas. Une panne renvoie source: "abstain", sans rules. Purger sur l'instantané conservé est une exception explicite, bornée et auditée (onOutage: "use-snapshot", maxSnapshotAge, un onDegrade obligatoire). Un échec d'authentification, de facturation, de validation ou de limite de débit lève une exception au lieu de s'abstenir.
  • Les suspensions suspendent les règles pour un client. syncHolds() lit un instantané, puis syncHolds({ changedSince }) les changements depuis votre curseur; traitez toute erreur sur ce flux comme « ne pas purger ».
  • Un client (portée registry) : getCustomerRetention, placeHold, releaseHold, listHolds, declareDisposition. Une disposition exige que la relation ait d'abord pris fin (409 relationship_active).
  • Le plancher de balayage (un par règle et par système hôte toutes les 15 minutes) lève AgreelySweepTooFrequentError, jamais ré-essayé automatiquement. Les noms de systèmes hôtes sont rationnés (10 par organisation sur 30 jours).

Inventaire (portée inventory)

await agreely.inventory.replaceCategories({
  hostSystem: "crm",
  categories: [{ key: "benevoles", label: "Bénévoles", fields: [{ key: "nom", label: "Nom" }] }],
});
const sets = await agreely.inventory.listCategories({ hostSystem: "crm" });
const statement = await agreely.inventory.getStatement(statementKey); // also readable with a 'check' key

replaceCategories envoie la liste complète pour ce système : tout ensemble déclaré auparavant et omis est retiré. Les champs portent des noms, jamais des valeurs. Jusqu'à 200 ensembles par déclaration et 60 champs par ensemble.

Hacher un PDF

hashPdf et hashPdfFile sont statiques et n'appellent aucun réseau. Elles produisent la forme exacte attendue par evidence.pdfSha256 : "0x" + le SHA-256 en hexadécimal.

const h1 = Agreely.hashPdf(bytes);        // bytes is a Uint8Array
const h2 = await Agreely.hashPdfFile("/path/to/consent.pdf");

Délais, nouvelles tentatives et limites

Le délai par défaut est un budget total serré de 800 ms, prévu pour un appelant dans la même région que l'API. Sur Internet, une poignée de main TLS à froid peut le dépasser avec un serveur en santé, et comme le SDK refuse par défaut, un délai dépassé est un refus injustifié. Donnez de la marge à un appelant exposé à Internet :

new Agreely({ apiKey, timeout: 8000, maxRetries: 1 });

Seules les lectures et la vérification sont ré-essayées sur une panne passagère (jusqu'à 2 tentatives, dans le budget). maxRetries ré-essaie aussi un 429 rate_limited après son Retry-After, et aucun autre 429. Aucune écriture n'est jamais ré-essayée automatiquement. Les deux appels qui produisent un PDF (getInformationPdf, createConsentSheet) disposent d'au moins 15 s.

Limite Valeur Au-delà
lot 500 cellules par checkBatch / checkFields AgreelyConfigError avant l'appel
débit 120 requêtes par minute par entreprise, toutes clés comprises 429 AgreelyRateLimitError avec Retry-After
retraits 50 par entreprise par 24 heures par l'API 429 AgreelyDailyCapError
suspensions 1 000 posées, 20 levées que l'API n'a pas posées, par 24 heures 429 AgreelyDailyCapError
inventaire 200 ensembles par déclaration, 1 000 nouvelles clés d'ensemble par 30 jours AgreelyConfigError; 422 new_set_limit
customerRef 200 caractères AgreelyConfigError avant l'appel

Ce sont des valeurs par défaut du déploiement : concevez en fonction d'elles, ce ne sont pas des engagements contractuels.

Erreurs typées

Chaque échec est une sous-classe d'AgreelyError (avec .code, .status, .field, .reason). Un refus n'est jamais une erreur. Toute méthode qui renvoie une promesse la rejette, elle ne lève jamais d'exception de façon synchrone, et les options d'une écriture sont fermées : un idempotencykey mal orthographié est refusé, jamais ignoré.

Erreur Quand
AgreelyAuthError 401 / 403
AgreelyValidationError 400 / 422 (.field nomme l'entrée)
AgreelyOtherConsentsRunningError 422 other_consents_running sur un retrait (une AgreelyValidationError) : .otherConsentRefs liste les consentements en vigueur pour la finalité
AgreelyNotFoundError 404
AgreelyConflictError 409 : code: "conflict" (l'état de l'enregistrement le refuse), code: "retry" (réessayez avec le même Idempotency-Key), ou un état nommé
AgreelyBillingInactiveError 402 (code: "billing_inactive") : l'abonnement est échu. Ce n'est pas une panne : check() renvoie false, checkDetailed() la lève
AgreelyRateLimitError 429 (.retryAfter en secondes)
AgreelyDailyCapError 429 sur un plafond quotidien (withdrawal_daily_cap, hold_budget_exhausted, hold_release_cap_reached); jamais ré-essayée automatiquement
AgreelyVerbalDailyCapError 429 verbal_daily_cap; jamais ré-essayée automatiquement
AgreelySweepTooFrequentError 429 sur le plancher de balayage (.retryAfterSeconds); jamais ré-essayée automatiquement
AgreelyUnavailableError 503 / réseau / délai (la seule erreur soumise à la politique de dégradation)
AgreelyTimeoutError budget de sondage client écoulé dans waitForSettlement (ce n'est pas une panne serveur)
AgreelyAbortError l'AbortSignal de l'appelant s'est déclenché
AgreelyConfigError mauvaise configuration client ou entrée invalide, avant toute requête

Aiguillez sur e.reason, jamais sur le message. Les routes des consentements sur papier et par téléphone, des liens de réclamation, des feuilles de consentement et des retraits portent une reason stable; les routes du registre, des suspensions, des dispositions, de l'inventaire et des documents portent leur chaîne stable dans code. AgreelyErrorReason reste ouvert : traitez une raison inconnue selon son statut HTTP.

import { AgreelyConflictError } from "@agreely/sdk";

try {
  await agreely.manualConsents.revoke(ref);
} catch (e) {
  if (e instanceof AgreelyConflictError && e.reason === "citizen_consent_at_gate") {
    await agreely.withdrawals.record(customerRef, ref, { channel: "email", operator: "agent-042" });
  } else throw e;
}

Refus par défaut, et l'exception à trois barrières

Lorsqu'Agreely est injoignable (503 / délai d'expiration / réseau), check() refuse et checkDetailed() lève AgreelyUnavailableError. Un véritable refus 200 n'est jamais touché.

Inscrire une catégorie en ouverture en cas de panne (fail-open) est possible, mais seulement à travers trois barrières indépendantes, afin que cela ne puisse jamais se produire par accident. Une autorisation dégradée est renvoyée comme { decision: "allow", status: "active", degraded: true, mode: … } sans champ assurance : c'est une dérogation délibérée et auditée au refus par défaut.

Barrière 1 : liste d'autorisation de configuration (avec un collecteur d'audit obligatoire)

const agreely = new Agreely({
  apiKey,
  degradeOnOutage: {
    mode: "fail-open",                 // the explicit word
    categories: ["Browsing/usage"],    // ONLY these may ever degrade
    maxOutageWindow: "5m",             // refuse to degrade past this
    onDegrade: (ctx) => audit.log(ctx) // MANDATORY: without it the constructor throws
  },
});

Barrière 2 : l'adhésion par appel

// Effective ONLY because the category is allow-listed above. Without the
// config, this per-call opt-in still denies.
await agreely.check("cust_8812", "Browsing/usage", "Analytics", { onOutage: "allow" });

Barrière 3 : bris de glace (le levier de l'exploitant)

agreely.breakGlass.engage({ reason: "incident-4471", ttl: "30m", scope: ["Browsing/usage"] });
// ...degraded checks in scope now allow, tagged breakGlass:true, until ttl expires
agreely.breakGlass.disengage();

Le bris de glace expire automatiquement, exige une reason (l'engager sans en fournir une lève une exception), et est indépendant de la liste d'autorisation de configuration. C'est une capacité propre au SDK TypeScript : le SDK PHP, à portée de requête, s'arrête à deux barrières.

Aucune mémoire-cache d'autorisations, et des fenêtres bornées

Il n'y a aucune mémoire-cache des décisions d'autorisation, par conception. Chaque autorisation dégradée émet un enregistrement de preuve via onDegrade, et chaque autorisation accordée par bris de glace émet un événement onBreakGlass. Le maxDegradeWindow du client (défaut 24 h) plafonne à la fois le ttl d'un bris de glace et la fenêtre de panne; une valeur au-delà du plafond lève AgreelyConfigError plutôt que d'ouvrir une fenêtre d'ouverture en cas de panne non bornée.

Une { onOutage: "allow" } par appel qui n'est pas adossée à une entrée degradeOnOutage.categories correspondante n'a aucun effet : la vérification refuse toujours (et le SDK consigne un avertissement de développement unique, réduit au silence avec AGREELY_SILENCE_WARNINGS).

Vérifier un ConsentReceipt

Agreely.verifyReceipt est statique, ne demande aucune clé d'API, et rend un verdict sur toute entrée, jamais une exception. Il prend le dossier de vérification qu'émet un consentement par clé d'accès, ou un reçu seul (isVerificationBundle distingue l'un de l'autre), et distingue une vérification qui a échoué (fail) d'une vérification qui n'a pas pu aboutir (unavailable), pour qu'un reçu valide pendant une panne ne ressemble jamais à une contrefaçon.

import { Agreely, isVerificationBundle } from "@agreely/sdk";

isVerificationBundle(bundle); // true for a verification bundle, false for a bare receipt
const v = await Agreely.verifyReceipt(bundle, { rpcUrl: "https://base-rpc.publicnode.com" });
v.overall;    // "verified" | "partial" | "incomplete" | "failed" | "unavailable" | "malformed" | "withdrawn"
v.withdrawal; // "active" | "withdrawn" | "not_checked"
const inForce = v.overall === "verified" && v.withdrawal === "active";

Un reçu sur papier (company_attested) et un consentement par clé d'accès vérifié à partir de son dossier peuvent atteindre verified, soit les contrôles énumérés, pas une preuve juridique; un reçu par clé d'accès seul rend incomplete, et un reçu téléphonique (company_documented) s'arrête à partial. Avec un rpcUrl, le SDK lit aussi les ancrages sur la chaîne et le retrait : withdrawn veut dire que la chaîne consigne le retrait d'un consentement authentique, qui n'est plus en vigueur. Les clés viennent des documents DID que lit le vérificateur : un resolver qui sert un document sans la clé fait rendre failed à un reçu authentique. Un failed seul n'est donc pas une preuve de falsification : lisez le contrôle qui a échoué et notes, ou vérifiez le reçu à app.agreely.ca/verify avec son dossier de vérification. La page dédiée détaille les contrôles, les options (timeoutMs, rpcDeadlineMs, registryAddress), le tableau ce qui est prouvé contre ce qui est présumé, et pourquoi :

Ce que 0.6.1 ne couvre pas

L'API accepte ces membres; 0.6.1 ne les envoie ni ne les type. Pour vous en servir, appelez la route directement ou agissez dans Agreely :

  • sur les consentements sur papier : qui a consenti (capacity), isMinor, ageBracket et guardianName, et consentedOnBehalf dans la réponse;
  • sur les consentements par téléphone : ageBracket;
  • sur la fin d'une relation : un endedAt antérieur;
  • les routes de la politique de confidentialité et la portée diffusion (référence de l'API).

Ensuite