Wise PHP SDK for Wise Platform APIs.
If this package has been useful to you, GitHub Sponsors is a simple way to support ongoing maintenance, improvements, and future releases.
This package is not affiliated with, endorsed by, or maintained by Wise.
- PHP 8.2+
- A transport implementation of
Sujip\Wise\Contracts\TransportInterface
composer require sudiptpa/wise-php-sdkuse 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);$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()
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
| 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
| 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,
);The SDK does not pick a transport for you.
Install optional dependencies:
composer require guzzlehttp/guzzle http-interop/http-factory-guzzleuse 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());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.
}
}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
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.
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;
}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.
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
}| 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 |
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.
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]);
}
}- 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.
- 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.
- 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.
- SemVer.
- Runtime target: PHP
^8.2. - CI runs on
8.2,8.3,8.4;8.5is non-blocking.
docs/transports/guzzle.mddocs/transports/curl.mddocs/transports/laravel.mddocs/API_REFERENCE.mddocs/WISE_API_STATUS.mddocs/SANDBOX_CHECKS.mddocs/VERSIONING.mdRELEASE.md
- Match token type to environment (
api.wise.comvsapi.wise-sandbox.com). - Confirm token is active and complete.
- Confirm scope/profile access.
No. Use id from /v2/profiles.
Not recommended. Use sandbox credentials in CI.
The SDK stays transport-agnostic. You bring the HTTP stack.
composer qaSee docs/API_REFERENCE.md.
See docs/SANDBOX_CHECKS.md.