PHPackages                             paymos/php-sdk - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. paymos/php-sdk

ActiveLibrary[API Development](/categories/api)

paymos/php-sdk
==============

Paymos Merchant API SDK for PHP 7.4+

v1.3.2(3w ago)07MITPHPPHP &gt;=7.4CI passing

Since May 30Pushed 3w agoCompare

[ Source](https://github.com/Paymos-labs/php-sdk)[ Packagist](https://packagist.org/packages/paymos/php-sdk)[ Docs](https://paymos.io)[ RSS](/packages/paymos-php-sdk/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (3)Dependencies (2)Versions (10)Used By (0)

Paymos PHP SDK — stablecoin payments client for PHP
===================================================

[](#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.

This is the same SDK the [WooCommerce](https://github.com/paymos-labs/woocommerce), [WHMCS](https://github.com/paymos-labs/whmcs), and [OpenCart](https://github.com/paymos-labs/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](https://paymos.io/docs/server-sdks)
- API credentials: [app.paymos.io/developers/api](https://app.paymos.io/developers/api)
- Webhooks dashboard: [app.paymos.io/developers/webhooks](https://app.paymos.io/developers/webhooks)

[![PHP 7.4+](https://camo.githubusercontent.com/a9a3eaec9b418a7faf0a724e9ffb89aac3fde2c6623024db0b1857d3d5724b56/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d372e342532422d373737626234)](https://www.php.net/)[![License: MIT](https://camo.githubusercontent.com/d6bc2b26794002c24d023acaab01b6dbb953c57ab9cb80ba5b8aa2f2bd5de99a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d626c7565)](LICENSE)

---

What this is
------------

[](#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)
- Pluggable transport (cURL by default, mock for tests)
- Built-in retry with exponential backoff and `Retry-After` support (429 on any method; 5xx only on idempotent methods)
- Webhook signature verification with secret-rotation support, Stripe-style multi-signature grace period

---

Installation
------------

[](#installation)

```
composer require paymos/php-sdk
```

Or vendor the `src/` directory directly into a plugin (e.g. WooCommerce, OpenCart) and register `Paymos\` -&gt; `src/` with your autoloader.

---

Quick start
-----------

[](#quick-start)

### 1. Get your credentials

[](#1-get-your-credentials)

In the Paymos dashboard go to **Developers -&gt; API Keys**(`/developers/api`) and create an API credential. You will receive two strings:

FieldFormatNotes**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

[](#2-bootstrap-the-client)

```
use Paymos\Client;
use Paymos\ClientConfig;

$client = new Client(new ClientConfig(
    'pk_test_REPLACE_WITH_YOUR_KEY',     // API Key
    'sk_test_REPLACE_WITH_YOUR_SECRET',  // API Secret
    'https://api.paymos.io',             // Base URL (omit for default)
    30                                   // Request timeout (seconds)
));
```

### 3. First request

[](#3-first-request)

```
$balances = $client->balances()->get();
foreach ($balances as $b) {
    echo $b['currency'] . ': ' . $b['available'] . PHP_EOL;
}
```

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
--------

[](#invoices)

### Create a fiat-denominated invoice

[](#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):

```
use Paymos\IdempotencyKey;

$invoice = $client->invoices()->create(array(
    'project_id'         => 'prj_xxxxxxxxxxxx',
    'amount'             => '49.95',
    'currency'           => 'USD',                // fiat -> network unlocked
    'external_order_id'  => IdempotencyKey::externalOrderId('order'),
    // optional:
    // 'allow_multiple_payments' => false,
    // 'customer_fee_percent'    => 0,            // 0..100
    // 'client_id'               => 'cust_42',
));

echo $invoice['payment_url'];   // hosted invoice page
echo $invoice['invoice_id'];    // "inv_..."
```

### Create a crypto-locked invoice

[](#create-a-crypto-locked-invoice)

Pre-select both currency and network:

```
$invoice = $client->invoices()->create(array(
    'project_id'        => 'prj_xxxxxxxxxxxx',
    'amount'            => '10.00',
    'currency'          => 'USDT',
    'network'           => 'tron',                // network locked
    'external_order_id' => 'order-7f3c',
));
```

### Get an invoice

[](#get-an-invoice)

```
$invoice = $client->invoices()->get('inv_xxxxxxxxxxxx');
$status  = $invoice['status'];                    // "awaiting_client" | "confirming" | "paid" | ...
$paid    = $invoice['payment']['paid'] ?? null;   // string decimal, or null
```

### List invoices

[](#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.

```
$page = $client->invoices()->listPage(array(
    'project_id' => 'prj_xxxxxxxxxxxx',
    'status'     => array('paid', 'paid_over'),
    'limit'      => 50,
));

foreach ($client->invoices()->iterate(array('status' => array('paid')), 10) as $invoice) {
    echo $invoice['invoice_id'] . PHP_EOL;
}
```

The second argument to `iterate()` is a client-side maximum page count.

### Cancel an invoice

[](#cancel-an-invoice)

A non-empty reason (max 500 chars) is **required** by the server:

```
$client->invoices()->cancel('inv_xxxxxxxxxxxx', 'customer abandoned checkout');
```

### Sandbox: simulate a payment

[](#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:

StageResult`'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`)```
$client->invoices()->simulatePayment('inv_xxxxxxxxxxxx', 'paid');
```

---

Withdrawals
-----------

[](#withdrawals)

### Create a withdrawal

[](#create-a-withdrawal)

```
$wd = $client->withdrawals()->create(array(
    'destination_address' => 'TRX...whitelisted...address',
    'network'             => 'tron',
    'currency'            => 'USDT',
    'amount'              => '50.00',
    'external_order_id'   => 'payout_2026_05_01_001',
));
echo $wd['withdrawal_id'];  // "wdr_..."
```

The destination must already be on the merchant's whitelist (`/balance`) - the server returns `403 whitelist_required`otherwise.

### Get / cancel / simulate

[](#get--cancel--simulate)

```
$wd = $client->withdrawals()->get('wdr_xxxxxxxxxxxx');

$client->withdrawals()->cancel('wdr_xxxxxxxxxxxx', 'merchant requested');

// Sandbox only:
$client->withdrawals()->simulateCompletion('wdr_xxxxxxxxxxxx');
```

### List withdrawals

[](#list-withdrawals)

```
$page = $client->withdrawals()->listPage(array(
    'status' => array('created', 'pending_review'),
    'limit'  => 20,
));

foreach ($client->withdrawals()->iterate(array('status' => array('completed')), 10) as $withdrawal) {
    echo $withdrawal['withdrawal_id'] . PHP_EOL;
}
```

---

Idempotency
-----------

[](#idempotency)

Both `invoices.create` and `withdrawals.create` use the request's `external_order_id` as the idempotency key. 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:

```
use Paymos\IdempotencyKey;

$key = IdempotencyKey::externalOrderId('wc');   // "wc_550e8400-e29b-41d4-a716-446655440000"
```

---

Webhooks
--------

[](#webhooks)

Webhooks are configured at **Developers -&gt; 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

[](#wire-format)

The server delivers each event as:

```
POST
Content-Type: application/json
X-Webhook-Signature: t=,v1=[,v1=]

{
  "event_id":   "evt_...",
  "event_type": "invoice.paid",
  "version":    1,
  "occurred_at": 1709000000,
  "data":       { ... InvoiceStatusContract ... }
}

```

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

[](#verify-and-process)

```
use Paymos\Webhook\InMemoryEventStore;
use Paymos\Webhook\WebhookEventProcessor;
use Paymos\Webhook\WebhookVerifier;
use Paymos\Exception\DuplicateEventException;
use Paymos\Exception\SignatureMismatchException;
use Paymos\Exception\TimestampSkewException;

$verifier  = new WebhookVerifier('whsec_xxxxxxxxxxxx', 300);
$processor = new WebhookEventProcessor($verifier, new InMemoryEventStore());

$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$rawBody   = file_get_contents('php://input');

try {
    $event = $processor->process($signature, $rawBody);
    // $event === ['event_id' => '...', 'event_type' => '...', 'data' => [...], ...]
} catch (DuplicateEventException $e) {
    http_response_code(200);  // already processed - ack
    exit;
} catch (SignatureMismatchException $e) {
    http_response_code(401);
    exit;
} catch (TimestampSkewException $e) {
    http_response_code(401);
    exit;
}

// Map the event to a precise business action and update your order/payout state.
use Paymos\Plugin\StatusMapper;

if (strpos($event['event_type'], 'invoice.') === 0) {
    $action = StatusMapper::invoiceAction($event['event_type']);

    switch ($action) {
        case StatusMapper::ACTION_CONFIRMING:
            // On-chain transfer detected, waiting for confirmations.
            break;
        case StatusMapper::ACTION_AWAITING_PAYMENT:
            // Partial payment received, waiting for the rest.
            break;
        case StatusMapper::ACTION_PAYMENT_COMPLETE:
            // Terminal: invoice paid (or paid_over) - mark order paid, fulfil.
            break;
        case StatusMapper::ACTION_FAIL_ORDER:
            // Terminal: invoice underpaid past deadline.
            break;
        case StatusMapper::ACTION_CANCEL_ORDER:
            // Terminal: invoice expired or cancelled.
            break;
        case StatusMapper::ACTION_IGNORE:
            // Unrecognized / future invoice event - no state change.
            break;
    }
} else {
    $action = StatusMapper::withdrawalAction($event['event_type']);

    switch ($action) {
        case StatusMapper::ACTION_PROCESSING:
            // Withdrawal broadcast on-chain.
            break;
        case StatusMapper::ACTION_COMPLETED:
            // Terminal success.
            break;
        case StatusMapper::ACTION_FAILED:
            // Terminal failure - reversed back to balance.
            break;
        case StatusMapper::ACTION_CANCELLED:
            // Cancelled before broadcast.
            break;
        case StatusMapper::ACTION_IGNORE:
            // Informational event (withdrawal.created).
            break;
    }
}

http_response_code(200);
```

### Replace `InMemoryEventStore` in production

[](#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.

```
use Paymos\Webhook\EventStoreInterface;

final class WordPressEventStore implements EventStoreInterface
{
    public function remember($eventId, $ttlSeconds)
    {
        $key = 'paymos_evt_' . $eventId;
        if (get_transient($key)) {
            return false;
        }
        set_transient($key, 1, (int) $ttlSeconds);
        return true;
    }
}
```

---

StatusMapper
------------

[](#statusmapper)

`Paymos\Plugin\StatusMapper` maps webhook event types to plugin-side actions. It is a pure static helper and contains no I/O.

MethodReturns`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
--------------

[](#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()`:

```
{
  "type":   "https://paymos.io/docs/errors/codes#insufficient_balance",
  "title":  "Conflict",
  "status": 409,
  "detail": "Insufficient balance.",
  "code":   "insufficient_balance",
  "field":  null
}
```

**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:

```
{
  "type":   "about:blank",
  "status": 400,
  "title":  "Bad Request",
  "detail": "Validation failed.",
  "code":   "validation_failed",
  "errors": [
    { "code": "field_required", "field": "currency", "message": "Field is required." }
  ]
}
```

```
use Paymos\Exception\ApiException;
use Paymos\Exception\ConflictException;
use Paymos\Exception\GoneException;
use Paymos\Exception\NotFoundException;
use Paymos\Exception\RateLimitException;
use Paymos\Exception\UnavailableException;
use Paymos\Exception\ValidationException;

try {
    $client->invoices()->create($payload);
} catch (ValidationException $e) {
    foreach ($e->errors() as $err) {
        // $err = ['code' => '...', 'field' => '...|null', 'message' => '...']
    }
} catch (NotFoundException $e) {
    // 404
} catch (ConflictException $e) {
    // 409 - e.g. insufficient_balance. $e->errorCode() / $e->detail() (flat envelope).
} catch (GoneException $e) {
    // 410 - resource is in a terminal state (cancel after Paid, etc.)
} catch (RateLimitException $e) {
    // 429 - SDK retries automatically (any method); surfaces only after RetryPolicy
    // is exhausted. $e->retryAfterSeconds() gives the server's Retry-After hint.
} catch (UnavailableException $e) {
    // 503 - upstream / transient. Retried only on idempotent methods (GET/HEAD);
    // a 503 on a POST surfaces immediately (it may already have taken effect).
} catch (ApiException $e) {
    // any other API error
}
```

The HTTP status -&gt; exception class mapping (see `Paymos\Exception\ApiException::fromResponse`):

StatusClass400`ValidationException`401, 403`AuthException`404`NotFoundException`409`ConflictException`410`GoneException`429`RateLimitException`503`UnavailableException`Other 5xx`ServerException`Anything else`ApiException`### Retries

[](#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-idempotent `POST` (cancel / simulate) is **not** retried — it may already have taken effect server-side. Invoice/withdrawal creation is additionally idempotency-keyed by `external_order_id`.

Override by constructing the client with a custom transport:

```
use Paymos\Client;
use Paymos\ClientConfig;
use Paymos\Http\CurlTransport;
use Paymos\Http\RetryPolicy;
use Paymos\Http\RetryingTransport;

$client = new Client(
    new ClientConfig('pk_test_...', 'sk_test_...'),
    new RetryingTransport(new CurlTransport(), new RetryPolicy(/* maxRetries */ 4, /* baseMs */ 250))
);
```

---

How HMAC signing works
----------------------

[](#how-hmac-signing-works)

Every authenticated request carries two headers:

```
X-Request-Timestamp:
Authorization:       HMAC-SHA256 :

```

The signed payload is:

```
\n\n\n\n

```

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
-------

[](#testing)

The SDK ships with a tiny xUnit-style runner. To run the test suite against a clean PHP 7.4 image:

```
docker run --rm -v "$(pwd):/sdk" -w /sdk php:7.4-cli php tests/run.php
```

You can plug a `Paymos\Http\MockTransport` into the client to avoid real HTTP in your own tests:

```
use Paymos\Client;
use Paymos\ClientConfig;
use Paymos\Http\MockTransport;
use Paymos\Http\HttpResponse;

$transport = new MockTransport(array(
    new HttpResponse(200, '{"invoice_id":"inv_123","status":"awaiting_client"}', array()),
));
$client = new Client(new ClientConfig('pk_test_a', 'sk_test_b'), $transport);
$client->invoices()->get('inv_123');

print_r($transport->requests());  // captured method/url/headers/body
```

---

Compatibility
-------------

[](#compatibility)

ComponentVersionPHP7.4 - 8.3+Required extensionscurl, hash, jsonAPI 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
-------

[](#support)

- Documentation: [paymos.io/docs/quick-start](https://paymos.io/docs/quick-start)
- API credentials: [app.paymos.io/developers/api](https://app.paymos.io/developers/api)
- Webhooks dashboard: [app.paymos.io/developers/webhooks](https://app.paymos.io/developers/webhooks)
- Authentication deep-dive: [paymos.io/docs/authentication](https://paymos.io/docs/authentication)
- Webhook verification: [paymos.io/docs/webhooks/verify](https://paymos.io/docs/webhooks/verify)
- Webhook retry schedule: [paymos.io/docs/webhooks/retry](https://paymos.io/docs/webhooks/retry)
- Error catalog: [paymos.io/docs/errors](https://paymos.io/docs/errors)
- Sandbox guide: [paymos.io/docs/testing](https://paymos.io/docs/testing)
- Status: [paymos.io/status](https://paymos.io/status)
- Issues: [github.com/paymos-labs/php-sdk/issues](https://github.com/paymos-labs/php-sdk/issues)
- Email:

---

Changelog
---------

[](#changelog)

See [CHANGELOG.md](CHANGELOG.md) — or browse the public release history at [paymos.io/changelog](https://paymos.io/changelog).

---

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance95

Actively maintained with recent releases

Popularity6

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity40

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~8 days

Total

9

Last Release

23d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/294bb4360b2d5ec00a661244b3c19711566178b8e4f16bcd70423f9bddc30992?d=identicon)[paymos-dev](/maintainers/paymos-dev)

---

Tags

composercrypto-paymentspackagistpayment-gatewaypaymosphpsdkstablecoinusdcusdtapisdkcryptopaymentsUSDCUSDTstablecoinspaymos

###  Code Quality

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/paymos-php-sdk/health.svg)

```
[![Health](https://phpackages.com/badges/paymos-php-sdk/health.svg)](https://phpackages.com/packages/paymos-php-sdk)
```

###  Alternatives

[transbank/transbank-sdk

Transbank SDK

62722.8k15](/packages/transbank-transbank-sdk)[manamine/php-eos-rpc-sdk

PHP SDK for the EOS RPC API

187.5k](/packages/manamine-php-eos-rpc-sdk)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
