createEnvelope('/chemin/devis.pdf', [ * ['name' => 'Claire Martin', 'email' => 'claire@client.fr', * 'fields' => [['page' => 1, 'x' => 0.55, 'y' => 0.63, 'w' => 0.36, 'h' => 0.09]]], * ], ['title' => 'Devis 2026-114', 'external_id' => 'DEV-114']); * echo $envelope['signers'][0]['signing_url']; */ class KaeziaSignature { public const VERSION = '1.0'; public function __construct( private readonly string $apiKey, private readonly string $baseUrl = 'https://claude.kaezia.fr', private readonly int $timeout = 30, ) { } /** * @param list}> $signers * @param array $options title, message, sender_name, sender_email, external_id, * metadata, expires_in_days, reminders, send_emails, * authentication ("email_otp" ou "none"), redirect_url, draft */ public function createEnvelope(string $pdfPath, array $signers, array $options = []): array { if (!is_readable($pdfPath)) { throw new KaeziaSignatureException('Fichier illisible : '.$pdfPath); } $fields = ['file' => new CURLFile($pdfPath, 'application/pdf', basename($pdfPath))]; $fields['signers'] = json_encode($signers, \JSON_UNESCAPED_UNICODE); foreach ($options as $k => $v) { $fields[$k] = \is_bool($v) ? ($v ? '1' : '0') : (\is_array($v) ? json_encode($v, \JSON_UNESCAPED_UNICODE) : (string) $v); } return $this->request('POST', '/api/v1/envelopes', $fields); } public function getEnvelope(string $id): array { return $this->request('GET', '/api/v1/envelopes/'.rawurlencode($id)); } /** @param array $filters status, external_id, limit, starting_after */ public function listEnvelopes(array $filters = []): array { return $this->request('GET', '/api/v1/envelopes?'.http_build_query($filters)); } public function send(string $id): array { return $this->request('POST', '/api/v1/envelopes/'.rawurlencode($id).'/send'); } public function cancel(string $id): array { return $this->request('POST', '/api/v1/envelopes/'.rawurlencode($id).'/cancel'); } public function remind(string $id): array { return $this->request('POST', '/api/v1/envelopes/'.rawurlencode($id).'/remind'); } public function account(): array { return $this->request('GET', '/api/v1/account'); } /** @param 'original'|'signed'|'proof' $kind */ public function downloadFile(string $id, string $kind): string { return $this->raw('/api/v1/envelopes/'.rawurlencode($id).'/files/'.$kind); } public function proofJson(string $id): array { return $this->request('GET', '/api/v1/envelopes/'.rawurlencode($id).'/proof.json'); } /** * Vérifie la signature d'un webhook. * $header : contenu de l'en-tête Kaezia-Signature. $body : corps brut de la requête. */ public static function verifyWebhook(string $header, string $body, string $secret, int $toleranceSeconds = 300): bool { if (!preg_match('/t=(\d+),v1=([a-f0-9]{64})/', $header, $m)) { return false; } if (abs(time() - (int) $m[1]) > $toleranceSeconds) { return false; } return hash_equals(hash_hmac('sha256', $m[1].'.'.$body, $secret), $m[2]); } private function request(string $method, string $path, array|string|null $body = null): array { $raw = $this->raw($path, $method, $body); $data = json_decode($raw, true); if (!\is_array($data)) { throw new KaeziaSignatureException('Réponse illisible du serveur.'); } return $data; } private function raw(string $path, string $method = 'GET', array|string|null $body = null): string { $ch = curl_init($this->baseUrl.$path); $headers = ['Authorization: Bearer '.$this->apiKey, 'Accept: application/json', 'User-Agent: KaeziaSignature-PHP/'.self::VERSION]; if (\is_string($body)) { $headers[] = 'Content-Type: application/json'; } curl_setopt_array($ch, [ \CURLOPT_RETURNTRANSFER => true, \CURLOPT_CUSTOMREQUEST => $method, \CURLOPT_TIMEOUT => $this->timeout, \CURLOPT_HTTPHEADER => $headers, \CURLOPT_FOLLOWLOCATION => false, ]); if (null !== $body) { curl_setopt($ch, \CURLOPT_POSTFIELDS, $body); } $response = curl_exec($ch); if (false === $response) { $error = curl_error($ch); curl_close($ch); throw new KaeziaSignatureException('Appel impossible : '.$error); } $status = curl_getinfo($ch, \CURLINFO_HTTP_CODE); curl_close($ch); if ($status >= 400) { $data = json_decode($response, true); $message = $data['error']['message'] ?? ('Erreur HTTP '.$status); throw new KaeziaSignatureException($message, $status, null, $data['error']['code'] ?? 'http_'.$status); } return $response; } } class KaeziaSignatureException extends RuntimeException { public function __construct(string $message, int $code = 0, ?Throwable $previous = null, public readonly string $errorCode = 'error') { parent::__construct($message, $code, $previous); } }