bunq/sdk_php
The official PHP SDK for the bunq banking API, helping developers build apps that manage accounts, payments, cards, and webhooks programmatically. It handles authentication, request signing, and API calls for bunq's neobank platform.
composer require bunq/sdk_php then check the examples/ directory and the README's example.php — that file shows the full bootstrap in one place. The generated API surface lives under src/Model/Generated/Endpoint/; that's your real reference for what operations exist.https://sandbox.bunq.com, create an app, and copy the API key. Production keys are separate and will reject sandbox keys, and vice versa.ApiContext once and persist it — do not create a new one per request:use bunq\ApiContext;
use bunq\BunqContext;
use bunq\Util\BunqEnumApiEnvironmentType;
$apiContext = ApiContext::create(
BunqEnumApiEnvironmentType::SANDBOX(),
getenv('BUNQ_API_KEY'),
'my-app-name',
['my-device']
);
$apiContext->save('/secure/bunq.conf');
BunqContext::loadApiContext(ApiContext::restore('/secure/bunq.conf'));
$userId = BunqContext::getUserId();
use bunq\Model\Generated\Endpoint\MonetaryAccountBank;
use bunq\Model\Generated\Endpoint\Payment;
use bunq\Model\Generated\Object\Amount;
use bunq\Model\Generated\Object\Pointer;
$accounts = MonetaryAccountBank::list($userId)->getValue();
$accountId = $accounts[0]->getId();
$payment = Payment::create(
new Amount('EUR', '0.01'),
new Pointer('EMAIL', '[email protected]'),
'first sandbox payment'
);
BunqClient or service class registered in the container. ApiContext::create() hits the API three times (installation, device server, session server) — never do it inside a request loop.Payment::create(), Payment::list($userId, $accountId), RequestInquiry::create(), etc. Older 0.x releases used PaymentService::create() — pin bunq/sdk_php in composer.json and don't mix the two.BunqResponse* objects. Use ->getValue() for a single entity or the list array, and ->getItems() when iterating paginated lists. Check getResponseCode() on the wrapper before assuming success.Idempotency object with a deterministic token (e.g. hash of your order ID) to create() calls so retries don't double-charge:$idempotency = Idempotency::create('order-12345-attempt-1');
Payment::create($amount, $counterparty, 'desc', $idempotency);
Pagination::create(50) and loop with setOlderId() until the result array is shorter than the page size. Don't assume one call returns all history.RequestInquiry creates a request the counterparty sees; listen for accepted responses via RequestResponse or a webhook, then reconcile.DraftPayment + Schedule + ScheduleInstance. Create the schedule first, then reference its ID in the draft.NotificationFilterUrlUser (all events) or NotificationFilterUrlMonetaryAccount for one account. The callback URL must be public HTTPS. The SDK does not verify signatures — treat the body as a hint and re-fetch the referenced object via the API before acting on it.Attachment::create() first, then pass the returned ID in the payment body; you cannot inline binary content.ApiContext (and their own API key). Store the serialized context encrypted, keyed by your user ID, and load the right one per request.setUp(), call BunqContext::clearContext() and load a fixture context; stub the SDK's low-level ApiClient (or use a fake HTTP client) so unit tests don't hit bunq. Keep integration tests against sandbox only.ApiContext will attempt a session re-creation, but if the installation/device server is stale you'll see 401s. Catch, restore(), and retry once; if that fails, re-run ApiContext::create() (which re-registers the device).BunqContext is a singleton — anything calling endpoint methods after another bootstrap will silently use the wrong context. Always loadApiContext() explicitly; call clearContext() in tests and long-running workers between tenants.BunqEnumApiEnvironmentType::SANDBOX() / PRODUCTION(), not the literal 'SANDBOX'. Mismatches throw TypeError — a common failure after upgrading.ApiContext::create() in production blocks until the user approves the device in the bunq app. In automation, wrap it in a poll loop on DeviceServer::get() and surface a clear error if not approved within a timeout.BunqException::getErrorDescriptions() returns an array of Error objects with getErrorDescription(), getCategory(), and getCode() — log all three. HTTP 429 means rate limited; back off exponentially and respect Retry-After.UserId and MonetaryAccountId are what you should persist, not Payment::getId() from a sandbox response. Cache the user ID from BunqContext::getUserId() at login time instead of resolving it every request.MonetaryAccount vs MonetaryAccountBank. The generic MonetaryAccount list returns references; to get balances use MonetaryAccountBank::get($userId, $accountId) or the MonetaryAccountSavings/Joint variants. getBalance() returns an Amount where the currency is the first field — don't assume EUR.OauthClient + BunqEnumOauthResponseType are for third-party apps that let users connect their own bunq account. For a single-tenant integration, API key + ApiContext::restore() is sufficient and much simpler.BunqLogger util; enable debug mode during integration to see request/response payloads. In production route it to a proper logger and redact the Authorization header and context contents.How can I help you explore Laravel packages today?