# SMM Africa V3 clients 3.3.0

Production: `https://smm.africa/api/v3`. Use Node 22+ or PHP 8.1+ with cURL.
Download `node-client.mjs` or `V3Client.php` beside this guide. Use API keys only
on your server. Keep a persistent, private state directory per account. Never
delete operation files or invent another operation ID to force a retry.

## First order

1. Read `balance` and `services`.
2. Select a service and inspect its type, min/max, rate, and billing unit.
3. Persist a business operation ID before submitting the purchase.
4. Submit using that operation ID and an optional `max_charge` ceiling in USD.
5. Save the returned order ID. `queued: true` means accepted, not fulfilled.
6. Poll status. Never repeat add because fulfillment is delayed or status is stale.

```js
import { V3Client } from './node-client.mjs';
const api = new V3Client({ url: process.env.SMM_API_URL,
  key: process.env.SMM_API_KEY, stateDirectory: './private-state' });
const balance = await api.request({action:'balance'});
const services = await api.request({action:'services'});
const service = services.find(s => s.type.toLowerCase() === 'default' && Number(s.min) <= 500 && Number(s.max) >= 500);
if (!service) throw new Error('Select an available service and quantity.');
const order = await api.request({action:'add', service:service.service,
  quantity:500, link:'https://example.com/post/123', max_charge:5},
  {operationId:'your-persisted-checkout-id'});
const status = await api.request({action:'status', orders:[String(order.order)]});
// Inspect each status.orders item's http_status, freshness, and retry_after.
```

```php
require 'V3Client.php';
$api = new V3Client(getenv('SMM_API_URL'), getenv('SMM_API_KEY'), '/private/api-state');
$balance = $api->request(['action'=>'balance']);
$order = $api->request(['action'=>'add', 'service'=>'YOUR_SERVICE_ID',
  'quantity'=>500, 'link'=>'https://example.com/post/123', 'max_charge'=>5],
  'your-persisted-checkout-id');
$status = $api->request(['action'=>'status', 'orders'=>[(string)$order['order']]]);
```

## Errors and retries

Log `code`, `request_id` / `X-Request-Id`, and HTTP status. Keep error details out
of customer messages. The clients honor retryable errors and Retry-After, add
jitter to fallback backoff, and save successful mutations across restarts.

| Condition | Action |
| --- | --- |
| Timeout / lost response | Retry identical payload with the same operation ID/key. |
| REQUEST_IN_PROGRESS | Wait for Retry-After; same payload and key. |
| RATE_LIMITED | Honor Retry-After; bulk items use retry_after individually. |
| IDEMPOTENCY_CONFLICT | Restore the original intent; reconcile before changing keys. |
| ACTION_OUTCOME_UNCONFIRMED | Stop. Ask support to verify the supplier outcome. |
| PRICE_LIMIT_EXCEEDED | No order/debit committed. Refresh pricing before approving a higher ceiling. |
| Insufficient balance | Fund the wallet; resume a paused subscription explicitly. |

Server add replay expires after 24 hours. Clients refuse unresolved mutations
after 23 hours, leaving time for reconciliation. Do not reuse expired operations
as new purchases. A successful cached client response is still returned later.
After a confirmed PRICE_LIMIT_EXCEEDED rejection, a newly approved purchase
attempt may use a new operation ID and a higher ceiling. This exception never
applies to a timeout or unconfirmed outcome.
If Retry-After exceeds five minutes, the clients stop automatic retry and ask
the caller to resume later rather than retrying before the server permits it.

## Managed subscriptions

Create with `subscription_create`, `service`, `quantity`, `currency: "USD"`,
`spending_cap`, `max_posts` (1–10000), and a persistent operation ID. No debit
occurs on creation. Submit discovered URLs using `subscription_post`; the
service/quantity are fixed but each post uses current pricing. Use `max_charge`
per post. URL fragments are ignored; query parameters and percent escapes matter.
Repeated canonical URLs return the original order even after cancellation.

