Download the PHP package thales/wave without Composer

On this page you can find all versions of the php package thales/wave. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package wave

thales/wave

A fully-typed PHP SDK for the Wave Business API, built on Saloon. Readonly DTOs, typed error codes, cursor pagination, and HMAC request signing. Works in plain PHP; ships an optional Laravel bridge.

Installation

Requires PHP 8.2+ and Saloon 4.

Saloon 3 is deliberately not supported: every 3.x release carries three unpatched CVEs (insecure deserialization, path traversal, and SSRF via absolute-URL endpoint override), so Composer refuses to install it at all. v4.0.0 is the first fixed release.

Status

Complete — all five Wave Business API domains.

Domain Status
Checkout ✅ all 6 endpoints
Payout ✅ all 7 endpoints, plus the undocumented B2B payout
Balance & Reconciliation ✅ all 3 endpoints
Aggregated Merchants ✅ all 5 endpoints
Webhooks ✅ all 6 events + HMAC verification

The shared foundation — connector, auth, request signing, typed exceptions, cursor pagination — is done and used by every domain that follows.

Configuration

Plain PHP

Laravel

The service provider is auto-discovered. Set your key and go:

Or inject the connector — it is bound as a singleton:

To customise anything else, publish the config:

Key Env Purpose
api_key WAVE_API_KEY From business.wave.com/dev-portal
base_url WAVE_BASE_URL Override for a proxy; defaults to https://api.wave.com
signing_secret WAVE_SIGNING_SECRET Outbound Wave-Signature. Set only once signing is enabled in the dev portal
webhook_secret WAVE_WEBHOOK_SECRET Inbound webhook verification
webhook_tolerance WAVE_WEBHOOK_TOLERANCE Max signature age in seconds, default 300 (Wave's own limit)
aggregated_merchant_id WAVE_AGGREGATED_MERCHANT_ID Default merchant for aggregators

Checkout

Reading a session

isComplete() is not isPaid(). checkout_status = complete only means the payer finished the flow; the payment can still be processing. Gate fulfilment on isPaid().

Payout

A 200 is not a success

This is the one thing to get right. Wave reports a failed payout as HTTP 200, with the failure in the body — so nothing throws and a naive try/catch sees a clean call:

receiveAmount is what the recipient gets; the fee is charged to you on top. totalDebited() adds them as integer minor units, so it never drifts:

Batches

Batch processing is asynchronous — createBatch() returns only an id, and the payouts are not in that response. Poll for them:

A batch can be complete with most of its payouts failed, so always look at failed() rather than trusting the batch status.

Reversals

Wave allows a reversal for exactly 3 days from the payout's timestamp, fees included:

Safe to retry — Wave's own idempotency means a second reversal of the same payout succeeds without creating another transaction, so you cannot double-reverse by accident. Past the window you get ErrorCode::PayoutReversalTimeLimitExceeded.

Verifying a recipient

Restricted access. Wave disables this endpoint by default and enables it per business against a documented compliance justification; without that you get a 403. It is also rate limited to 30 checks per phone number per 5 minutes, and exceeding that blocks the number for an hour.

Every field is nullable and null means "not asked", never "no". A null nameMatch means you sent no name; NAME_NOT_KNOWN means Wave holds no name. Neither is a mismatch, which is why nameMatches() and nameMismatches() are separate questions rather than one boolean.

B2B payouts

@experimentalPOST /v1/b2b/payout is not in Wave's public documentation. It is included because it is in production use, and its contract here was derived from that usage rather than a published spec. The response DTO requires only id and status and exposes raw(), so an unannounced change cannot break hydration. Everything else in this package maps to a documented endpoint.

Balance & Reconciliation

Balance

Transactions

transactions() walks every page lazily — nothing is fetched until you iterate, and only one page is held at a time:

Omit the date for today. Pass a DateTimeInterface or a 'YYYY-MM-DD' string — anything else is rejected before the request goes out, because Wave would quietly return a different day and a silently wrong day is the worst outcome for reconciliation.

Two things that will bite a naive ledger

1. transactionId is not unique. A reversal reuses the id of the row it reverses. Wave's own example feed contains T_2YJNPWMCIY twice — once as a payment, once as its reversal. Keying on the id alone silently collapses them:

2. amount is already net of fee. It is the signed balance delta, not the gross figure. A received payment of 100 with a fee of 1 comes back as amount: "99", fee: "1"; a payout as amount: "-101", fee: "1". Adding or subtracting fee from amount double-counts.

Also worth knowing: transactionType is nullable — Wave documents it as "can be empty" and omits it entirely from its own examples — and fee/balance are nullable too, so one sparse row cannot fail a whole day's reconciliation.

Resumable reconciliation

When the cursor has to outlive the process — a nightly job picking up where it stopped — drive the pages yourself:

transactionPaginator() is also available if you want Saloon's paginator itself — for collect(), setMaxPages(), or the raw responses. Note that iterating a paginator directly yields Response objects rather than rows; that is Saloon's contract, and why transactions() exists as the everyday path.

Refunds

Reverses a payment you received, fees included. No reason required. Idempotent on Wave's side, so a retry cannot produce a second refund. An unknown id raises NotFoundException.

Aggregated Merchants

Merchant identities you transact under, each with its own name and fee structure.

Partner-only — Wave limits this API to selected aggregators. A key without access gets ErrorCode::NoPermission (403). If your business is not an aggregator, skip this section.

Creating one, then transacting as it:

Or set it once on the connector and every Checkout/Payout call inherits it — see aggregated_merchant_id in the config table above.

Merchants lock after review

Once Wave reviews a merchant and assigns its fee structures, the record locks. Updates then fail with ErrorCode::RecordLocked (403); deletion still works.

The fee structures are read-only — Wave sets them, and AggregatedMerchantData has no way to pass one. Their names are not parallel, so decode them rather than parsing the strings:

Update is a full replacement

update() is a PUT that replaces the whole record: any field left null in the payload is cleared, not left alone. To change one field, start from the current state:

toRequestData() copies the merchant's current values and overrides only the arguments you name. (It therefore cannot clear a field — construct AggregatedMerchantData directly for that.)

These endpoints are not idempotent

Unlike every other write in this package, Wave does not offer an idempotency key here, so none is sent. create() consequently opts out of the retry policy: with nothing to make a retry safe, a retried create could produce a second merchant. update() and delete() stay retryable, being idempotent by HTTP semantics.

A duplicate name raises a ValidationException carrying ErrorCode::DuplicateAggregatedMerchantName.

Webhooks

Everything above asks Wave for state. Webhooks are how Wave tells you — and since a checkout's payment_status can sit at processing, the completion event is the primary success path, not an optional extra.

Verify, then read

fromRequest() verifies before it parses, so you cannot read a payload you haven't authenticated. A bad signature, a missing header, a replayed timestamp, or an unconfigured secret all raise InvalidWebhookSignatureException — reject with 403 and never act on it.

Pass the raw body. The signature covers the exact bytes Wave sent; decoding the JSON and re-encoding it changes whitespace and key order, and the digest stops matching. This is the single most common webhook bug, and there's a test pinning it. In Laravel that means $request->getContent(), never $request->all().

Laravel

Two things to set up alongside it, both of which will otherwise bite you:

The middleware fails closed: missing header, bad signature, or no configured secret all give a 403, never a fall-through to "accept unverified".

Events and payloads

Event type Typed payload
Checkout paid checkout.session.completed CheckoutEvent (full session)
Checkout failed checkout.session.payment_failed CheckoutEvent (partial)
B2B received b2b.payment_received B2BPaymentEvent
B2B failed b2b.payment_failed B2BPaymentEvent (Wave publishes no example)
Customer paid merchant.payment_received MerchantPaymentEvent
Portal test ping test.test_event $event->data (no payload)

CheckoutEvent is deliberately not the REST API's CheckoutSession, because the two aren't the same shape: completed carries a full session, but payment_failed carries only four fields and reports both statuses as "failed" — a value the Checkout API's own enum doesn't document. Feeding that to CheckoutSession would throw. Only id is required on CheckoutEvent; upgrade when the payload is complete:

An unknown event type is acknowledged, not rejected

Everywhere else in this package an unrecognised enum value throws. Here it must not: Wave redelivers any non-2xx for three days, so an event type added after your installed version would become a retry storm. $event->type is null for anything unrecognised and $event->rawType keeps the original string:

Delivery is best effort

Wave is explicit that events may be missed, duplicated, or arrive out of order, with retries for up to three days. The SDK can't fix that for you, so:

Verifying by hand

If you're not on Laravel, Support\Signature is public and handles rotation (Wave sends two v1= signatures while a secret is being rotated):

Amounts are strings

Wave transmits amounts as strings, and this package never converts one to a float — a single round-trip through a binary float turns "1234567.89" into a value that no longer equals itself.

Request DTOs validate their amount on construction, so a malformed value fails locally instead of costing a round-trip and a 400. XOF and UGX are zero-decimal and reject "10.50" outright.

Amounts you read follow a looser rule than amounts you send: a balance can be "0" and a transaction amount is a signed delta like "-99", both of which are rejected as request amounts. Amount::toMinorUnits() accepts them; Amount::validate() does not.

Error handling

Everything the SDK throws implements WaveException, so one catch covers the package:

HTTP failures are also typed by status — AuthenticationException (401), AuthorizationException (403), NotFoundException (404), ValidationException (400/422), IdempotencyException (409), RateLimitException (429), ServerException (5xx) — and each carries the Wave error code:

An error code this release does not know about leaves errorCode() null and keeps rawCode() intact, so a new Wave code never masks the underlying failure.

Idempotency and retries

Every POST carries an Idempotency-Key, generated per request or supplied by you:

The connector retries 3 times with exponential backoff, but only on connection failures, 429, and 5xx — a 400 or 422 fails identically on a second attempt, so retrying it just delays the error. The key is resolved once when the request object is constructed, so all attempts of a retried request share one key. Without that, three retries of one checkout would create three sessions.

Request signing

Set a signing secret and every request gains a Wave-Signature: t=…,v1=… header, computed over the exact bytes being transmitted:

The helper is public, and verification accepts multiple v1= values so a secret rotation does not drop traffic:

Pass the raw body. Decoding JSON and re-encoding it changes whitespace and key order, which changes the digest.

Pagination

Cursor pagination over Wave's page_info envelope, lazily:

Testing

The package is Saloon-native, so MockClient works as usual:

Running the package's own suite:

The suite is fully offline. A guarded smoke test can run against the real API — it creates a session and expires it, moving no money:

Notes on the API

License

MIT. See LICENSE.md.


All versions of wave with dependencies

PHP Build Version
Package Version
Requires php Version ^8.2
saloonphp/saloon Version ^4.0
saloonphp/pagination-plugin Version ^2.3
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package thales/wave contains the following files

Loading the files please wait ...