sudiptpa / wise-php-sdk
Unofficial Wise Platform PHP SDK with rich models and transport-agnostic architecture.
Requires
- php: ^8.2
- psr/http-client: ^1.0
- psr/http-factory: ^1.1
- psr/http-message: ^1.1 || ^2.0
- psr/log: ^1.1 || ^2.0 || ^3.0
Requires (Dev)
- laravel/pint: ^1.18
- phpstan/phpstan: ^1.12
- phpunit/phpunit: ^10.5
Suggests
- ext-curl: Use with a custom Curl transport implementation.
- guzzlehttp/guzzle: Use with Psr18Transport by passing a PSR-18 client.
- http-interop/http-factory-guzzle: PSR-17 factories for Guzzle users.
README
Unofficial PHP SDK for Wise Platform APIs.
Unofficial Disclaimer
This package is not affiliated with, endorsed by, or maintained by Wise.
Requirements
- PHP 8.2+
- A transport implementation of
Sujip\Wise\Contracts\TransportInterface
Installation
composer require sudiptpa/wise-php-sdk
Quick Start
use GuzzleHttp\Client; use Http\Factory\Guzzle\RequestFactory; use Http\Factory\Guzzle\StreamFactory; use Sujip\Wise\Config\ClientConfig; use Sujip\Wise\Transport\Psr18Transport; use Sujip\Wise\Wise; $config = ClientConfig::apiToken('your-wise-api-token', ClientConfig::SANDBOX_BASE_URL); $transport = new Psr18Transport(new Client()); $wise = Wise::client($config, $transport, new RequestFactory(), new StreamFactory()); $profiles = $wise->profile()->list(); $firstProfileId = $profiles->all()[0]->id ?? null; $rates = $wise->rate()->list(); $balances = $wise->balance()->list((int) $firstProfileId);
Available Resources
$wise->profile()$wise->contact()$wise->currencies()$wise->address()$wise->quote()$wise->recipientAccount()$wise->transfer()$wise->payment()$wise->webhook()$wise->activity()$wise->balance()$wise->rate()$wise->balanceStatement()$wise->bankAccountDetails()$wise->user()$wise->userTokens()
Configuration
use Sujip\Wise\Config\ClientConfig; $api = ClientConfig::apiToken('your-wise-api-token'); $apiSandbox = ClientConfig::apiToken('your-wise-api-token', ClientConfig::SANDBOX_BASE_URL); $oauth = ClientConfig::oauth2('oauth-access-token'); $oauthSandbox = ClientConfig::oauth2('oauth-access-token', ClientConfig::SANDBOX_BASE_URL);
Base URLs:
- Production:
https://api.wise.com - Sandbox:
https://api.wise-sandbox.com
Auth Modes
| Mode | Use case | Credential | Token handling | Notes |
|---|---|---|---|---|
| API Token | Automating your own Wise account | Personal/Business API token | Managed by you | Best fit for single-account access; do not assume API funding is available |
| OAuth2 | Wise Platform / partner integrations | OAuth2 access token | Refresh flow in your app | Required for partner-style flows and connected-account access |
See the full auth capability guide:
docs/AUTH_CAPABILITIES.md
Auth Capability Summary
| Capability | Personal API Token | OAuth2 |
|---|---|---|
| Read your own account data | Yes | Yes |
| Create quotes and transfer drafts | Yes | Yes |
| Fund transfers through API | Limited and not guaranteed | Yes, in partner setups |
| Manage other Wise accounts | No | Yes, in partner setups |
Use /oauth/token app credentials flow |
No | Yes |
Notes:
- Personal API tokens are for your own Wise account.
- OAuth2 with
clientId/clientSecretis the Wise partner path. - If your profile is in the UK or EEA, do not rely on personal-token funding by API.
- Outside the UK/EEA, funding can still depend on your account setup. Check with Wise if this matters for your use case.
- If you only need self-account automation, start with personal token support.
- If you need delegated account access or API funding flows, plan for OAuth2.
If you rotate OAuth2 tokens, provide your own token provider:
use Sujip\Wise\Auth\AuthMode; use Sujip\Wise\Config\ClientConfig; use Sujip\Wise\Contracts\AccessTokenProviderInterface; final class OAuthProvider implements AccessTokenProviderInterface { public function getAccessToken(): string { return 'fresh-access-token'; } } $config = new ClientConfig( authMode: AuthMode::OAuth2, accessTokenProvider: new OAuthProvider(), baseUrl: ClientConfig::DEFAULT_BASE_URL, );
Transport Options (Choose One)
The SDK does not pick a transport for you.
1) PSR-18 + Guzzle
Install optional dependencies:
composer require guzzlehttp/guzzle http-interop/http-factory-guzzle
use GuzzleHttp\Client; use Http\Factory\Guzzle\RequestFactory; use Http\Factory\Guzzle\StreamFactory; use Sujip\Wise\Transport\Psr18Transport; use Sujip\Wise\Wise; $transport = new Psr18Transport(new Client()); $wise = Wise::client($config, $transport, new RequestFactory(), new StreamFactory());
2) Curl transport (example)
final class CurlTransport implements \Sujip\Wise\Contracts\TransportInterface { public function send(\Psr\Http\Message\RequestInterface $request): \Psr\Http\Message\ResponseInterface { // Run curl and map response to PSR-7. } }
3) Laravel transport (example)
final class LaravelTransport implements \Sujip\Wise\Contracts\TransportInterface { public function send(\Psr\Http\Message\RequestInterface $request): \Psr\Http\Message\ResponseInterface { // Call Laravel HTTP client and map response to PSR-7. } }
See full transport setup guides:
docs/transports/guzzle.mddocs/transports/curl.mddocs/transports/laravel.md
Send Money in 4 Steps
use Sujip\Wise\Resources\Payment\Requests\FundTransferRequest; use Sujip\Wise\Resources\Quote\Requests\CreateAuthenticatedQuoteRequest; use Sujip\Wise\Resources\RecipientAccount\Requests\CreateRecipientAccountRequest; use Sujip\Wise\Resources\Transfer\Requests\CreateTransferRequest; $quote = $wise->quote()->createAuthenticated( 123, CreateAuthenticatedQuoteRequest::fixedTarget('USD', 'EUR', 100) ); $recipient = $wise->recipientAccount()->create( new CreateRecipientAccountRequest(123, 'Jane Doe', 'EUR', 'iban', ['iban' => 'DE123']) ); $transfer = $wise->transfer()->create(CreateTransferRequest::from($quote, $recipient)); $payment = $wise->payment()->fundTransfer(123, $transfer->id, new FundTransferRequest('BALANCE'));
Important:
- The funding step is not the same as creating a draft transfer.
- If your profile is in the UK or EEA, do not rely on personal-token funding by API.
- Outside the UK/EEA, check with Wise if API funding is important for your flow.
- In most cases, personal-token users will create the transfer draft by API and complete funding in Wise web or mobile.
- OAuth2 partner environments are the expected path for API funding.
Activity Listing + Pagination
use Sujip\Wise\Resources\Activity\Requests\ListActivitiesRequest; $page = $wise->activity()->list(123, new ListActivitiesRequest(status: 'COMPLETED', size: 20)); foreach ($page->activities as $activity) { echo $activity->status().' - '.$activity->titlePlainText().PHP_EOL; } while ($page->hasNext()) { $page = $wise->activity()->list(123, new ListActivitiesRequest(nextCursor: $page->nextCursor(), size: 20)); }
Or iterate through all pages:
foreach ($wise->activity()->iterate(123, new ListActivitiesRequest(size: 50)) as $activity) { echo $activity->titlePlainText().PHP_EOL; }
Finding Your Profile ID
curl -sS https://api.wise-sandbox.com/v2/profiles \ -H "Authorization: Bearer <TOKEN>" \ -H "Accept: application/json"
Use the id field from the response.
member id is different from profile id.
Error Handling
use Sujip\Wise\Exceptions\ApiException; use Sujip\Wise\Exceptions\AuthException; use Sujip\Wise\Exceptions\RateLimitException; try { $quote = $wise->quote()->get(123, 456); } catch (AuthException $e) { // 401/403 } catch (RateLimitException $e) { // 429, retry delay in $e->retryAfter (seconds) } catch (ApiException $e) { // Other 4xx/5xx, payload in $e->errorBody }
Error Map
| HTTP status | Exception | Typical action |
|---|---|---|
| 401 / 403 | AuthException |
Check token type, token value, and scope |
| 429 | RateLimitException |
Wait and retry using retryAfter |
| 4xx / 5xx | ApiException |
Check error payload and request IDs |
| Transport failure | TransportException |
Check connectivity and transport implementation |
Retry and Idempotency
Retries are off by default. Enable them explicitly:
use Sujip\Wise\Auth\AuthMode; use Sujip\Wise\Auth\StaticAccessTokenProvider; use Sujip\Wise\Config\ClientConfig; $config = new ClientConfig( authMode: AuthMode::ApiToken, accessTokenProvider: new StaticAccessTokenProvider('your-token'), baseUrl: ClientConfig::DEFAULT_BASE_URL, retryEnabled: true, retryMaxAttempts: 4, retryBaseDelayMs: 200, retryMaxDelayMs: 2000, retryMethods: ['GET', 'POST'], idempotencyKey: 'your-stable-idempotency-key', );
Notes:
- Retry middleware applies only when enabled.
- It retries 429 and selected 5xx responses.
- Use idempotency keys for retryable write operations.
- Idempotency key is attached to SDK
POSToperations when configured.
Webhooks
Create subscriptions through WebhookResource.
Verify payload signatures before processing:
use Sujip\Wise\Resources\Webhook\WebhookVerifier; $payload = file_get_contents('php://input') ?: ''; $signature = $_SERVER['HTTP_X_SIGNATURE_SHA256'] ?? ''; $secret = 'your-webhook-secret'; $ok = (new WebhookVerifier())->verify($payload, $signature, $secret);
Replay protection helper:
use Sujip\Wise\Resources\Webhook\WebhookReplayProtector; use Sujip\Wise\Support\InMemoryWebhookReplayStore; $replayProtector = new WebhookReplayProtector(new InMemoryWebhookReplayStore(), 300); $replayProtector->validate($eventId, $eventTimestamp);
Redis replay store example:
use Sujip\Wise\Contracts\WebhookReplayStoreInterface; final class RedisWebhookReplayStore implements WebhookReplayStoreInterface { public function __construct(private \Redis $redis) {} public function remember(string $eventId, int $ttlSeconds): bool { return (bool) $this->redis->set("wise:webhook:{$eventId}", '1', ['nx', 'ex' => $ttlSeconds]); } }
Production Checklist
- Set
timeoutSecondsto a value suitable for your runtime and workload. - Keep retries off by default; enable only with explicit retry methods and limits.
- Use idempotency keys for retryable write flows.
- Rotate API/OAuth credentials and never store them in source control.
- Verify webhook signatures and enforce replay checks in persistent storage.
- Track
requestId/correlationIdfrom exceptions in logs and alerts.
Test Confidence
- Unit tests cover request path/method/body contracts for implemented endpoints.
- Fixtures in
tests/Fixtures/wiseare used for deterministic model hydration tests. - Middleware tests cover retry, idempotency behavior, and logging sanitization.
- Sandbox workflow provides scheduled live verification against Wise sandbox.
Production Checklist
- Set timeout and connect-timeout values.
- Use structured logging and keep secrets redacted.
- Rotate API/OAuth credentials.
- Use idempotency for retryable writes.
- Log request/correlation IDs for support.
- Monitor auth, rate-limit, and server error rates.
Versioning and Compatibility
- SemVer.
- Runtime target: PHP
^8.2. - CI runs on
8.2,8.3,8.4;8.5is non-blocking.
Guides
docs/transports/guzzle.mddocs/transports/curl.mddocs/transports/laravel.mddocs/API_REFERENCE.mddocs/WISE_API_STATUS.mddocs/SANDBOX_CHECKS.mddocs/VERSIONING.mdRELEASE.md
FAQ
I get invalid_token. What should I check?
- Match token type to environment (
api.wise.comvsapi.wise-sandbox.com). - Confirm token is active and complete.
- Confirm scope/profile access.
Is profile ID the same as member ID?
No. Use id from /v2/profiles.
Can I use live personal token in CI?
Not recommended. Use sandbox credentials in CI.
Why does SDK require transport + PSR-17 factories?
The SDK stays transport-agnostic. You bring the HTTP stack.
Quality
composer qa
API Reference
See docs/API_REFERENCE.md.
Sandbox Checks
See docs/SANDBOX_CHECKS.md.