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.tscomplet). - Refus par défaut : une panne refuse.
- Node 18+ (
fetchglobal), avec un repliundiciparesseux 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 envoieassurance: "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), etotherConsents: "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 422other_consents_running, uneAgreelyOtherConsentsRunningErrordontotherConsentRefsliste ces consentements, rien d'écrit : demandez à la personne lesquels, puis réessayez. Ceux retirés au même instant reviennent dansalsoWithdrawn, ceux encore en vigueur dansstillInForce. - Sa propre portée, jamais accordée par défaut, et un plafond quotidien (429
AgreelyDailyCapError,withdrawal_daily_cap, sansRetry-After); le propriétaire du compte est avisé. - Refus : 404
unknown_consent, 409consent_lapsed, 422not_revocable,requested_at_in_future,requested_at_before_grant,not_running_for_purpose(une entrée d'alsoWithdrawqui 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.
legalBasisetbasisNoteont quitté l'API : une valeur envoyée est ignorée, et la réponse ne porte pluslegalBasisnihasBasisNote. 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.
createdvaut 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) ouidentity_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", sansrules. Purger sur l'instantané conservé est une exception explicite, bornée et auditée (onOutage: "use-snapshot",maxSnapshotAge, unonDegradeobligatoire). 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é, puissyncHolds({ 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 (409relationship_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,ageBracketetguardianName, etconsentedOnBehalfdans la réponse; - sur les consentements par téléphone :
ageBracket; - sur la fin d'une relation : un
endedAtantérieur; - les routes de la politique de confidentialité et la portée
diffusion(référence de l'API).
Ensuite
- Composer avec les pannes parcourt en profondeur la décision refus par défaut contre ouverture en cas de panne.
- SDK PHP et CLI pour la même surface ailleurs.