sudiptpa/wise-php-sdk

Unofficial Wise Platform PHP SDK with rich models and transport-agnostic architecture.

Maintainers

Package info

github.com/sudiptpa/wise-php-sdk

pkg:composer/sudiptpa/wise-php-sdk

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-03-13 08:41 UTC

This package is auto-updated.

Last update: 2026-03-13 10:46:58 UTC


README

Unofficial PHP SDK for Wise Platform APIs.

Tests Latest Version Total Downloads License

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 / clientSecret is 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.md
  • docs/transports/curl.md
  • docs/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 POST operations 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 timeoutSeconds to 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 / correlationId from exceptions in logs and alerts.

Test Confidence

  • Unit tests cover request path/method/body contracts for implemented endpoints.
  • Fixtures in tests/Fixtures/wise are 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.5 is non-blocking.

Guides

  • docs/transports/guzzle.md
  • docs/transports/curl.md
  • docs/transports/laravel.md
  • docs/API_REFERENCE.md
  • docs/WISE_API_STATUS.md
  • docs/SANDBOX_CHECKS.md
  • docs/VERSIONING.md
  • RELEASE.md

FAQ

I get invalid_token. What should I check?

  • Match token type to environment (api.wise.com vs api.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.