`subscription_status` returns up to 20 receipts. Pass `after: next_cursor` until
next_cursor is null. Poll their order IDs in batches of 20. Pause/resume/cancel
control future posts; they do not cancel existing orders. Spending caps count
gross charges; refunds do not replenish capacity. Native supplier subscriptions
and drip-feed runs/interval remain unsupported.

## Local sandbox

Run `node sandbox.mjs`, then use `http://127.0.0.1:4013/api/v3` and
`sandbox_test_key`. The simulator binds loopback only, starts with USD 100,
supports JSON and URL-encoded forms, and resets on restart. It never imports
application code, accesses a database, or contacts suppliers. Never use live keys.
Service 1 costs USD 1 per 1,000 units; quantity is 1–100000.

Set `sandbox_scenario` on add to `completed` (default), `queued`, `partial`,
`stale`, `insufficient_balance`, or `ambiguous_cancel`. The latter returns
ACTION_OUTCOME_UNCONFIRMED when canceled, including when retried with another key.
`rate_limited` returns HTTP 429 and Retry-After. Standard orders become processing
on the first status call and completed/partial on the next. Idempotency conflicts,
duplicate posts, caps, balance exhaustion, and cancellation can be tested locally.
This is a deterministic integration simulator, not proof of supplier behavior;
it does not simulate refunds for partial fulfillment, multipart, or webhook delivery.
Webhook transport is verified separately with the transactional outbox tests.

## Signed order webhooks

Available on main-account V3 when enabled by the operator. One receiver per
account, covering order activity from API and website orders. Configure using
`webhook_set`, a public HTTPS URL on port 443, and a random 32–128 character
base64url `secret`. Generate one with `openssl rand -base64 32` converted to
base64url, or Node `randomBytes(32).toString('base64url')`. Never reuse an API key.

Before saving, the API sends `{ "type":"webhook.verify", "challenge":"..." }`.
Verify its signature and return JSON with the identical challenge. Invalid,
private-network, redirecting, or unverified destinations cannot be registered.
Signing secrets are encrypted at rest and are never returned by status.

`X-SMM-Signature: t=UNIX_SECONDS,v1=HEX_HMAC_SHA256` signs the exact bytes of
`timestamp + "." + rawBody`. The downloadable `webhook-receiver.mjs` verifies
the MAC in constant time and rejects timestamps outside a five-minute window.
Synchronize your clock. Verify before parsing JSON.

Events contain `id`, `type: "order.updated"`, `api_version`, `created_at`, and
`data: {order,status}`. Store each ID uniquely and enqueue work durably before
acknowledging with 2xx. Delivery is at least once and may be out of order. Query
status for the current order state and refund amounts; retain bulk reconciliation.

Use `webhook_status`, `webhook_disable`, and `webhook_deliveries` (20 per page;
`after=next_cursor`). History includes payload, state, attempts, last HTTP status,
and next attempt time. Delivery retries up to eight times. `webhook_replay` with
`event` requeues a delivered/failed event for the same endpoint version and keeps
its event ID. Reconfiguring invalidates old pending deliveries; an in-flight
request may finish. History is retained 30 days. Management set/replay calls are
limited to five per minute per account, in addition to normal API limits.

## Compatibility and releases

3.3 adds price ceilings, signed webhooks, complete documentation, and the local
sandbox. API authentication, response ID types, and existing monetary types stay
compatible. Store IDs as opaque strings. Balance/rates use decimal strings where
specified; charged/charge are JSON numbers with six-decimal accounting precision.
Rates are per 1,000 quantity units or per package depending on service type.
Ignore unknown response fields; handle unknown errors conservatively.

The canonical clients are in examples/v3; public downloads are generated using
`node scripts/sync-v3-artifacts.mjs`. CI checks them with `--check`.
The earlier `v3-reseller-client.mjs` remains a legacy one-shot compatibility
download. New integrations should use the versioned Node/PHP clients above.
