FR EN

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 le CurlHttpClient fourni.

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 une AgreelyOtherConsentsRunningError (422 other_consents_running) dont ->otherConsentRefs liste 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, sans Retry-After).
  • 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.

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. legalBasis et basisNote ont 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 lit hasBasisNote comme false et legalBasis comme null; 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) ou identity_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, un onDegrade obligatoire). 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 (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)

$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, ageBracket et guardianName (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 endedAt antérieur;
  • les routes de la politique de confidentialité et la portée diffusion (référence de l'API).

Ensuite