znojil / revolut-business
💸 A simple and modern PHP library for communicating with the Revolut API.
Requires
- php: ^8.2
- znojil/http: ^1.1
Requires (Dev)
- mockery/mockery: ^1.6
- nette/tester: ^2.6
- phpstan/phpstan: ^2.2
- phpstan/phpstan-mockery: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A simple and modern PHP library for communicating with the Revolut Business API.
Covers all 98 endpoints of the Merchant API v1.0 and the Webhooks API v2.0, with typed DTOs, enums and full PHPStan level max coverage.
🚀 Installation
composer require znojil/revolut-business
🔑 Authorization
The Revolut Business API uses OAuth 2.0 with a JWT client assertion. Before you can send a single request, you need to complete a one-time consent flow:
- Create an API certificate. In Revolut Business, go to Settings → API, upload your public key and set a redirect URI. Revolut gives you a client ID and you choose an issuer (the domain of your redirect URI).
- Get the authorization code. Open the consent link shown next to your certificate. After you confirm, Revolut redirects to your redirect URI with a
?code=query parameter. - Exchange the code for a token pair — once, using
Client::authorize().
See Make your first API request for the full walkthrough.
use Znojil\RevolutBusiness\Client; use Znojil\RevolutBusiness\Config; use Znojil\RevolutBusiness\FileTokenStorage; $config = new Config( clientId: 'YOUR_CLIENT_ID', issuer: 'example.com', // the domain of your redirect URI privateKey: file_get_contents('/path/to/privatekey.pem'), sandbox: false // true for the sandbox environment ); $client = new Client($config, new FileTokenStorage('/path/to/tokens.json')); // once, with the code from the redirect $client->authorize($_GET['code']);
From then on the library handles tokens on its own — the access token is refreshed from the stored refresh token whenever it expires, so you only ever call send().
The refresh token expires after 90 days and cannot be renewed automatically. Once that happens,
AuthenticationExceptionis thrown and you have to run the consent flow again.
📖 Usage
1. Sending a Request
Every endpoint is a *Request class. Pass it to Client::send() and you get back a typed result:
use Znojil\RevolutBusiness\Request\GetAccountsRequest; foreach($client->send(new GetAccountsRequest) as $account){ $account->id; // string $account->name; // ?string $account->balance; // float $account->currency; // Currency enum, or string for a currency not in the enum $account->state; // AccountState enum $account->accountType; // AccountType enum $account->createdAt; // DateTimeImmutable }
Requests that need a body take their required parameters first and the optional ones as named arguments:
use Znojil\RevolutBusiness\DTO\PaymentReceiverDTO; use Znojil\RevolutBusiness\Enum\Currency; use Znojil\RevolutBusiness\Enum\TransferReasonCode; use Znojil\RevolutBusiness\Request\CreatePaymentRequest; $result = $client->send(new CreatePaymentRequest( accountId: '05018b0d-e67c-4fec-bea6-415e9da9432c', receiver: new PaymentReceiverDTO('7e18625a-3e6c-4d4f-8429-216c25309a5f', null, null), amount: 123.45, currency: Currency::Gbp, requestId: 'invoice-2026-03', // your own idempotency key reference: 'Invoice 2026/03', transferReasonCode: TransferReasonCode::Services )); $result->id; // string $result->state; // TransactionState enum
Listing endpoints are paginated by time, not by page number — you walk backwards using the timestamp of the oldest item you received:
use Znojil\RevolutBusiness\Request\GetTransactionsRequest; $before = null; do{ $transactions = $client->send(new GetTransactionsRequest(to: $before, count: 100)); foreach($transactions as $transaction){ $transaction->id; $transaction->type; // TransactionType enum $transaction->createdAt; } $before = $transactions !== [] ? end($transactions)->createdAt : null; }while(count($transactions) === 100);
2. Custom Token Storage
FileTokenStorage stores the token pair as JSON in a file with 0600 permissions. To keep tokens in a database or a cache instead, implement Znojil\RevolutBusiness\TokenStorage:
use Znojil\RevolutBusiness\TokenPair; use Znojil\RevolutBusiness\TokenStorage; final class MyTokenStorage implements TokenStorage{ public function load(): ?TokenPair{ // return null when nothing is stored yet } public function save(TokenPair $tokenPair): void{ $tokenPair->accessToken; // string $tokenPair->expirationDatetime; // DateTimeImmutable $tokenPair->refreshToken; // string } } $client = new Client($config, new MyTokenStorage);
save() is called both by authorize() and by every automatic token refresh, so whatever you write to must be durable — losing the refresh token means going through the consent flow again.
3. Using a Custom HTTP Client
By default the library uses znojil/http. You can inject your own implementation as the third argument to the Client constructor. It must implement the Znojil\RevolutBusiness\Http\Client interface.
use Znojil\RevolutBusiness\Client; use Znojil\RevolutBusiness\Http\Client as RevolutHttpClient; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; class MyCustomHttpClient implements RevolutHttpClient{ public function send(string $method, string|UriInterface $uri, array $headers = [], mixed $data = null, array $options = []): ResponseInterface{ // your implementation } } $client = new Client($config, $tokenStorage, new MyCustomHttpClient);
String keys in $options are Znojil\RevolutBusiness\Http\Option::* enum values that every implementation must honor, and unknown string keys must be rejected with an exception. Integer keys are raw CURLOPT_* constants — non-cURL implementations must reject them with an exception rather than silently ignore them, so that a consumer never ends up with options that silently don't apply.
🧩 Conventions
A few things that are specific to this library rather than to the API itself.
Clearing a Property
On Update* requests, null and "remove this value" are two different intentions, so null cannot mean both. Passing null leaves a property unchanged; to have the API remove it, pass the Clear::Value sentinel:
use Znojil\RevolutBusiness\Clear; use Znojil\RevolutBusiness\Request\UpdatePaymentDraftRequest; // only the title is sent, the schedule stays as it is $client->send(new UpdatePaymentDraftRequest($draftId, title: 'New title')); // the schedule is explicitly removed $client->send(new UpdatePaymentDraftRequest($draftId, scheduleFor: Clear::Value));
Where a property cannot be cleared through the API, its parameter simply doesn't accept Clear.
Requests That Need At Least One Property
Most Update* endpoints reject an empty body. Those requests throw InvalidArgumentException when you construct one with nothing to update, rather than sending a request that is guaranteed to fail.
Enums
Most enums are closed: an unknown value in a response throws UnexpectedValueException, so you learn about an API change instead of silently getting a wrong value.
Three of them are open, because their real value sets are larger than what the documentation lists and a new entry must not break a running application. Currency, PaymentRoute and TransferReasonCode are typed as string|Enum on the response side — you get the enum case when the value is known and the raw string when it isn't:
use Znojil\RevolutBusiness\Enum\Currency; use Znojil\RevolutBusiness\Request\GetAccountRequest; $account = $client->send(new GetAccountRequest($accountId)); if($account->currency instanceof Currency){ // a known currency }else{ // a currency not covered by the enum, as a raw string }
On the request side these are always plain enums — you can only send what the library knows.
Parameter Validation
Requests do not validate their parameters. The API returns precise errors for invalid input, and duplicating those rules here would mean maintaining a second copy of somebody else's specification. Every request class links to its official documentation page in a @link annotation.
📋 Available Requests
All 98 requests live in the Znojil\RevolutBusiness\Request namespace, grouped here the same way as the official documentation.
Accounting
| Request | Documentation |
|---|---|
CreateAccountingCategoryRequest |
Create an accounting category |
GetAccountingCategoriesRequest |
Retrieve accounting categories |
GetAccountingCategoryRequest |
Retrieve an accounting category |
UpdateAccountingCategoryRequest |
Update an accounting category |
DeleteAccountingCategoryRequest |
Delete an accounting category |
CreateLabelGroupRequest |
Create a label group |
GetLabelGroupsRequest |
Retrieve label groups |
GetLabelGroupRequest |
Retrieve a label group |
UpdateLabelGroupRequest |
Update a label group |
DeleteLabelGroupRequest |
Delete a label group |
CreateLabelRequest |
Create a label |
GetLabelsRequest |
Retrieve labels |
UpdateLabelRequest |
Update a label |
DeleteLabelRequest |
Delete a label |
CreateTaxRateRequest |
Create a tax rate |
GetTaxRatesRequest |
Retrieve tax rates |
GetTaxRateRequest |
Retrieve a tax rate |
UpdateTaxRateRequest |
Update a tax rate |
DeleteTaxRateRequest |
Delete a tax rate |
Accounts
| Request | Documentation |
|---|---|
GetAccountsRequest |
Retrieve all accounts |
GetAccountRequest |
Retrieve an account |
GetAccountBankDetailsRequest |
Retrieve account's full bank details |
Cards
| Request | Documentation |
|---|---|
GetCardsRequest |
Retrieve all cards |
CreateCardRequest |
Create a card |
GetCardRequest |
Retrieve a card |
UpdateCardRequest |
Update a card |
TerminateCardRequest |
Terminate a card |
UpdateCardContactsRequest |
Update card contacts |
UpdateCardReferencesRequest |
Update card references |
FreezeCardRequest |
Freeze a card |
UnfreezeCardRequest |
Unfreeze a card |
LockCardRequest |
Lock a card |
UnlockCardRequest |
Unlock a card |
GetSensitiveCardDetailsRequest |
Retrieve sensitive card details |
Card invitations
| Request | Documentation |
|---|---|
CreateCardInvitationRequest |
Create a card invitation |
GetCardInvitationsRequest |
Retrieve card invitations |
GetCardInvitationRequest |
Retrieve a card invitation |
UpdateCardInvitationRequest |
Update a card invitation |
CancelCardInvitationRequest |
Cancel a card invitation |
Counterparties
| Request | Documentation |
|---|---|
ValidateAccountNameRequest |
Validate an account name |
GetCounterpartiesRequest |
Retrieve counterparties |
CreateCounterpartyRequest |
Create a counterparty |
GetCounterpartyCountriesRequest |
Retrieve counterparty countries |
GetCounterpartyFieldsRequest |
Retrieve counterparty requirements |
GetCounterpartyRequest |
Retrieve a counterparty |
DeleteCounterpartyRequest |
Delete a counterparty |
UpdateCounterpartyPaymentMethodRequest |
Update a counterparty payment method |
Expenses
| Request | Documentation |
|---|---|
GetExpensesRequest |
Retrieve expenses |
GetExpenseRequest |
Retrieve an expense |
GetExpenseReceiptRequest |
Retrieve an expense receipt |
Foreign exchange
| Request | Documentation |
|---|---|
GetExchangeRateRequest |
Get exchange rate |
ExchangeMoneyRequest |
Exchange money |
GetExchangeReasonsRequest |
Retrieve exchange reasons |
Payment drafts
| Request | Documentation |
|---|---|
GetPaymentDraftsRequest |
Retrieve payment drafts |
CreatePaymentDraftRequest |
Create a payment draft |
GetPaymentDraftRequest |
Retrieve a payment draft |
DeletePaymentDraftRequest |
Delete a payment draft |
UpdatePaymentDraftRequest |
Update a payment draft |
CreatePaymentDraftPaymentRequest |
Add a payment to a draft |
UpdatePaymentDraftPaymentRequest |
Update a draft payment |
DeletePaymentDraftPaymentRequest |
Delete a draft payment |
Payout links
| Request | Documentation |
|---|---|
CreatePayoutLinkRequest |
Create a payout link |
GetPayoutLinksRequest |
Retrieve payout links |
GetPayoutLinkRequest |
Retrieve a payout link |
CancelPayoutLinkRequest |
Cancel a payout link |
Simulations
Sandbox only.
| Request | Documentation |
|---|---|
SimulateTransactionStateRequest |
Simulate a transfer state update |
SimulateTopUpRequest |
Simulate an account top-up |
Teams
| Request | Documentation |
|---|---|
GetTeamMembersRequest |
Retrieve team members |
InviteTeamMemberRequest |
Invite a team member |
GetTeamMemberRequest |
Retrieve a team member |
DeleteTeamMemberRequest |
Delete a team member |
AssignTeamMemberDepartmentRequest |
Assign a department |
UnassignTeamMemberDepartmentRequest |
Unassign a department |
AssignTeamMemberManagerRequest |
Assign a manager |
UnassignTeamMemberManagerRequest |
Unassign a manager |
UpdateTeamMemberRoleRequest |
Update a team member's role |
SuspendTeamMemberRequest |
Suspend a team member |
UnsuspendTeamMemberRequest |
Unsuspend a team member |
GetRolesRequest |
Retrieve roles |
CreateDepartmentRequest |
Create a department |
GetDepartmentsRequest |
Retrieve departments |
GetDepartmentRequest |
Retrieve a department |
UpdateDepartmentRequest |
Update a department |
DeleteDepartmentRequest |
Delete a department |
Transactions
| Request | Documentation |
|---|---|
GetTransactionsRequest |
Retrieve transactions |
GetTransactionRequest |
Retrieve a transaction |
Transfers
| Request | Documentation |
|---|---|
CreatePaymentRequest |
Create a payment |
GetIndicativeQuoteRequest |
Get an indicative quote |
GetPaymentFieldsRequest |
Retrieve payment requirements |
CreateTransferRequest |
Move money between your accounts |
GetTransferReasonsRequest |
Retrieve transfer reasons |
Webhooks
Version 2.0 of the Webhooks API. The deprecated v1 endpoints are not implemented.
| Request | Documentation |
|---|---|
CreateWebhookRequest |
Create a webhook |
GetWebhooksRequest |
Retrieve webhooks |
GetWebhookRequest |
Retrieve a webhook |
UpdateWebhookRequest |
Update a webhook |
DeleteWebhookRequest |
Delete a webhook |
RotateWebhookSigningSecretRequest |
Rotate a webhook signing secret |
GetFailedWebhookEventsRequest |
Retrieve failed webhook events |
⚠️ Error Handling
The client throws exceptions to help you identify the issue:
Znojil\RevolutBusiness\Exception\ClientException: For HTTP client-side errors (4xx).Znojil\RevolutBusiness\Exception\ServerException: For HTTP server-side errors (5xx).Znojil\RevolutBusiness\Exception\ResponseException: For other unsuccessful HTTP responses. The base class of the three above — it carriesapiErrorCode,apiErrorIdand the rawresponseBody.Znojil\RevolutBusiness\Exception\JsonException: When a response body is not valid JSON.Znojil\RevolutBusiness\Exception\JsonResponseException: When a response body is valid JSON but not the object or array the endpoint promises (subtype ofResponseException).Znojil\RevolutBusiness\Exception\UnexpectedValueException: When a response contains an enum value the library does not know.Znojil\RevolutBusiness\Exception\InvalidArgumentException: For invalid input (e.g. an update request with no properties to update).Znojil\RevolutBusiness\Exception\MissingTokenException: When no token pair is stored yet — run the authorization flow first.Znojil\RevolutBusiness\Exception\IOException: WhenFileTokenStoragecannot read or write the token file.Znojil\RevolutBusiness\Auth\Exception\AuthenticationException: When the token exchange or refresh fails (e.g. an expired or revoked refresh token).Znojil\RevolutBusiness\Auth\Exception\ClientAssertionException: When the JWT client assertion cannot be created (e.g. an invalid private key).
use Znojil\RevolutBusiness\Auth\Exception\AuthenticationException; use Znojil\RevolutBusiness\Exception\ClientException; use Znojil\RevolutBusiness\Exception\ServerException; try{ $result = $client->send(new CreatePaymentRequest(...)); }catch(ClientException $e){ echo $e->getMessage(); // error message from Revolut echo $e->getCode(); // HTTP status code echo $e->apiErrorCode; // ?int error code from Revolut echo $e->apiErrorId; // ?string error id, useful when contacting support }catch(ServerException $e){ // Revolut server error (5xx) }catch(AuthenticationException $e){ // the refresh token has expired or been revoked — run the consent flow again }
All exceptions thrown by the library implement the
Znojil\RevolutBusiness\Exception\Exceptionmarker interface, so a single catch can cover them all.
📄 License
This library is open-source software licensed under the MIT license.