postbode / postbode-api
Official supported client for Postbode.nu (https://postbode.nu)
Requires
- php: ^8.2
- php-http/discovery: ^1.19
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1|^2.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- guzzlehttp/guzzle: ^7.5
- nyholm/psr7: ^1.8
- php-http/mock-client: ^1.6
- phpunit/phpunit: ^11
Suggests
- guzzlehttp/guzzle: A PSR-18 HTTP client implementation, for example ^7.5
README
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
Requirements
- PHP 8.2 or higher
- A PSR-18 HTTP client and a PSR-17 factory
Installation
composer require postbode/postbode-api
If your project does not have an HTTP client yet, add one — Guzzle is the usual choice and needs no further configuration:
composer require guzzlehttp/guzzle
Already using Symfony's HttpClient, or a client of your own? Pass it in and skip the extra dependency.
Getting started
- 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]));
Sending a letter
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(), ); }
Creating a concept first
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)
Checking the price up front
$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.
Following an item
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;
Endpoints
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)]], ]);
Enums
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(); // true
Available: 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 description
Error handling
Failed 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(); }
Upgrading from 1.x and 2.x
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.
Examples
Runnable scripts live in examples/. Set POSTBODE_API_KEY and run one:
POSTBODE_API_KEY=your-key php examples/list-mailboxes.php
Contributing
composer install composer test # phpunit, no network access required composer format # php-cs-fixer