PHP
useyona/einvoice-php is the official SDK for PHP 8.1 and later. It is built on PSR-18, so it works with the HTTP client your project already has, and every array it returns has a documented shape for phpstan and your IDE.
Install
composer require useyona/einvoice-phpThe SDK needs a PSR-18 HTTP client. If your project has none, composer require guzzlehttp/guzzle installs one that the SDK finds automatically.
Quick start
Create one in the Yona app under API keys, with the invoicing preset; it starts with sk_test_. The SDK needs nothing else: no host and no environment setting, because the key chooses the sandbox or the live service.
<?php
require 'vendor/autoload.php';
use Useyona\EInvoice\EInvoice;
$client = new EInvoice(['api_key' => getenv('YONA_API_KEY')]);
// 1. A buyer
$buyer = $client->buyers->create([
'name' => 'Acme Nigeria Ltd',
'taxId' => '12345678-0001',
'email' => 'accounts@acme.ng',
'partyType' => 'company',
'address' => ['line1' => '1 Marina', 'city' => 'Lagos', 'country' => 'NG'],
]);
// 2. A saved item
$item = $client->items->create([
'name' => 'Laptop',
'itemType' => 'goods',
'hsnCode' => '8471.30',
'productCategory' => 'Machinery',
'unitCode' => 'EA',
'unitPriceMinor' => '45000000',
'currency' => 'NGN',
'taxCategory' => 'STANDARD_VAT',
]);
// 3. A draft invoice
$invoice = $client->invoices->create([
'invoiceKind' => 'B2B',
'invoiceDate' => '2026-10-05',
'currency' => 'NGN',
'buyerId' => $buyer['id'],
'lineItems' => [['itemId' => $item['id'], 'quantity' => 2]],
]);
// 4. Finalise it and report it to the tax authority
$client->invoices->finalise($invoice['id']);
$client->submissions->submit($invoice['id']);
$status = $client->submissions->getStatus($invoice['id']);Submitting is asynchronous: poll submissions->getStatus or listen for the invoice.accepted webhook. The states are explained in Invoice lifecycle.
Sandbox and live
An sk_test_ key works in the sandbox and an sk_live_ key in live, on the same host; $client->mode() tells you which. To make a deployment refuse a key of the wrong kind:
new EInvoice(['api_key' => $key, 'assert_mode' => 'live']);Responses and pagination
Methods return the API’s answer as an array with the API’s field names ($invoice['invoiceNumber']). List methods return a page:
$page = $client->buyers->list(['limit' => 50]);$page->data holds the buyers and $page->pagination carries total, page, pageSize, totalPages, hasNext and hasPrevious.
EInvoice::paginate walks every page for you:
$list = fn (array $q) => $client->buyers->list($q);
foreach (EInvoice::paginate($list, ['limit' => 100]) as $buyer) {
echo $buyer['name'], "\n";
}Received invoices and issued history return a Paginated object with the items under $res->data['items']. PDF downloads return a BinaryResponse with data, contentType and fileName.
Errors
A refused request throws an exception chosen by status:
| Status | Exception |
|---|---|
| 400, 422 | ValidationException |
| 401 | AuthenticationException |
| 402 | InsufficientCreditsException |
| 403 | PermissionException |
| 404 | NotFoundException |
| 409 | ConflictException |
| 429 | RateLimitException |
| 5xx | ServerException |
All extend ApiException, which carries status, errorCode, errors (per field), requestId and retryAfter:
use Useyona\EInvoice\Exception\ApiException;
use Useyona\EInvoice\Exception\ValidationException;
try {
$client->invoices->create($params);
} catch (ValidationException $e) {
foreach ($e->errors as $error) {
echo $error->field, ': ', $error->message, "\n";
}
} catch (ApiException $e) {
echo $e->status, ' ', $e->errorCode, ' ', $e->requestId;
}Branch on errorCode, not on the message, and quote requestId when you write to support. Problems before the API answers are TimeoutException, ConnectionException and ConfigException; all SDK exceptions extend EInvoiceException. The codes are listed on each page of the API Reference.
Retries and idempotency
Reads, and writes that carry an Idempotency-Key, are retried automatically on network errors, timeouts, 408, 429 and 5xx, with backoff and respecting Retry-After. Other writes are never retried.
Where the API accepts an Idempotency-Key (creating an invoice, submitting, sending, downloading and so on) the SDK generates one per call, so a retry is never applied or charged twice. Pass your own to make a retry safe across restarts:
use Useyona\EInvoice\RequestOptions;
$options = new RequestOptions(idempotencyKey: "order-{$orderId}");
$client->invoices->create($params, $options);Tune the defaults with timeout and retry in the configuration, or per call with RequestOptions (headers, maxRetries).
Webhooks
In plain PHP:
use Useyona\EInvoice\Exception\WebhookException;
use Useyona\EInvoice\Webhooks;
$secret = getenv('YONA_WEBHOOK_SECRET');
try {
$raw = file_get_contents('php://input');
$event = Webhooks::verifyWebhook($raw, $_SERVER, $secret);
} catch (WebhookException $e) {
http_response_code(400);
exit;
}
if ($event['type'] === 'invoice.accepted') {
// …
}With a PSR-7 request (Laravel, Symfony, Slim and the like), pass the request itself as the headers:
$event = Webhooks::verifyWebhook($request->getBody(), $request, $secret);verifyWebhook checks the Yona-Signature header over the raw body, accepts either signature while a rotated secret overlaps, and returns the event. Always pass the raw body, and deduplicate on $event['id']. Webhooks::signWebhookPayload signs a payload the way Yona does, so you can test your handler locally.
Endpoints are created and their secrets rotated in the Yona app; the SDK can list and test endpoints and read and redeliver deliveries and events. See Webhooks for the events.
Configuration
new EInvoice([...]) accepts:
| Option | Purpose |
|---|---|
assert_mode | 'sandbox' or 'live': refuse a key of the other kind |
timeout | timeout in seconds for the HTTP client the SDK creates (default 30) |
retry | max_retries (2), base_delay (0.5 s), max_delay (8 s), max_retry_after (60 s) |
headers | headers sent on every request |
http_client, request_factory, stream_factory | your own PSR-18 client and PSR-17 factories (proxy, TLS) |
base_url | another gateway, for local development only; the key decides sandbox or live |
What it covers
Every module mirrors a tag of the API Reference, and the method names are the reference’s operation names in the casing of the language (issueCreditNote, getStatus). Every method takes an optional trailing ?RequestOptions $options; request and query arrays have shapes named after the operation (CreateInvoiceBody, ListInvoicesQuery) in Useyona\EInvoice\Generated\Types. Each method needs the capability shown on the key; the full_integration preset has them all.
| Module | What it does | Reference |
|---|---|---|
$client->invoices, $client->submissions, $client->output, $client->shareLinks | Create, edit and finalise drafts; submit, retry and track them; credit and debit notes; PDFs, sending and share links | Invoices |
$client->buyers | The businesses and people you invoice, with tax-number checks and reachability | Buyers |
$client->items | What you sell, with codes, units, prices and tax categories | Items |
$client->reference | HS codes, reference lists, tax-ID lookup and invoice validation | Reference data |
$client->sellers | Your organisation as the seller, read-only | Sellers |
$client->inboundInvoices, $client->issuedHistory | Invoices other businesses sent you, and your issued history retrieved from the authority | Received invoices |
$client->organization, $client->invoiceSettings, $client->taxConnection | Your organisation and its readiness, invoice settings, the tax connection | Organizations, Tax connection |
$client->billing | Credits and usage, payments, statements and your subscription, in accounts, payments, sandbox, statements, subscriptions, transactions | Billing |
$client->webhooks | List and test endpoints; read and redeliver deliveries and events, in endpoints, deliveries, events, eventTypes | Webhooks |
Account management (users, roles, API keys, purchases, webhook endpoint settings) is done in the Yona app, not through the API key.
The source is on GitHub at Elyonar/einvoice-php , with runnable examples under examples/.
Next steps
- Invoice lifecycle: the states a submission climbs through, and what each one means
- Webhooks: the events, and how deliveries are retried
- Going live: the checklist before you deploy a live key
- API Reference: every field, status code and error code