SDK PHP
agreely/sdk est le portage PHP de la référence TypeScript.
Les deux SDK valident les mêmes vecteurs de référence partagés par rapport à
l'API en direct, de sorte qu'aucun ne dérive du contrat. 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.
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 |
degradeOnOutage |
(désactivé) | adhésion explicite à l'ouverture en cas de panne |
maxDegradeWindow |
"24h" |
plafond ferme de la fenêtre de panne |
maxRetries |
0 |
tentatives supplémentaires sur un 429 seulement, appels idempotents |
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->consentRequests(),
$agreely->manualConsents(), $agreely->relationships(), $agreely->catalog().
Divergences honnêtes avec le SDK TS
Le SDK PHP n'a pas d'option onBreakGlass ni de 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 pas d'option fetch (il utilise httpClient). La pagination se fait par iterate() (un Generator), et non par listAll. Les entrées sont des tableaux associatifs vérifiés à l'exécution. Il n'y a pas d'AbortSignal. Le SDK TS est publié UNLICENSED; ce SDK PHP est 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é.
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 "active" | "none" | "revoked" | "expired" | "erased"
// $d->consentRef "0x…" (null when status is "none")
// $d->checkedAt "2026-…Z"
// $d->assurance "citizen_signed" | "company_attested" | null
// $d->degraded / $d->mode present only on a degraded allow
Un refus est un 200 normal; checkDetailed le renvoie, il ne lève pas
d'exception.
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>: $d->customerRef; $d->category; $d->purpose; $d->decision;
// $d->status; $d->consentRef (null for "none"); $d->assurance (null for "none"); $d->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')) {
// ...affichez ce champ pour ce client
}
Cas d'usage : protéger l'affichage d'une liste de clients
Afficher un tableau de N clients x M champs est N x M usages de renseignements personnels : sous la Loi 25, chaque champ affiché est un usage encadré par une vérification (limitation de l'usage, art. 12; responsabilité, art. 3.1). 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).
La réponse (côté fil) est à divulgation minimale : uniquement les portées; l'objet
renvoyé rappelle aussi la baseUrl configurée, côté client.
$me = $agreely->identity();
// Identity: $me->scopes (list<string>), $me->baseUrl (string|null)
if (in_array('issue', $me->scopes, true)) {
// ...this key may issue consent requests
}
Demandes de consentement
create envoie un courriel au destinataire; les items sont des identifiants de
catalogue ou des tableaux ['category' => …, 'purpose' => …].
$r = $agreely->consentRequests()->create([
'customerId' => 'cust_8812',
'recipientEmail' => '[email protected]',
'items' => ['<catalogEntryId>', ['category' => 'Email address', 'purpose' => 'Newsletter']],
'validUntil' => '2031-01-01',
]);
// IssuedRequest: $r->requestId "0x…64hex"; $r->status "pending"; $r->deepLink; $r->emailDelivered; $r->items
$page = $agreely->consentRequests()->list(['status' => 'pending', 'cursor' => $cursor]);
// $page->items (list<ConsentRequestRecord>); $page->nextCursor (null when exhausted)
$one = $agreely->consentRequests()->get('0x…'); // the protocol requestId, NOT a uuid
// ConsentRequestRecord: requestId, status (pending|approved|refused|expired|revoked_before_action),
// validUntil, expiresAt, createdAt, settledAt (nullable), items
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']);
Parcourir toutes les pages, et attendre le règlement
En PHP, la pagination complète se fait par iterate() (un Generator), pas
listAll. collect() réunit tout; waitForSettlement() interroge get.
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 manuels
Pour un consentement recueilli hors ligne (un PDF signé à la main). Vous enregistrez la preuve : le hachage SHA-256 du PDF, et facultativement le PDF lui-même.
$m = $agreely->manualConsents()->record([
'customerId' => 'cust_8812',
'documentVersionId' => 'doc_v3',
'effectiveDate' => '2026-01-01',
'validUntil' => '2031-01-01',
'items' => [['category' => 'Phone number', 'purpose' => 'Billing']],
'evidence' => [
'pdfSha256' => Agreely::hashPdf($bytes), // "0x"+sha256, required
'pdf' => $base64Pdf, // optional opt-in upload
],
]);
// Note: manual consents do NOT honor an Idempotency-Key (only consentRequests()->create() does).
// ManualConsentResult: $m->consentId; $m->merkleRoot; $m->consentRefs (one per cell);
// $m->assurance "company_attested"; $m->anchored (false at record time)
// A one-time link for the citizen to claim/confirm. NOTE the name createClaimLink.
$link = $agreely->manualConsents()->createClaimLink(['customerId' => 'cust_8812', 'reference' => 'order-4471']);
// $link->claimUrl; $link->token; $link->expiresAt
// Revoke or erase by consentRef.
$agreely->manualConsents()->revoke('0x…', ['reason' => 'customer request']);
// { consentRef, revoked, alreadyRevoked }
$agreely->manualConsents()->erase('0x…', ['reason' => 'erasure request']);
// { consentRef, erased, alreadyErased }
Fin d'une relation client
Attestez, depuis votre propre code, qu'une relation client est terminée (Loi 25
art. 23). 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 AgreelyException). 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.
$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"
Catalogue
$catalog = $agreely->catalog()->list();
// list<CatalogEntry>: id, category, purpose, description (nullable)
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');
Options de limite de débit
maxRetries (défaut 0) ajoute des tentatives supplémentaires sur un 429
seulement, et seulement pour les appels idempotents (GET et check); un
appel mutant ne ré-essaie jamais un 429. respectRetryAfter (défaut true)
honore l'en-tête Retry-After du serveur.
Erreurs typées
Chaque échec est une sous-classe d'Agreely\Sdk\Errors\AgreelyError (avec
->code, ->status, ->field). Un refus n'est jamais une erreur.
| Erreur | Quand |
|---|---|
AgreelyAuthError |
401 / 403 |
AgreelyValidationError |
400 / 422 (->field) |
AgreelyNotFoundError |
404 |
AgreelyRateLimitError |
429 (->retryAfter en secondes) |
AgreelyUnavailableError |
503 / réseau / délai (ré-essayable; 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 (levée à l'initialisation) |
use Agreely\Sdk\Errors\AgreelyRateLimitError;
try {
$agreely->check($id, $cat, $pur);
} catch (AgreelyRateLimitError $e) {
sleep($e->retryAfter ?? 1);
}
Le délai d'expiration par défaut est un budget total de 800 ms. 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 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. Inscrire une catégorie en ouverture en cas de panne utilise 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']);
maxDegradeWindow est plafonné à 24 h; au-delà du plafond, lève
AgreelyConfigError. Une ['onOutage' => 'allow'] par appel non adossée à 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 omis en PHP, à dessein
La troisième barrière du SDK TS, un levier de bris de glace d'exploitant, en cours de processus et à expiration automatique, n'est intentionnellement pas en PHP. Les requêtes PHP sont à portée de requête, sans état en cours de processus de longue durée, de sorte qu'un bris de glace vivant dans un seul objet ne survivrait pas d'une requête à l'autre et donnerait un faux sentiment de dérogation à l'échelle du parc. PHP livre les parties qui se portent 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 magasin partagé.
Vérifier un ConsentReceipt
Agreely::verifyReceipt est statique, ne demande aucune clé d'API, et
résout toujours : un reçu falsifié rend un verdict, jamais une exception. Elle
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 DID ne ressemble
jamais à une contrefaçon. Les seams réseau sont injectés par des appelables
httpGet / httpPost (et non par fetch).
$v = Agreely::verifyReceipt($receipt, ['rpcUrl' => getenv('BASE_SEPOLIA_RPC')]);
$v->overall; // "verified" | "partial" | "failed" | "unavailable"
La page dédiée détaille les quatre contrôles, le tableau ce qui est prouvé
contre ce qui est présumé, et pourquoi un reçu citoyen ne peut jamais être
verified hors ligne :
Ensuite
- Composer avec les pannes et le SDK TypeScript (qui ajoute le bris de glace).