SDK PHP
agreely/sdk est le portage PHP de la référence TypeScript.
Les deux SDK vérifient les mêmes vecteurs de référence partagés contre l'API en
direct, pour qu'aucun ne s'écarte du contrat, et 0.6.2 expose la
même surface sous les mêmes noms de méthodes. Un seul appel protège un usage de
données par une vérification en direct et faisant autorité; il n'y a aucune copie
locale et aucune mémoire-cache.
- DX à un seul appel, objets de résultat typés et erreurs typées, PSR-4 / PSR-12, phpstan au maximum. Publié sous licence MIT.
- Refus par défaut : une panne refuse.
- PHP 8.2+ (aussi 8.3 / 8.4 / 8.5),
ext-curl+ext-json. Apportez votre propre client HTTP ou utilisez leCurlHttpClientfourni.
Cette page documente la version 0.6.2, celle publiée sur Packagist. Les membres de l'API que 0.6.2 n'envoie pas sont énumérés dans Ce que 0.6.2 ne couvre pas.
Installer
composer require agreely/sdk
Construire
use Agreely\Sdk\Agreely;
$agreely = new Agreely(['apiKey' => getenv('AGREELY_API_KEY')]); // baseUrl optional
Toutes les options du constructeur (un tableau associatif; un champ requis manquant
lève AgreelyConfigError) :
| 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 |
maxDegradeWindow |
"24h" |
borne de la fenêtre de panne, relevable (par exemple "48h") |
maxRetries |
0 |
tentatives supplémentaires sur un 429 rate_limited, lectures et vérifications seulement |
respectRetryAfter |
true |
honore le Retry-After du serveur |
httpClient |
CurlHttpClient |
tout Agreely\Sdk\Http\HttpClient |
Les ressources sont des méthodes d'accès : $agreely->consentDocuments(),
$agreely->consentRequests(), $agreely->manualConsents(),
$agreely->verbalConsents(), $agreely->withdrawals(),
$agreely->relationships(), $agreely->customers(), $agreely->catalog(),
$agreely->retention(), $agreely->inventory().
Écarts assumés avec le SDK TS
Le SDK PHP n'a aucune option onBreakGlass et aucune ressource breakGlass (l'ouverture en cas de panne est à deux barrières; la troisième, le bris de glace, est propre au TS). Il n'a aucune option fetch (il utilise httpClient). La pagination est iterate() (un Generator), pas listAll. Les entrées sont des tableaux associatifs vérifiés à l'exécution. Il n'y a pas d'AbortSignal. Les deux SDK sont sous licence MIT.
check() renvoie un bool
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 ($agreely->check('cust_8812', 'Phone number', 'Billing')) {
// ...you may use the phone number for billing
}
checkDetailed() renvoie la décision
$d = $agreely->checkDetailed('cust_8812', 'Phone number', 'Billing');
// $d->decision "allow" | "deny"
// $d->status one of the nine values below
// $d->consentRef "0x…" (null for "none" and "necessity")
// $d->checkedAt "2026-…Z"
// $d->assurance "citizen_signed" | "company_attested" | "company_documented" | null
// $d->tier "full" | "manual" | "verbal" | null (present exactly when assurance is)
// $d->basis the declared non-consent ground (ONLY on "necessity", else null)
// $d->validUntil the backing consent's end (null without a record)
// $d->revokedAt when it was withdrawn (ONLY on "revoked", else null)
// $d->degraded / $d->mode set only on a degraded allow
Un refus est un 200 normal; checkDetailed le renvoie, il ne lève pas
d'exception. validUntil est une borne supérieure, jamais un bail de cache : un
retrait peut mettre fin au consentement avant cette date, et seul un hôte qui
vérifie de nouveau le voit.
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 ($d->isNecessity()) 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.
Une autorisation fondée sur un consentement nomme sa preuve deux fois, assurance
et tier, et votre code 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é |
use Agreely\Sdk\Types\ConsentTier;
if ($d->isAllow() && ($d->isNecessity() || ConsentTier::atLeast($d->tier, ConsentTier::MANUAL))) {
// this use requires at least a signed paper
}
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, 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).
use Agreely\Sdk\Types\BatchCheckItem;
$decisions = $agreely->checkBatch([
new BatchCheckItem('cust_8812', 'Phone number', 'Billing'),
['customerRef' => 'cust_8813', 'category' => 'Email address', 'purpose' => 'Newsletter'],
]);
// list<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.
$grid = $agreely->checkFields(
['cust_8812', 'cust_8813'],
[ ['category' => 'Phone number', 'purpose' => 'Billing'],
['category' => 'Email address', 'purpose' => 'Newsletter'] ],
);
// CheckFieldsResult: $grid->decisions; $grid->isAllowed($customerRef, $category, $purpose): bool
if ($grid->isAllowed('cust_8812', 'Phone number', 'Billing')) {
// ...you may display this field for this customer
}
Un lot compte au plus 500 cellules (Agreely::BATCH_CAP), refusé côté client
avant l'appel; checkFields 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. L'objet renvoyé reprend aussi la
baseUrl configurée, côté client.
$me = $agreely->identity();
// Identity: $me->scopes (list<string>), $me->baseUrl (string|null),
// $me->company (IdentityCompany|null: name, sector, statute, publicPolicyUrl)
if ($me->hasScope('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
scopes contient tout ce que le serveur envoie, y compris une portée que 0.6.2 ne
nomme pas (comme diffusion). L'API renvoie aussi company.slug, que 0.6.2 ne lit
pas.
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 (portée check ou issue) :
$docs = $agreely->consentDocuments()->list();
$doc = $agreely->consentDocuments()->get('conditions-marketing');
$doc->documentVersionId; // the version in force
$doc->disclosure->withdrawal->text('fr'); // the published text, verbatim
// The information document of one version, to print and hand to the person
// (scope check, issue, attest or attest_verbal):
$info = $agreely->consentDocuments()->getInformationPdf($doc->documentVersionId, ['locale' => 'fr']);
file_put_contents('/tmp/' . ($info->filename ?? 'information.pdf'), $info->pdf);
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. L'obtenir ne
consigne rien : ce n'est pas une preuve que quiconque a été informé. Un rendu
dispose d'au moins 15 s ('timeout' => ms le modifie).
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.
$r = $agreely->consentRequests()->create([
'customerId' => 'cust_8812',
'recipientEmail' => '[email protected]',
'documentCode' => 'conditions-marketing', // or: 'consentDocumentId' => '<documentVersionId>'
'validUntil' => '2031-01-01',
]);
// IssuedRequest: $r->requestId "0x…64hex"; $r->status "pending"; $r->deepLink; $r->emailDelivered; $r->items
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). Passez
votre propre clé pour qu'une nouvelle tentative rejoue le 201 d'origine :
$agreely->consentRequests()->create($input, ['idempotencyKey' => 'order-4471']);
Avant d'émettre, hasPending('cust_8812', 'conditions-marketing') vous dit si une
demande est déjà en attente (le code de document est facultatif). Elle signale une
demande en attente; elle ne dit rien du consentement.
$page = $agreely->consentRequests()->list([
'customerId' => 'cust_8812', // optional
'status' => 'pending', // optional
'limit' => 50, // default 50, max 100
'cursor' => $cursor, // a prior nextCursor
]);
// $page->items (list<ConsentRequestRecord>); $page->nextCursor (null when exhausted)
$one = $agreely->consentRequests()->get('0x…'); // the protocol requestId, NOT a uuid
// ConsentRequestRecord: requestId, status, validUntil, expiresAt, createdAt,
// settledAt (nullable), customerId, documentCode, items
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.
Parcourir toutes les pages, et attendre le règlement
En PHP, la pagination complète est iterate() (un Generator), pas listAll.
collect() réunit tout; waitForSettlement() interroge get jusqu'à ce que la
demande soit réglée (asks_declined compte comme réglée).
foreach ($agreely->consentRequests()->iterate(['status' => 'pending']) as $rec) {
// one ConsentRequestRecord at a time
}
$all = $agreely->consentRequests()->collect(['status' => 'pending']);
// Poll until settled. Defaults: intervalMs 2000, timeoutMs 120000.
$settled = $agreely->consentRequests()->waitForSettlement('0x…', [
'intervalMs' => 2000,
'timeoutMs' => 120000,
]);
waitForSettlement lève AgreelyTimeoutError si le budget client s'écoule (ce
n'est pas une panne serveur). Il n'y a pas d'AbortSignal en PHP.
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.
$c = $agreely->consentRequests()->cancel('0x…');
// CancelledRequest: $c->requestId; $c->status "revoked_before_action"; $c->cancelled true
// Already terminal -> $c->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.
$m = $agreely->manualConsents()->record([
'customerId' => 'cust_8812',
'documentVersionId' => $doc->documentVersionId,
'effectiveDate' => '2026-06-01',
'validUntil' => '2031-01-01',
'items' => [['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: $m->consentId; $m->merkleRoot; $m->consentRefs; $m->assurance "company_attested";
// $m->anchored (false at record time); $m->acknowledged; $m->asksDeclined
L'entrée est fermée : un membre autre que ceux ci-dessus est refusé avant
l'appel (AgreelyConfigError). record n'est jamais ré-essayé
automatiquement et porte un Idempotency-Key : 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.
Refus, par leur ->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, predates_withdrawal,
renewal_ends_before_current. Rien n'est consigné.
// A link for the person to claim the consent in their own space.
$link = $agreely->manualConsents()->createClaimLink(['customerId' => 'cust_8812']);
// $link->claimUrl; $link->token; $link->expiresAt
// Revoke or erase by consentRef.
$w = $agreely->manualConsents()->revoke('0x…', ['reason' => 'customer request']);
// $w->consentRef; $w->revoked; $w->alreadyRevoked; $w->gate "denied" | "superseded" | "unchanged"
$e = $agreely->manualConsents()->erase('0x…', ['reason' => 'erasure request']);
// $e->consentRef; $e->erased; $e->alreadyErased; $e->gate
// Keep in force a consent the person signed online (see the refusal below).
$k = $agreely->manualConsents()->revoke('0x…', ['otherConsents' => 'keep']);
// $k->gate "superseded"; $k->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 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 ->stillInForce vaut 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, et refusent toute autre valeur avant l'appel.
Imprimer une feuille de consentement
$sheet = $agreely->manualConsents()->createConsentSheet('cust_8812', [
'documentVersionId' => $doc->documentVersionId,
'locale' => 'fr',
]);
$printer->print($sheet->signatureSheet->bytes()); // the BLANK sheet, reference printed on it
$sheet->printedReference; // e.g. "ABCD-EFGH"
$mailer->send($person, $sheet->claim->claimUrl); // ANOTHER channel, never with the sheet
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; la même Idempotency-Key répond 409
already_minted et ne produit rien.
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.
$call = $agreely->verbalConsents()->record([
'customerId' => 'cust_8812',
'documentVersionId' => $doc->documentVersionId,
'answers' => [ // every ask put on the call, "yes" or "no"
['category' => 'Email address', 'purpose' => 'Newsletter', 'answer' => 'yes'],
['category' => 'Email address', 'purpose' => 'Surveys', 'answer' => 'no'],
],
'obtainedAt' => new DateTimeImmutable('now'), // 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,
]);
// The signed paper came back (scope attest): each "yes" moves into a new paper consent.
$rise = $agreely->verbalConsents()->confirmWithPaper($call->consentId, [
'signedAt' => new DateTimeImmutable('now'),
'answers' => [['category' => 'Email address', 'purpose' => 'Newsletter', 'answer' => 'yes']],
'evidence' => ['pdfSha256' => Agreely::hashPdfFile($path)],
]);
$history = $agreely->verbalConsents()->get($call->consentId);
$history->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.
$w = $agreely->withdrawals()->record('cust_8812', $consentRef, [
'channel' => 'phone', // phone | email | mail | in_person | other
'operator' => 'agent-042', // YOUR opaque staff id, never a name or an email
'requestedAt' => '2026-10-08T09:15:00-04:00', // optional: when they asked
'reason' => 'Asked on the phone', // optional, at most 1000 characters
]);
$w->gate; // "denied" | "superseded" | "unchanged": what /v1/check does now
$w->alsoWithdrawn; // the other consents of the purpose withdrawn at the same instant
$w->stillInForce; // the 0x refs of the other consents of the purpose still in force
$w->recordedOnBehalf; // always true
$w->assurance; // "company_documented", as the server sends it
- La réponse dit qui parle. 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,Withdrawals::SCOPE_MAX;[]ne déclare rien), et'otherConsents' => 'keep'retire ce consentement seul. Les deux ensemble sont refusés avant l'appel (AgreelyConfigError). Sans déclaration, l'appel lève uneAgreelyOtherConsentsRunningError(422other_consents_running) dont->otherConsentRefsliste ces consentements, rien d'écrit : demandez à la personne lesquels, puis réessayez avec une seule déclaration.
use Agreely\Sdk\Errors\AgreelyOtherConsentsRunningError;
try {
$agreely->withdrawals()->record('cust_8812', $consentRef, ['channel' => 'phone', 'operator' => 'agent-042']);
} catch (AgreelyOtherConsentsRunningError $e) {
// Ask the person which of $e->otherConsentRefs the withdrawal covers, then retry once:
$agreely->withdrawals()->record('cust_8812', $consentRef, [
'channel' => 'phone',
'operator' => 'agent-042',
'alsoWithdraw' => ['0x…'], // or 'otherConsents' => 'withdraw' (all of them), or 'keep' (this one only)
]);
}
- Sa propre portée, jamais accordée par défaut, et un plafond quotidien (429
AgreelyDailyCapError,withdrawal_daily_cap, sansRetry-After). - 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.
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 AgreelyConfigError avant l'appel). 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. 0.6.2 consigne la fin à l'instant de
l'appel; l'API accepte aussi un endedAt antérieur, que 0.6.2 n'envoie pas.
$r = $agreely->relationships()->end(['customerRef' => 'cust_8812', 'reason' => 'Compte fermé, fins accomplies']);
// RelationshipEnded: $r->customerRef; $r->status "ended"; $r->endedAt; $r->endedBy "company" | "citizen_request"
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.
$u = $agreely->relationships()->revert(['customerRef' => 'cust_8812', 'reason' => 'Fin enregistrée par erreur']);
// RelationshipReverted: $u->customerRef; $u->status "active"; $u->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).
$record = $agreely->customers()->upsert('cust_8812', [
'displayName' => 'Marie Tremblay',
'email' => null, // null or "" CLEARS a field
'noticeLocale' => 'fr',
]); // a member you leave out is left as it stands
$record->created; // true when this call created the record (201)
$record->hasDisplayName; // metadata only: never the name or the email
$agreely->customers()->get('cust_8812'); // the same metadata, with the relationship's status
- 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 les porte plus. 0.6.2 les accepte encore, et lithasBasisNotecommefalseetlegalBasiscommenull; ne vous y fiez pas. La finalité appartient au traitement, documentée par cellule du catalogue. - 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
$catalog = $agreely->catalog()->list();
// list<CatalogEntry>: id, category, purpose, description (nullable), legalBasis, sensitive
$form = $agreely->catalog()->forDocument('conditions-marketing'); // only the cells that document groups
$cells = $agreely->catalog()->listCells(); // each cell with its retentionRuleKey
$cells->gaps(); // cells with NO rule: abstain, never guess
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".
use Agreely\Sdk\Types\PurgeMethod;
$forRun = $agreely->retention()->rulesForPurge(['snapshot' => $stored]); // $stored may be null
if (!$forRun->mayPurge()) {
return; // Agreely unreachable: do NOT purge this run
}
$sync = $agreely->retention()->syncHolds(['changedSince' => $storedCursor]); // any error throws: purge nothing
foreach ($forRun->rules() as $rule) {
// your purge, skipping what a hold covers, then:
$agreely->retention()->declarePurge($rule->key, [
'ranAt' => new DateTimeImmutable('now'),
'recordsAffected' => 1_240, // at least 1; a pass that found nothing is declareSweep
'method' => PurgeMethod::DESTROYED, // or ::ANONYMIZED + anonymizationProcessKey
'coveredFrom' => '2024-01-01',
'coveredUntil' => '2024-06-30',
'hostSystem' => 'billing', // a STABLE name, never a pod name
'hostCategory' => 'invoices',
], ['idempotencyKey' => "nightly-{$runId}:{$rule->key}"]);
}
- Une tâche de purge échoue fermée en ne purgeant pas.
rules()lève une exception sur une abstention plutôt que de rendre une liste vide. Purger sur les règles conservées est une exception explicite, bornée et auditée ('onOutage' => 'use-snapshot',maxSnapshotAge, unonDegradeobligatoire). Tout ce qui n'est pas une panne lève une exception. - 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)
$declared = $agreely->inventory()->replaceCategories([
'hostSystem' => 'crm',
'categories' => [[
'key' => 'beneficiaires',
'label' => 'Bénéficiaires',
'fields' => [['key' => 'nom', 'label' => 'Nom complet']],
]],
]);
$declared->withdrawn; // how many sets this call withdrew
$sets = $agreely->inventory()->listCategories(['hostSystem' => 'crm']);
$resolved = $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.
$h1 = Agreely::hashPdf($bytes); // $bytes is a string (raw PDF bytes)
$h2 = Agreely::hashPdfFile('/path/to/consent.pdf');
Délais, nouvelles tentatives et limites
Le délai par défaut est un budget total de 800 ms, prévu pour un appelant dans la même région que l'API. Sur Internet, un délai dépassé est un refus injustifié : donnez de la marge à un appelant exposé à Internet.
new Agreely(['apiKey' => $key, '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, et aucun autre 429. Aucune écriture n'est jamais ré-essayée
automatiquement. Les limites sont celles du
SDK TypeScript :
500 cellules par lot et 120 requêtes par minute par entreprise, toutes clés
comprises.
Erreurs typées
Chaque échec est une sous-classe d'Agreely\Sdk\Errors\AgreelyError (avec
->code, ->status, ->field, ->reason). Un refus n'est jamais une erreur.
| Erreur | Quand |
|---|---|
AgreelyAuthError |
401 / 403 |
AgreelyValidationError |
400 / 422 (->field) |
AgreelyOtherConsentsRunningError |
422 other_consents_running sur un retrait (une sous-classe de l'erreur de validation) : ->otherConsentRefs liste les consentements en vigueur pour la finalité. Jamais ré-essayée automatiquement |
AgreelyNotFoundError |
404 |
AgreelyConflictError |
409 : code retry (->isRetryable(), réessayez avec le même Idempotency-Key), code conflict (->isStateConflict(), ->reason dit lequel), 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; jamais ré-essayée automatiquement |
AgreelyUnavailableError |
503 et tout autre 5xx, réseau, délai (la seule erreur soumise à la politique de dégradation) |
AgreelyTimeoutError |
budget de sondage client écoulé dans waitForSettlement (code: "timeout", ce n'est pas une panne serveur) |
AgreelyConfigError |
mauvaise configuration client, ou entrée refusée avant l'appel |
Aiguillez sur ->reason, jamais sur le message. ErrorReason et ErrorCode
nomment les valeurs connues, et une raison ajoutée plus tard se lit comme une
simple chaîne.
use Agreely\Sdk\Errors\AgreelyConflictError;
use Agreely\Sdk\Errors\ErrorReason;
try {
$agreely->manualConsents()->revoke($consentRef);
} catch (AgreelyConflictError $e) {
if ($e->hasReason(ErrorReason::CITIZEN_CONSENT_AT_GATE)) {
$agreely->withdrawals()->record($customerRef, $consentRef, ['channel' => 'phone', 'operator' => 'agent-12']);
}
}
AgreelyAbortError n'existe pas en PHP (il n'y a pas d'AbortSignal).
Refus par défaut, et l'exception à deux barrières
Lorsqu'Agreely est injoignable, check() refuse et checkDetailed() lève
AgreelyUnavailableError. Une autorisation dégradée revient avec $d->degraded à
vrai et $d->mode renseigné, sans assurance : c'est une dérogation
délibérée et auditée au refus par défaut. Inscrire une catégorie en ouverture en
cas de panne passe par deux barrières :
$agreely = new Agreely([
'apiKey' => $key,
'degradeOnOutage' => [
'mode' => 'fail-open', // gate 1: the explicit word
'categories' => ['Browsing/usage'], // ONLY these may degrade
'maxOutageWindow' => '5m',
'onDegrade' => fn ($ctx) => $audit->log($ctx), // MANDATORY: without it the constructor throws
],
]);
// gate 2: the call must ALSO opt in. Without the config, this still denies.
$agreely->check('cust_8812', 'Browsing/usage', 'Analytics', ['onOutage' => 'allow']);
maxOutageWindow est borné par maxDegradeWindow, 24 h par défaut, que
l'option du client peut relever; au-delà, AgreelyConfigError est levée. Un ['onOutage' => 'allow'] par appel qui n'est pas adossé à une entrée
categories correspondante n'a aucun effet (la vérification refuse toujours; un
avertissement unique est consigné via error_log, réduit au silence avec
AGREELY_SILENCE_WARNINGS).
Le bris de glace est absent en PHP, par conception
La troisième barrière du SDK TS, un levier de bris de glace de l'exploitant, en mémoire et à expiration automatique, n'est volontairement pas en PHP. Les requêtes PHP vivent le temps d'une requête, sans état durable en mémoire : un bris de glace logé dans un seul objet ne survivrait pas d'une requête à l'autre et donnerait la fausse impression d'une dérogation valable pour tout le parc. PHP livre ce qui se porte proprement : le refus par défaut, l'ouverture en cas de panne à deux barrières, le plafond maxDegradeWindow et l'avertissement d'adhésion sans effet. Une version future pourra adosser le bris de glace à un stockage partagé.
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, de
préférence en texte JSON (ReceiptVerifier::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. Les accès réseau s'injectent comme callables
httpGet / httpPost (pas fetch).
$v = Agreely::verifyReceipt($bundleJson, ['rpcUrl' => 'https://base-rpc.publicnode.com']);
$v->overall; // "verified" | "partial" | "incomplete" | "failed" | "unavailable" | "malformed" | "withdrawn"
$v->withdrawal; // "active" | "withdrawn" | "not_checked"
$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), et explique pourquoi :
Ce que 0.6.2 ne couvre pas
L'API accepte ces membres; 0.6.2 ne les envoie pas. Pour vous en servir, appelez la route directement ou agissez dans Agreely :
- sur les consentements sur papier : qui a consenti (
capacity),isMinor,ageBracketetguardianName(l'entrée fermée les refuse avant l'appel); - 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 et le SDK TypeScript (qui ajoute le bris de glace).