Download the PHP package paymos/php-sdk without Composer
On this page you can find all versions of the php package paymos/php-sdk. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download paymos/php-sdk
More information about paymos/php-sdk
Files in paymos/php-sdk
Package php-sdk
Short Description Paymos Merchant API SDK for PHP 7.4+
License MIT
Homepage https://paymos.io
Informations about the package php-sdk
Paymos PHP SDK — stablecoin payments client for PHP
Official PHP SDK for the Paymos Merchant API. Accept USDT (11 chains: Tron, Ethereum, BSC, Polygon, Arbitrum, Optimism, TON, Avalanche, Solana, NEAR, Plasma) and USDC (10 chains: Ethereum, BSC, Polygon, Arbitrum, Optimism, Base, Avalanche, Solana, NEAR, Sui) — native settlement, no auto-conversion.
Give each customer a static wallet — one permanent deposit address per network that never changes — and read every confirmed deposit from a resumable feed.
This is the same SDK the WooCommerce, WHMCS, and OpenCart plugins use under the hood. Drop it into a custom PHP backend and you get the same HMAC signing, webhook verification, and retry logic the official plugins ship.
- Documentation: paymos.io/docs/server-sdks
- API credentials: app.paymos.io/developers/api
- Webhooks dashboard: app.paymos.io/developers/webhooks
What this is
A thin, dependency-free client for the public Paymos Merchant API (HMAC-SHA256 authentication, snake_case JSON, webhook signature verification).
- PHP 7.4 / 8.x compatible
- No Composer runtime dependencies (uses ext-curl, ext-hash, ext-json, ext-openssl)
- Pluggable transport (cURL by default, mock for tests)
- Built-in retry with exponential backoff and
Retry-Aftersupport (429 on any method; 5xx only on idempotent methods) - Webhook signature verification with secret-rotation support, Stripe-style multi-signature grace period
Installation
Or vendor the src/ directory directly into a plugin (e.g. WooCommerce,
OpenCart) and register Paymos\ -> src/ with your autoloader.
Quick start
1. Get your credentials
In the Paymos dashboard go to Developers -> API Keys
(/developers/api) and create an API credential. You will
receive two strings:
| Field | Format | Notes |
|---|---|---|
| API Key | pk_test_... / pk_live_... (Payment) |
Sent in the Authorization header |
rk_test_... / rk_live_... (Payout) |
||
| API Secret | sk_test_... / sk_live_... |
Used to compute the HMAC signature. |
| Never sent over the wire. |
The _test_ / _live_ segment identifies the environment - there is no
separate X-Environment header. Sandbox-only endpoints under
/v1/sandbox/... reject _live_ keys with HTTP 403.
2. Bootstrap the client
3. First request
Each entry is one coin's balance totalled across every network — a merchant's balance is network-agnostic; the network is chosen only at withdrawal time.
Invoices
Create a fiat-denominated invoice
The customer pays the displayed crypto amount (the network is selected on the hosted invoice page if not pre-locked):
Create a crypto-locked invoice
Pre-select both currency and network:
Get an invoice
List invoices
Fetch one page, or lazily follow the opaque cursor. Repeatable filters such as
status are passed as arrays; query keys are deterministically RFC3986-encoded
and the exact query string is covered by HMAC.
The second argument to iterate() is a client-side maximum page count.
Cancel an invoice
A non-empty reason (max 500 chars) is required by the server:
Sandbox: simulate a payment
In sandbox you can drive an invoice to a terminal state without any real
on-chain activity. This call requires a pk_test_... / rk_test_... key.
simulatePayment takes a stage string — the server computes the amount:
| Stage | Result |
|---|---|
'paid' |
invoice fully paid (invoice.paid) |
'overpaid' |
invoice paid above the requested amount (invoice.paid_over) |
'underpay' |
partial payment, then final underpayment (invoice.underpaid) |
'cancel' |
invoice cancelled (invoice.cancelled) |
Withdrawals
Create a withdrawal
The destination must already be on the merchant's whitelist
(/balance) - the server returns 403 whitelist_required
otherwise.
Get / cancel / simulate
List withdrawals
Payment channels
A payment channel is one payer's own reusable set of deposit addresses — no per-order invoice, no expiry. The payer keeps the address; every confirmed deposit shows up in the feed.
Create a channel and read its rails
Poll the deposit feed
Four things that bite an integrator who guesses:
- Repeating the same
external_idreturns the same channel. The pair (project_id,external_id) is the idempotency key: a repeat answers200with the channel that already exists instead of201with a duplicate. Both responses are a channel, so you never inspect the status code — callingcreate()on every checkout is safe, not a duplicate and not an error. - A rail's
addressis absent until that rail finishes provisioning, and never changes once it appears. Absent means "not yet", not "no address" — show the payer only the rails that already have one. - An absent
minimum_depositmeans "we cannot quote a minimum right now", not "there is no minimum". Treating absent as0is how a merchant accepts a deposit that lands below the live minimum and is never credited. next_cursoris never empty — not even on a page with no items. Store it and resume from it on the next poll. Do not loop until it is null the wayinvoices()->iterate()ends a list: the feed has no last page, so that loop either spins forever or, worse, stops on the first quiet page and leaves the merchant's reconciliation silently behind.confirmed_fromis only the first poll's lower bound — after that the stored cursor is the resume mechanism.
readPage() deliberately has no iterate() companion for exactly that reason.
$feed['blocked'] appears only when one position could not be rendered; it
carries {deposit_id, reason} and explains a stall rather than causing one, so
log it — without it, being stuck looks exactly like a quiet day.
Block, unblock, simulate
Idempotency
invoices.create and withdrawals.create use the request's
external_order_id as the idempotency key; paymentChannels.create uses
(project_id, external_id) the same way. Calling the same endpoint
again with the same external_order_id returns the existing resource
instead of creating a duplicate. Use IdempotencyKey::externalOrderId('prefix')
to mint a UUID-v4 backed key:
Webhooks
Webhooks are configured at Developers -> Webhooks
(/developers/webhooks). The dashboard generates a
whsec_... secret, supports rotation with a grace period, and shows
a delivery log + manual replay for each event.
Wire format
The server delivers each event as:
Multiple v1= entries appear during the secret-rotation grace period
(Stripe pattern). The SDK accepts the message if any of them validates.
Verify and process
Replace InMemoryEventStore in production
InMemoryEventStore resets on every PHP request - it is only
useful inside one CLI process or for tests. In a real plugin (Laravel /
WordPress / Symfony) implement EventStoreInterface against your
database, Redis, or filesystem cache so event_id deduplication works
across requests.
Payment-channel deposit events
The three payment_channel.deposit.* events each carry the full deposit shape
(the same snake_case array paymentChannelDeposits()->get(...) already
returns) as data:
confirming and reorged are advisory and may arrive out of order. A
confirming can land after the confirmed for the same deposit, and a
reorged can be superseded by a later confirmed. Credit only on
payment_channel.deposit.confirmed with is_final true, and never let an
advisory event regress a deposit you already know is confirmed. Crediting on
confirming releases goods against money a reorg can still take back.
StatusMapper
Paymos\Plugin\StatusMapper maps webhook event types to plugin-side
actions. It is a pure static helper and contains no I/O.
| Method | Returns |
|---|---|
invoiceAction($eventType, $status = null) |
ACTION_CONFIRMING / ACTION_AWAITING_PAYMENT / ACTION_PAYMENT_COMPLETE / ACTION_FAIL_ORDER / ACTION_CANCEL_ORDER / ACTION_IGNORE |
withdrawalAction($eventType, $status = null) |
ACTION_PROCESSING / ACTION_COMPLETED / ACTION_FAILED / ACTION_CANCELLED / ACTION_IGNORE |
paymentAction($eventType, $status = null) |
Legacy coarse mapper — collapses all mid-flight invoice events to ACTION_PROCESSING. Prefer invoiceAction() for new code. |
Pass $eventType from the webhook payload's event_type field. The
optional $status is a fallback for legacy callers that only have the
invoice/withdrawal status string.
Error handling
Every non-2xx response raises Paymos\Exception\ApiException (or a
subclass). The server uses RFC 9457 "Problem Details" in two shapes.
A single error is flat — code/field/detail live at the top level,
read them with errorCode(), field() and detail():
Multiple validation errors add an errors[] breakdown — iterate
errors() for fields. errorCode() and field() always read the top-level
request error; they never promote the first field entry:
The HTTP status -> exception class mapping (see
Paymos\Exception\ApiException::fromResponse):
| Status | Class |
|---|---|
| 400 | ValidationException |
| 401, 403 | AuthException |
| 404 | NotFoundException |
| 409 | ConflictException |
| 410 | GoneException |
| 429 | RateLimitException |
| 503 | UnavailableException |
| Other 5xx | ServerException |
| Anything else | ApiException |
Retries
RetryingTransport retries with exponential backoff (default: 2 retries,
150 ms base), honoring the server's Retry-After header when it asks for
longer than the computed backoff. Retry safety is method-aware:
- 429 is retried for any method — rate limiting happens before the request is processed, so no side effect occurred.
- 5xx is retried only for idempotent methods (
GET/HEAD/OPTIONS). A 5xx on a non-idempotentPOST(cancel / simulate) is not retried — it may already have taken effect server-side. Invoice/withdrawal creation is additionally idempotency-keyed byexternal_order_id.
Override by constructing the client with a custom transport:
How HMAC signing works
Every authenticated request carries two headers:
The signed payload is:
where bodyHash is the lowercase hex of sha256(body) (or the empty
string for requests without a body), and the signature is
base64(HMAC-SHA256(secret, payload)).
Anti-replay: the server rejects requests whose timestamp is more than five minutes off its own clock - keep the host clock NTP-synced.
The SDK does this for you in Paymos\Http\RequestSigner and
Paymos\Resources\BaseResource::requestJson. You should not need to
sign requests by hand, but the helpers are public so you can build
ad-hoc tooling against the same scheme.
Testing
The SDK ships with a tiny xUnit-style runner. To run the test suite against a clean PHP 7.4 image:
You can plug a Paymos\Http\MockTransport into the client to avoid
real HTTP in your own tests:
Compatibility
| Component | Version |
|---|---|
| PHP | 7.4 - 8.3+ |
| Required extensions | curl, hash, json, openssl |
| API surface | /v1/* |
The SDK uses no language features beyond PHP 7.4 syntax so it can be vendored into legacy WooCommerce / OpenCart deployments without changes.
Support
- Documentation: paymos.io/docs/quick-start
- API credentials: app.paymos.io/developers/api
- Webhooks dashboard: app.paymos.io/developers/webhooks
- Authentication deep-dive: paymos.io/docs/authentication
- Webhook verification: paymos.io/docs/webhooks/verify
- Webhook retry schedule: paymos.io/docs/webhooks/retry
- Error catalog: paymos.io/docs/errors
- Sandbox guide: paymos.io/docs/testing
- Status: paymos.io/status
- Issues: github.com/paymos-labs/php-sdk/issues
- Email: [email protected]
Changelog
See paymos.io/changelog.
License
MIT — see LICENSE.
All versions of php-sdk with dependencies
ext-curl Version *
ext-hash Version *
ext-json Version *
ext-openssl Version *