Official PHP client for version 2 of the Postbode.nu API: send letters, postcards and fulfillment orders, and follow them until they land on the doormat.
Full API reference: https://postbode.app/docs/api
- PHP 8.2 or higher
- A PSR-18 HTTP client and a PSR-17 factory
composer require postbode/postbode-apiIf your project does not have an HTTP client yet, add one — Guzzle is the usual choice and needs no further configuration:
composer require guzzlehttp/guzzleAlready using Symfony's HttpClient, or a client of your own? Pass it in and skip the extra dependency.
- Set up an account at https://postbode.nu
- Create an API token at https://postbode.app/gebruiker/api-tokens
use Postbode\PostbodeApiClient;
$postbode = new PostbodeApiClient('your-api-key');
foreach ($postbode->mailboxes->list() as $mailbox) {
printf("%s (%s): € %.2f available\n", $mailbox->name, $mailbox->customerCode, $mailbox->balance->available);
}To use a specific HTTP client rather than whichever one is discovered:
$postbode = new PostbodeApiClient('your-api-key', new GuzzleHttp\Client(['timeout' => 30]));Build the request, hand it to the API, and you get a fully typed item back.
use Postbode\Enum\PostalPlex;
use Postbode\Enum\PostalPrinting;
use Postbode\Enum\ShippingType;
use Postbode\Request\PostalRequest;
$request = PostalRequest::make('PSBD', $envelopeUuid)
->addDocumentFromFile(__DIR__ . '/invoice.pdf')
->shipping(ShippingType::NL_FAST)
->printing(PostalPrinting::COLOR)
->plex(PostalPlex::DUPLEX)
->customerReference('INV-1234')
->metadata(['invoice_id' => 1234])
->send();
$postal = $postbode->postals->create($request);
echo $postal->reference; // PSBD-000123
echo $postal->status->name; // Sent
echo $postal->financial->price->amountInclVat;The client base64-encodes your PDFs, so never do that yourself. addDocumentFromFile() reads them off disk,
addDocumentFromContents() takes bytes you already have in memory.
Every builder method returns a new instance, so a partly configured request is safe to keep as a template:
$template = PostalRequest::make('PSBD', $envelopeUuid)->shipping(ShippingType::NL_SLOW);
foreach ($invoices as $invoice) {
$postbode->postals->create(
$template->addDocumentFromFile($invoice->path)->customerReference($invoice->number)->send(),
);
}Leave out ->send(), or pass ->send(false), to create the item without shipping it. Check it, then release it:
$postal = $postbode->postals->create($request->send(false));
file_put_contents('preview.pdf', $postbode->postals->document($postal->uuid));
$postbode->postals->send($postal->uuid); // or ->cancel($postal->uuid)$calculation = $postbode->postals->calculate(
envelope: $envelopeUuid,
pages: 3,
mailbox: 'PSBD',
shipping: ShippingType::NL_FAST,
);
echo $calculation->totalInVat;
foreach ($calculation->elements as $element) {
printf("%-30s € %.2f\n", $element->description, $element->price);
}Nothing is created and nothing is charged by asking.
use Postbode\Enum\PostalStatus;
$postal = $postbode->postals->get($uuid);
if ($postal->status->isDelivered()) {
echo 'Delivered';
}
if ($postal->tracking->isAvailable()) {
echo $postal->tracking->url;
}
foreach ($postbode->postals->list('PSBD', limit: 25, status: PostalStatus::IN_TRANSIT) as $item) {
echo $item->reference, ': ', $item->status->name, PHP_EOL;
}Your recipients can look an item up themselves with just its reference and their own postal code:
$tracked = $postbode->tracking->track($reference, 'NL', '1234AB');
echo $tracked->status;Every endpoint hangs off the client as a property.
| Property | Methods |
|---|---|
$postbode->mailboxes |
list(), get(), create() |
$postbode->postals |
list(), get(), create(), delete(), calculate(), send(), cancel(), performAction(), proof(), document(), logs(), findByV1Id() |
$postbode->envelopes |
listForMailbox(), get(), create(), delete(), pdf(), windowPreview() |
$postbode->products |
listForMailbox(), get() |
$postbode->tags |
listForMailbox(), get(), create() |
$postbode->paperTypes |
list() |
$postbode->address |
validate() |
$postbode->fulfillment |
create() |
$postbode->tracking |
track() |
Every endpoint that takes a request builder also accepts a plain array, if you would rather build the payload yourself:
$postbode->postals->create([
'mailbox' => 'PSBD',
'envelope' => $envelopeUuid,
'documents' => [['filename' => 'invoice.pdf', 'content' => base64_encode($pdf)]],
]);Statuses, shipping methods and printing options are backed enums, each with a label() describing it in the
same words the API documentation uses.
use Postbode\Enum\PostalStatus;
use Postbode\Enum\ShippingType;
PostalStatus::DELIVERED->value; // 150
PostalStatus::DELIVERED->label(); // 'Delivered'
PostalStatus::DELIVERED->isFinal(); // true
ShippingType::NL_REGISTERED->isTracked(); // trueAvailable: PostalStatus, PostalType, PostalAction, ShippingType, PostalPrinting, PostalPlex,
EnvelopeStatus, FulfillmentOrderStatus, TransactionType, TagColor.
Statuses on a resource keep both the typed enum and the raw value, so a status code introduced after this release still decodes:
$postal->status->code; // PostalStatus|null — null if the API added a code we do not know
$postal->status->rawCode; // int — always what the API actually sent
$postal->status->name; // string — the API's own descriptionFailed calls throw; they never return a status code. Everything derives from PostbodeException, so one catch
covers the lot.
use Postbode\Exception\AuthenticationException;
use Postbode\Exception\NotFoundException;
use Postbode\Exception\PostbodeException;
use Postbode\Exception\TransportException;
use Postbode\Exception\ValidationException;
try {
$postbode->postals->create($request);
} catch (ValidationException $e) {
foreach ($e->getErrors() as $field => $messages) {
echo $field, ': ', implode(' ', $messages), PHP_EOL;
}
} catch (AuthenticationException $e) {
// 401 or 403 — bad key, or no access to this mailbox
} catch (NotFoundException $e) {
// 404 — the envelope, mailbox or item does not exist
} catch (TransportException $e) {
// the API could not be reached at all
} catch (PostbodeException $e) {
// anything else, including 400 and 5xx
echo $e->getMessage();
}Version 3 is a rewrite against the v2 API and shares no method names with earlier releases. See UPGRADING.md for the full mapping, including how to translate the letter IDs you already stored into v2 UUIDs.
Runnable scripts live in examples/. Set POSTBODE_API_KEY and run one:
POSTBODE_API_KEY=your-key php examples/list-mailboxes.phpcomposer install
composer test # phpunit, no network access required
composer format # php-cs-fixer