esigner / sdk
Official PHP client for the esigner e-signing API: BankID, MitID and other eID signature requests, signed PDFs and verified webhooks.
Requires
- php: >=8.1
- ext-curl: *
- ext-json: *
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
esigner SDK for PHP
Send PDFs for signing with BankID, MitID and other national eIDs, get the signed PDF back, and verify webhooks — from PHP and WordPress.
API reference · esigner.no · esigner.dk · esigner.se · esigner.fi · esigner.de · esigner.me
- Only
ext-curlandext-json— drops into WordPress or any framework - Safe retries: every POST carries an idempotency key that stays the same across retries
- Cursor pagination as generators, webhook verification in one call
Install
composer require esigner/sdk
PHP 8.1 or newer. Create an API key under Konto → Utvikler on your esigner site. Keys starting
with sk_test_ sign against the test environment: the real flow, with test identities, no payment
and no legally binding signature.
Send a document for signing
use Esigner\EsignerClient; // ['market' => 'se'] for esigner.se — see the table below $esigner = new EsignerClient(getenv('ESIGNER_API_KEY')); $request = $esigner->signatureRequests->createWithFiles('/path/to/contract.pdf', [ // The sender: receives the receipt and the signed PDF 'ownerEmail' => 'post@company.no', 'signers' => [ ['name' => 'Ola Nordmann', 'email' => 'ola@example.no'], ['name' => 'Kari Nordmann', 'email' => 'kari@example.no'], ], // Draw from your plan's quota 'useSubscription' => true, // Optional: Norwegian BankID only 'signingEids' => ['no_bankid'], // Optional: where signers land after the eID step 'returnUrl' => 'https://example.com/contracts/123/signed', ]); echo $request['signers'][0]['sign_url'];
Files can be paths or bytes — ['data' => $pdfBytes, 'filename' => 'contract.pdf'] — and several are
merged in the order given. Or upload first and create separately:
$document = $esigner->documents->upload( ['a.pdf', 'b.docx'], ['ownerEmail' => 'post@company.no'], ); $request = $esigner->signatureRequests->create([ 'documentId' => $document['id'], 'signers' => [/* … */], ]);
Using another market? new EsignerClient($key, ['market' => 'se']) — see the table below.
Follow it up
// status, signers, … $request = $esigner->signatureRequests->retrieve($id); if ($request['status'] === 'completed') { // The PDF's bytes $pdf = $esigner->signatureRequests->downloadSignedDocument($id); file_put_contents('signed.pdf', $pdf); } // Before anyone has signed $esigner->signatureRequests->cancel($id, ['reason' => 'Wrong version']); // Several files → one entry per position $esigner->signatureRequests->listSignedDocuments($id);
Balance and events
// ['balance_ore' => …, 'currency' => …] $esigner->balance->retrieve(); foreach ($esigner->balance->allTransactions() as $tx) { // every page, newest first } foreach ($esigner->events->all(['type' => 'document.completed']) as $event) { // the same events your webhook receives — for backfilling after downtime }
Webhooks (WordPress example)
esigner signs every delivery with Esigner-Signature: t=<unix>,v1=<hmac>. Verify against the raw
body — a re-encoded JSON body will not match.
use Esigner\SignatureVerificationException; use Esigner\Webhook; add_action('rest_api_init', function () { register_rest_route('mysite/v1', '/esigner', [ 'methods' => 'POST', 'permission_callback' => '__return_true', 'callback' => function (WP_REST_Request $req) { try { $event = Webhook::constructEvent( $req->get_body(), // the raw body $req->get_header('esigner_signature'), getenv('ESIGNER_WEBHOOK_SECRET'), // whsec_… ); } catch (SignatureVerificationException $e) { return new WP_REST_Response(null, 400); } switch ($event['type']) { case 'signer.signed': // $event['data']['signersSigned'] of ['signersTotal'] break; case 'document.completed': // downloadSignedDocument($event['data']['orderId']) break; } return new WP_REST_Response(null, 200); }, ]); });
Webhook::EVENT_TYPES lists them all. Deliveries are retried; dedupe on $event['id'].
Errors
Any non-2xx answer throws Esigner\EsignerException with the API's stable code:
use Esigner\EsignerException; try { $esigner->signatureRequests->create([/* … */]); } catch (EsignerException $e) { if ($e->errorCode === 'quota_exceeded') { /* … */ } // also $e->status, $e->detail and the full decoded $e->body }
Network errors, 429 and 5xx are retried twice with backoff.
Options
new EsignerClient($key, [ 'market' => 'no', // which esigner site the key belongs to 'base_url' => null, // or a full API base, e.g. a staging host 'max_retries' => 2, 'timeout' => 60, // seconds per attempt ]);
Every POST takes a last $opts argument: ['idempotency_key' => '…'] to make a retry from your side
return the original result too, and ['timeout' => 120] for one slow call.
Upgrading from 0.1
Esigner\Client and its flat methods (uploadDocument, createSignatureRequest, …) still work and
are deprecated; EsignerClient with its services is the way forward.
esigner around the world
esigner runs one site per country, each signing with that country's own eID. An API key belongs to
the site it was created on — pick it with the market option.
| Site | Signers identify with | API reference | |
|---|---|---|---|
| 🇳🇴 Norway | esigner.no | BankID | esigner.no/dokumentasjon |
| 🇩🇰 Denmark | esigner.dk | MitID | esigner.dk/dokumentation |
| 🇸🇪 Sweden | esigner.se | BankID | esigner.se/dokumentation |
| 🇫🇮 Finland | esigner.fi | Bank credentials (FTN) | esigner.fi/dokumentaatio |
| 🇩🇪 Germany | esigner.de | Personalausweis | esigner.de/dokumentation |
📱 On the go: esigner.me — the esigner app for iPhone and Android. Scan or upload a document, send it for signing and follow it until everyone has signed.
License
MIT