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

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