Developers
PoweX Pay API
Accept crypto payments on your site, app or chat. Create an invoice or a payment link, send the customer to a hosted checkout, and get a signed webhook the moment the money lands — credited, minus the fee, straight into your PoweX wallet.
How it works
- Create an invoice (or share a payment link) with your server and your API key.
- Send the customer to
checkoutUrl. They pick a coin and network, scan the QR code and pay; the price in that coin is locked for a few minutes. - We watch the chain. When the payment is confirmed it is credited to you and your webhook fires:
invoice.payment_received, theninvoice.paid. - You fulfil the order. The net amount is already in your PoweX wallet, in the same coin the customer paid with. Swap it or withdraw it whenever you like.
Amounts are priced in USD. Customers pay in the coin they choose; you are never exposed to a price move after the checkout locks the quote.
Quickstart
1. Enable PoweX Pay in the dashboard (new here? Create a merchant account) and create a test key (pxk_test_…). Test mode moves no money and never touches the blockchain.
2. Create an invoice:
curl https://powex.me/v1/invoices \
-H "Authorization: Bearer pxk_test_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042" \
-d '{
"amount": "25.00",
"reference": "order-1042",
"description": "T-shirt",
"returnUrl": "https://shop.example.com/thanks"
}'Response (201):
{
"id": "inv_Qm8x2VtFhKz0aBcDeFgHiJ",
"object": "invoice",
"livemode": false,
"status": "OPEN",
"amount": "25",
"currency": "USD",
"reference": "order-1042",
"description": "T-shirt",
"feeBps": 50,
"checkoutUrl": "https://powex.me/i/inv_Qm8x2VtFhKz0aBcDeFgHiJ",
"expiresAt": "2026-10-21T17:20:00.000Z",
"paidUsd": "0",
"remainingUsd": "25",
"payment": null,
"payments": []
}3. Open checkoutUrl to see the customer's checkout, then pretend they paid:
curl -X POST https://powex.me/v1/invoices/inv_…/simulate \
-H "Authorization: Bearer pxk_test_…" \
-H "Content-Type: application/json" \
-d '{ "outcome": "paid" }'The invoice becomes PAID, a payment appears in your statement, and — if you registered a webhook — your endpoint receives the events.
Authentication
Send your key as Authorization: Bearer pxk_… (or X-Api-Key: pxk_…). Keys are created in the dashboard, shown once, and can be revoked at any time. Keep them on your server — never in a browser or mobile app.
A key is either test (pxk_test_…) or live (pxk_live_…); the mode comes from the key, never from the request. Live keys need an approved merchant account. Each key also carries scopes — give an integration only what it needs:
| Scope | Allows |
|---|---|
| invoices:read | Read invoices, payments, summary and payment links. |
| invoices:write | Create and cancel invoices; simulate outcomes in test mode. |
| links:write | Create, update and delete payment links. |
| webhooks:manage | Create, change, rotate and test webhook endpoints; read the delivery log. |
| payouts:read | Read payouts and your wallet balance. |
| payouts:write | Send payouts from your wallet. Moves money: give it to as few keys as possible. |
| addresses:read | Read your customers' fixed deposit addresses and the deposits they receive. |
| addresses:write | Create fixed deposit addresses for your customers. |
| refunds:write | Refund balance payments back to the customer, from your wallet. |
| customers:read | Read your customers (name, email, phone, masked tax id). |
| customers:write | Create and edit customers. Personal data: give it only to the systems that need it. |
| subscriptions:read | Read subscription plans and subscriptions. |
| subscriptions:write | Create and archive plans, cancel subscriptions; simulate subscriptions in test mode. |
| accounts:read | Platforms: list and read your subaccounts. |
| accounts:write | Platforms: create subaccounts and edit them. |
| accounts:act | Platforms: act on behalf of a subaccount with the PoweX-Account header (each call still needs its own scope). |
| transfers:read | Platforms: list and read transfers between your wallet and your subaccounts. |
| transfers:write | Platforms: move money between your wallet and your subaccounts. Moves money: give it to as few keys as possible. |
Lock a key to your servers. In the dashboard, give each key the IPs or ranges it may be used from (203.0.113.7, 198.51.100.0/24, IPv6 too; up to 20). A leaked key then does not work anywhere else — the call is refused with API_KEY_IP_NOT_ALLOWED. Changing the list needs your PIN and takes effect on the next request. Strongly recommended for live keys with payouts:write or transfers:write.
GET /v1/me returns the merchant, the key's mode and its scopes — handy as a connection test.
Conventions
- Base URL:
https://powex.me— JSON in, JSON out, TLS only. - Amounts are strings in canonical decimal form (
"25","10.5","0.125"). Never parse money with floating point; use a decimal type. - Dates are ISO 8601 in UTC.
- Errors share one shape and a stable
code— branch on it, never onmessage:
{
"statusCode": 409,
"code": "INVOICE_NOT_OPEN",
"message": "Only an open invoice with no payments can be cancelled",
"requestId": "7c1d…"
}- Pagination: list endpoints take
limit(1–100, default 25) andstartingAfter(theidof the last item you got) and returnhasMore. Results are newest first. - Idempotency: send
Idempotency-Key(any unique string, up to 200 characters) when creating an invoice. Repeating the call with the same key and body returns the original invoice instead of a second one; the same key with a different body is a409. Keys are remembered for 7 days. Use your order id. - Rate limit: 300 calls per minute per key. Above that you get
429with aRetry-Afterheader.
Invoices
An invoice is one request for payment of a fixed amount. Price it in USD, or in a local currency (BRL, EUR, GBP, JPY, CNY) with currency: the exchange rate is fetched and locked when the invoice is created and you receive the USD equivalent (amountUsd, rounded to the cent). Without a recent rate the request fails with CURRENCY_UNAVAILABLE: retry, or price in USD. Instead of one amount you can send items (up to 50 lines), a discount or one of your coupon codes, and shipping: the total is always recomputed on our side, and an amount that does not match is refused. Its lifecycle:
| Status | Meaning |
|---|---|
| OPEN | Waiting for payment. |
| PAID | Paid in full (a small tolerance, 0.5% by default, counts as full). |
| UNDERPAID | Closed with only part of the amount received. What arrived is already credited to you. |
| EXPIRED | Closed without any payment. |
| CANCELLED | You cancelled it (only possible while open and with no payment). |
- An invoice is valid for 60 minutes unless you set
expiresInMinutes(5 to 10,080). - Pass
method({ "asset": "USDT", "network": "TRC20" }) to skip the coin picker: the checkout opens straight on the QR code. referenceis your order number;metadataholds up to 20 simple key/values that come back on every read and webhook.- The fee is fixed when the invoice is created (
feeBps): changing your rate later never affects an open invoice. - Late payments: if money arrives after the invoice closed, or in another coin than chosen, it is still credited to you and flagged
late: true— you decide what to do (refund, ship, ignore). - Do not treat a visit to your
returnUrlas proof of payment. Confirm with the webhook, or by retrieving the invoice. - Send it by e-mail:
POST /v1/invoices/:id/sendmails the invoice (PDF attached, with the payment link) totoor to the invoicecustomerEmail. In test mode nothing is sent (simulated: true). Up to 10 per minute per key and 100 per day.
Payment links
A payment link is a reusable page — https://powex.me/l/pl_… — for people who sell without a site: put it in a message, a bio or a printed QR code. Each time someone opens it and pays, a new invoice is created from the link (amount, validity and return URL come from the link), so every sale has its own checkout and webhook events. Invoices opened from a link carry reference: "link:pl_…".
curl https://powex.me/v1/payment_links \
-H "Authorization: Bearer pxk_test_…" \
-H "Content-Type: application/json" \
-d '{ "title": "Online course", "amount": "49.90", "expiresInMinutes": 60 }'Turning a link off (active: false) stops new payments but leaves invoices already opened payable. Changing the amount affects only invoices opened afterwards. The statement can be filtered by link, and each link shows how many invoices were opened and paid.
Payments & reconciliation
Every credited payment is a row you can list with GET /v1/payments: which invoice, order reference and payment link it came from, the coin and network the customer used, the value in USD at that moment, the fee, the net credited to your wallet and the blockchain transaction hash. GET /v1/summary gives totals, a per-day series, a per-coin split and your conversion rate. The same data is in the dashboard's Statement, with a CSV export.
In the wallet, each payment appears in History as “PoweX Pay payment” with the invoice, order and link it came from — so you can always tell where money came from.
Payouts (paying out)
Your PoweX wallet is a normal wallet, so the money you receive can also be sent on by API: pay a supplier, an affiliate or a customer refund to another PoweX account (recipient: "PWX-123456") or to an address on a blockchain (destination). Check what is available with GET /v1/balance.
curl https://powex.me/v1/payouts \
-H "Authorization: Bearer pxk_test_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: payout-2026-10-week-42" \
-d '{
"asset": "USDT",
"amount": "25.00",
"recipient": "PWX-123456",
"reference": "payout-2026-10-week-42",
"description": "Weekly payout"
}'Safety model
- Off by default. PoweX enables payouts per merchant after review and sets two ceilings: a maximum per payout and a maximum per 24 hours (in USD). Without that you get
PAYOUTS_NOT_ENABLED. - Its own scope.
payouts:writeis separate from everything else; a leaked invoice key cannot move money. - Same checks as the app. Balance, the account's daily limit, compliance screening and your address whitelist all apply. Above the amount at which the app asks for a second factor, the API call is refused — make that payment in the app.
Idempotency-Keyis required. Repeating a request with the same key returns the same payout and never pays twice.- Approval for large payouts (optional). In PoweX Pay → Payouts you can set an amount in USD above which a live payout — from your account or any subaccount — is created with
status: "AWAITING_APPROVAL"and nothing moves until someone approves it in the dashboard with the PIN. You then getpayout.paid/payout.failedas usual. Rejected, or not decided within 72 h, it becomesFAILED. While it waits it counts toward your daily limit. In a batch, such an item comes back asokwith that status. - Destination allowlist (recommended). In PoweX Pay → Payouts you can turn on a list of allowed destinations (addresses, with network and memo, or
PWX-accounts). While it is on, live payouts — from your account and from all your subaccounts — only go to destinations on it; anything else is refused withPAYOUT_DESTINATION_NOT_ALLOWED. A new destination can receive only 24 h after it is added, turning the list off only takes effect 24 h after the request, and each of these sends you a security alert by Telegram and e-mail — so a leaked key or a stolen session cannot drain the account before you notice. Adding a destination and turning the list off need your PIN; turning it on and removing a destination are immediate. If a destination has a memo, the payout must use the same memo. Test mode is not checked against the list. - Travel Rule. At or above the threshold PoweX applies (it depends on the jurisdiction), a live payout to a blockchain address carries who sends and who receives. The sender is your business profile (KYB) — fill it in Settings. The receiver goes in
beneficiary(name,walletType:SELF_HOSTEDorVASPwithvaspName, optionalcountryandpersonType:INDIVIDUALorCOMPANY) or, once, on the allowlist destination — then payouts to that address need nothing extra. Missing data is refused withTRAVEL_RULE_INFO_REQUIREDbefore anything moves. Payouts toPWX-accounts and test mode are not affected. - The recipient is only ever a
PWX-account number — never an e-mail or a username — so the API cannot be used to find out who has an account. - You are told by webhook:
payout.paid/payout.failed. A payout inREVIEWwas interrupted mid-way and PoweX is checking it; it still counts toward your daily limit.
In test mode nothing moves: the payout is returned as PAID so you can build your flow. Everything is listed in Payouts in your dashboard, with your limits and how much of today's ceiling you have used.
Customer addresses
Building a platform, a game or a marketplace? Give each of your customers a permanent deposit address. Whatever arrives there is credited — minus your regular fee — to your own PoweX wallet, and you are told who it was for by the deposit.received webhook, which carries the customerRef you chose. You keep full control of the funds: they are in your wallet, where you can hold, convert, send or withdraw them.
curl https://powex.me/v1/deposit_addresses \
-H "Authorization: Bearer pxk_test_…" \
-H "Content-Type: application/json" \
-d '{
"customerRef": "user_8421",
"asset": "USDT",
"network": "TRC20"
}'- Same customer, same address. Asking again with the same
customerRef, coin and network always returns the same address. - Your id, not ours.
customerRefis any id you use (1–100 characters). It comes back on every deposit and webhook. - Switched on by PoweX. Each customer address has to be watched and swept, so PoweX enables it per merchant and sets how many customers you can have. Without it you get
CUSTOMER_ADDRESSES_NOT_ENABLED. Test mode works without it. - Test it end to end. In test mode addresses are fake (
test_…) andPOST /v1/deposit_addresses/:id/simulate_depositfires the webhook as in production — nothing moves. - List them with
GET /v1/deposit_addressesand every deposit withGET /v1/customer_deposits(filter bycustomerRef). See them in the dashboard's Customers tab. - A merchant account that is not active does not get new credits: the deposit waits in the customer account until PoweX reviews it. Nothing is lost.
Customers
Keep who pays you in one place, like a CRM: POST /v1/customers (scope customers:write) with name, email, phone (E.164) and a document from any country — BR_CPF, AR_CUIT, MX_RFC… validated with the official check digits and never returned in full (taxIdMasked).
refis your id for the customer, unique per mode (CUSTOMER_REF_TAKENpoints to the existing one). The samerefis used by fixed deposit addresses, so a customer and their addresses are one record.- Issue an invoice to a customer with
customer: "cus_…"— their email fillscustomerEmail— and list them withGET /v1/invoices?customer=cus_…. PATCH /v1/customers/:idchanges only what you send;nullclears a field. Find customers with?email=or?ref=.
Subscriptions
Charge a customer every day, week or month from their PoweX balance. You create the plan; the customer subscribes on the plan page with PIN or biometrics and pays the first cycle on the spot. After that PoweX charges each cycle automatically, at the price and in the coin the customer authorized.
POST /v1/subscription_plans(scopesubscriptions:write) returns the plan with itsurl. Send your customer there, adding?ref=<your id>: it comes back asreferenceon the subscription and on everysubscription.*webhook.- Price changes need a new authorization. Archiving a plan (
PATCH /v1/subscription_plans/:idwithisActive: false) removes its page; current subscribers keep their price and cycle. To change the price, create a new plan. - Follow them with
GET /v1/subscriptions(filter bystatus,planorreference) and stop one withPOST /v1/subscriptions/:id/cancel— idempotent, and it never needs the customer. - Failed charges are retried 1, 3 and 5 days later (
PAST_DUE, the customer is notified); the 4th failure cancels the subscription. - Test it end to end. With a test key, plans have no page; use
POST /v1/subscription_plans/:id/simulate_subscriptionand thenPOST /v1/subscriptions/:id/simulatewithpaidorfailed— the webhooks fire exactly as in production, nothing moves. - Platforms can do all of this for a subaccount with the
PoweX-Accountheader.
Subaccounts (platforms)
Running a SaaS, a marketplace or a payment service for other businesses? Open a PoweX Pay account for each of your customers. Every subaccount has its own balance, invoices, payment links and limits — the money of one customer never mixes with another's or with yours — and you operate all of them with your own key.
# 1. Open an account for one of your customers (live ones start PENDING_REVIEW)
curl https://powex.me/v1/accounts \
-H "Authorization: Bearer pxk_test_…" \
-H "Idempotency-Key: onboard-store-8421" \
-H "Content-Type: application/json" \
-d '{
"name": "Ana Coffee",
"externalRef": "store_8421",
"business": {
"type": "COMPANY",
"legalName": "Ana Coffee Ltda",
"taxCountry": "BR",
"taxId": "12.ABC.345/01DE-35",
"email": "finance@anacoffee.com",
"address": { "line": "Rua A, 100", "city": "São Paulo", "postalCode": "01001-000", "country": "BR" }
}
}'
# 2. Charge on its behalf: same endpoints, plus the PoweX-Account header
curl https://powex.me/v1/invoices \
-H "Authorization: Bearer pxk_test_…" \
-H "PoweX-Account: acct_4Fz9QeTt2Lm8Hk1WcY7aNd" \
-H "Content-Type: application/json" \
-d '{ "amount": "25.00", "description": "Order #1042" }'- Act with one header. Every endpoint of this API works for a subaccount: send
PoweX-Account: acct_…with a key that has theaccounts:actscope (plus the scope of the call itself). Idempotency keys are per subaccount. - One webhook endpoint for everything. Events of a subaccount are also delivered to your endpoints, with an
accountfield naming it. Events of your own account have noaccountfield. - Know your customer, anywhere.
businessis the account holder. SendtaxCountry,taxIdand optionallytaxIdType— e.g.BR_CNPJ,AR_CUIT,CL_RUT,MX_RFC_PM,US_EIN,ES_CIF,DE_VAT,GB_VAT,AU_ABN,IN_GSTIN(full list in the reference). Where the document has an official check-digit algorithm it is verified (the alphanumeric CNPJ included). WithouttaxIdTypethe type is inferred from the country; countries without a specific document useOTHER. The tax id is never returned in full (taxIdMasked). - Reviewed before it moves money. Live subaccounts start
PENDING_REVIEW; PoweX reviews the holder and turns themACTIVE. While pending, the holder can still be corrected withPATCH /v1/accounts/:id. Test subaccounts start active. Every status change made by PoweX (approval, suspension with itsstatusNote, reactivation) emitsaccount.updated. - Hosted onboarding. Prefer not to handle your customers' tax ids? Create the subaccount without
businessand callPOST /v1/account_linkswith theaccount, arefreshUrland areturnUrl(https). Send the holder to theurlyou get back: they fill in their own details on a PoweX page and come back toreturnUrl. The link is single use and valid for 1 hour; an expired one sends them torefreshUrlto get a new link. A live subaccount is not approved without a holder. - Switched on by PoweX. Each subaccount is a real account in the ledger, so PoweX enables subaccounts per platform and sets how many you can have (
SUBACCOUNTS_NOT_ENABLED,SUBACCOUNT_LIMIT_REACHED). A subaccount inherits your fee and limits. - Use
externalReffor your own id and find a subaccount withGET /v1/accounts?externalRef=…. The same holder cannot have two subaccounts on your platform in the same mode.
Moving money
- Transfers.
POST /v1/transfers(scopetransfers:write,Idempotency-Keyrequired) moves money between your wallet and a subaccount in one ledger entry:to_accountfunds it;from_accountbrings money back and must be enabled by PoweX for your platform (SUBACCOUNT_DEBITS_NOT_ENABLED). Each one emitstransfer.created. - Payouts from a subaccount. Call
POST /v1/payoutswithPoweX-Account. A subaccount without its own payout policy uses your platform's (per-payout and daily limits, the daily one counted per subaccount). Check it withGET /v1/balanceunder the same header.
Your commission and PoweX fees
Every live sale of a subaccount is split when it settles, in the same ledger transaction — invoice paid on-chain, paid with a PoweX balance, or a deposit to a customer's fixed address alike:
- Your commission (
platformFeeBpson the subaccount, 0 to 5000 = 50%) goes to your wallet, in the coin of the sale. Set it onPOST /v1/accountsor change it withPATCH /v1/accounts/:id; it applies to the sales that settle afterwards. - The PoweX subaccount fee is a per-sale rate PoweX sets for your platform. Depending on your contract it is either taken from the sale (paid by the subaccount) or billed to you in USD (paid by the platform).
- The payment shows every part:
amount = fee + applicationFee + net. Refunds return the PoweX fees proportionally (to the subaccount, or as a credit to you); your commission is not reversed. Nothing is charged in test mode. GET /v1/accounts/feeslists each sale with its commission and PoweX fee (filter byaccount);GET /v1/accounts/fees/summarygives the period totals and what you currently owe PoweX.
What PoweX bills your platform
- Charges are listed in
GET /v1/accounts/charges: the per-sale fee when you are the payer, refund credits (negative) and, if your contract has one, a monthly fee per live subaccount, billed for each closed month and prorated by the days the subaccount was active (approved and not suspended). - Once a day everything open is debited from your wallet in stablecoin in a single entry: USDT, then USDC — combined when neither covers it alone. Each debit emits
platform_bill.paid. Keep a stablecoin balance to stay current. - Without balance the billing becomes past due (
billing.pastDueSincein the fees summary) and you receiveplatform_billing.past_dueonce. After the grace period you cannot open new subaccounts (SUBACCOUNT_BILLING_PAST_DUE) until it is settled — existing subaccounts and their money keep working. Settle any time from the dashboard (Pay now).
Webhooks
Register an https:// URL (dashboard or POST /v1/webhook_endpoints) and PoweX will POST a JSON event to it whenever an invoice changes. Pick the events you want, or leave the list empty for all. A test endpoint only receives test-mode events, and a live endpoint only live ones.
| Event | When |
|---|---|
| invoice.payment_received | A payment was credited to an invoice. It may be partial; sent for every payment, including late ones. |
| invoice.paid | The invoice is now paid in full. |
| invoice.underpaid | The invoice closed with only part of the amount paid. |
| invoice.expired | The invoice closed without any payment. |
| invoice.cancelled | You cancelled the invoice. |
| payout.paid | A payout you requested was delivered (on-chain ones: after confirmation). |
| payout.failed | A payout failed. Nothing was sent and the limit was released. |
| payment.refunded | You refunded a balance payment back to the customer. |
| subscription.created | A customer subscribed to one of your plans (first charge paid). |
| subscription.renewed | A subscription was charged again for a new cycle. |
| subscription.payment_failed | A subscription charge failed; PoweX retries 1, 3 and 5 days later. |
| subscription.cancelled | A subscription ended: the customer or you cancelled, or payments kept failing. |
| deposit.received | A deposit arrived at one of your customers' addresses and was credited to your wallet. |
| transfer.created | Money moved between your wallet and one of your subaccounts. |
| account.updated | PoweX approved, suspended or reactivated one of your subaccounts; data.previousAttributes has the status before. |
| platform_bill.paid | Your PoweX billing was debited from your stablecoin balance (one leg per coin used). |
| platform_billing.past_due | A PoweX billing debit failed for lack of balance: the grace period before new subaccounts are blocked starts now. |
| ping | Sent by "test" so you can check your endpoint and signature code. |
curl https://powex.me/v1/webhook_endpoints \
-H "Authorization: Bearer pxk_test_…" \
-H "Content-Type: application/json" \
-d '{ "url": "https://shop.example.com/powex/webhook", "events": ["invoice.paid", "invoice.expired"] }'The response includes the signing secret (whsec_…) once; rotate it any time (the old one stops working immediately).
The event
{
"id": "evt_9f2c4a7b1d3e5f60a8b9c0d1",
"object": "event",
"type": "invoice.paid",
"livemode": true,
"createdAt": "2026-10-21T16:20:11.000Z",
"data": {
"object": {
"id": "inv_Qm8x2VtFhKz0aBcDeFgHiJ",
"object": "invoice",
"status": "PAID",
"amount": "25",
"currency": "USD",
"reference": "order-1042",
"paidUsd": "25",
"remainingUsd": "0",
"payments": [
{
"asset": "USDT",
"amount": "25",
"fee": "0.125",
"net": "24.875",
"usdValue": "25",
"late": false,
"txid": "0x…",
"createdAt": "2026-10-21T16:20:09.000Z"
}
]
}
}
}Headers on every delivery:
| Header | Value |
|---|---|
| PoweX-Signature | t=<unix>,v1=<hex> |
| PoweX-Event-Id | Same as event.id — identical on every retry. |
| PoweX-Event-Type | e.g. invoice.paid |
| PoweX-Delivery-Attempt | 1 for the first try, 2 for the first retry… |
| User-Agent | PoweX-Webhooks/1.0 |
Verify the signature
Always verify. The signature is HMAC-SHA256(secret, "<t>.<raw body>") as lowercase hex, where t is the unix timestamp in the header. Compute it over the raw request body (before any JSON parsing), compare in constant time and reject timestamps more than 5 minutes old (replay protection). A header may carry several v1= values; accept if any matches.
const crypto = require('node:crypto');
// rawBody must be the EXACT bytes you received (not re-serialized JSON).
function verifyPoweXSignature(secret, header, rawBody, toleranceSeconds = 300, now = Date.now() / 1000) {
const items = String(header || '').split(',').map((p) => p.trim());
const t = items.find((p) => p.startsWith('t='))?.slice(2);
const candidates = items.filter((p) => p.startsWith('v1=')).map((p) => p.slice(3));
if (!t || !/^\d+$/.test(t) || candidates.length === 0) return false;
if (Math.abs(now - Number(t)) > toleranceSeconds) return false; // replay protection
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return candidates.some(
(c) => c.length === expected.length && crypto.timingSafeEqual(Buffer.from(c), Buffer.from(expected)),
);
}
// Express: use express.raw({ type: 'application/json' }) on this route so req.body is the raw Buffer.
app.post('/powex/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8');
if (!verifyPoweXSignature(process.env.POWEX_WEBHOOK_SECRET, req.get('PoweX-Signature'), raw)) {
return res.sendStatus(400);
}
const event = JSON.parse(raw);
// Deduplicate by event.id, do the work, answer fast:
res.sendStatus(204);
});Test vector: secret whsec_test_secret_for_docs, t=1700000000, body {"id":"evt_1","object":"event","type":"invoice.paid"} → v1=a38f3edeeb6856c2c5e8bc056b15cdf123db3564ee88a86ab0192debcb7f2d93.
Delivery & retries
- Answer with any 2xx within 8 seconds. Do slow work after responding.
- Otherwise we retry up to 8 attempts in total, waiting 30 s, 2 min, 10 min, 1 h, 6 h, 12 h and 24 h between them. After that the delivery is marked failed; you can redeliver it from the dashboard or the API.
- A
410 Goneresponse stops the delivery and turns the endpoint off. 40 failed attempts in a row also turn it off (turn it back on from the dashboard). - Deduplicate by
event.id. Delivery is at-least-once, and events can arrive out of order — readdata.object.statusrather than assuming sequence. - We never follow redirects. The URL must be
https, on a public domain name (no IP addresses, no internal hosts), port 443 or 8443 — this is checked when you save it and on every delivery. - The delivery log (status, attempts, last error) is kept for 30 days: dashboard, or
GET /v1/webhook_endpoints/:id/deliveries.
Errors
Every error has the shape shown in Conventions. The codes you can meet:
| Code | HTTP | Meaning |
|---|---|---|
| API_KEY_INVALID | 401 | Missing, malformed, unknown or revoked key. |
| API_KEY_SCOPE_MISSING | 403 | The key lacks the scope this call needs (`details.scope` says which). |
| API_KEY_IP_NOT_ALLOWED | 403 | The key is restricted to a list of IPs and this request came from another one (`details.ip`). |
| MERCHANT_NOT_ACTIVE | 403 | Your merchant account is not approved (live) or is suspended. |
| PAYOUT_DESTINATION_NOT_ALLOWED | 403 | Your payout allowlist is on and this destination is not on it (`details.reason: "not_listed"`) or is still in its 24 h hold (`"on_hold"`, `details.activeAt`). |
| TRAVEL_RULE_INFO_REQUIRED | 400 | At or above the Travel Rule threshold: send `beneficiary` (or set it on the allowlist destination) and fill your business profile. `details.party` is `beneficiary` or `originator`; `details.thresholdUsd` is the threshold. |
| MERCHANT_LIMIT_EXCEEDED | 403 | Above the per-invoice or daily limit set for your account (`details.limit`, `details.max`). |
| VALIDATION_ERROR | 400 | The request body or query is invalid; `message` lists what. |
| INVOICE_AMOUNT_INVALID | 400 | At most 2 decimals, and the USD equivalent must be 0.50–1,000,000. |
| COUPON_INVALID | 422 | The coupon is unknown, inactive, expired, used up, or a fixed coupon in another currency. |
| CURRENCY_UNAVAILABLE | 422 | No recent exchange rate for `currency`. Retry shortly or price in USD. |
| INVOICE_NOT_FOUND | 404 | No such invoice for your account and mode. |
| INVOICE_NOT_OPEN | 409 | The invoice is not open (paid, expired or cancelled), or already has payments. |
| INVOICE_EXPIRED | 410 | The invoice expired. |
| INVOICE_TEST_ONLY | 409 | `simulate` works only on test-mode invoices. |
| INVOICE_METHOD_UNAVAILABLE | 409 | That coin or network is not available right now. |
| INVOICE_METHOD_LOCKED | 409 | A payment already arrived with the chosen method; it cannot change. |
| PAY_PRICE_UNAVAILABLE | 503 | No fresh price for that coin; retry shortly. |
| PAYMENT_LINK_NOT_FOUND | 404 | No such payment link. |
| PAYMENT_LINK_INACTIVE | 410 | The payment link was turned off. |
| PAYMENT_LINK_BUSY | 429 | Too many unpaid invoices are open on that link; retry later. |
| PAYMENT_LINK_LIMIT_REACHED | 409 | You reached the limit of payment links. |
| PAYOUTS_NOT_ENABLED | 403 | PoweX has not enabled payouts for your account (or they are paused). |
| PAYOUT_LIMIT_EXCEEDED | 403 | Above your per-payout or daily payout limit (`details.limit`, `details.max`). |
| PAYOUT_NOT_FOUND | 404 | No such payout for your account and mode. |
| PAYOUT_IDEMPOTENCY_REQUIRED | 400 | Creating a payout needs the `Idempotency-Key` header. |
| CUSTOMER_ADDRESSES_NOT_ENABLED | 403 | PoweX has not enabled customer addresses for your account. |
| CUSTOMER_ADDRESS_LIMIT_REACHED | 403 | You reached the customer limit set for your account (`details.limit`). |
| CUSTOMER_ADDRESS_NOT_FOUND | 404 | No such deposit address for your account and mode. |
| CUSTOMER_ADDRESS_TEST_ONLY | 409 | `simulate_deposit` works only on test-mode addresses. |
| WEBHOOK_URL_INVALID | 400 | The URL is not allowed (needs public https on a domain name). |
| WEBHOOK_NOT_FOUND | 404 | No such webhook endpoint or delivery. |
| WEBHOOK_LIMIT_REACHED | 409 | You reached the limit of webhook endpoints. |
| RATE_LIMITED | 429 | More than 300 calls in a minute with this key; wait `Retry-After` seconds. |
| DATABASE_BUSY | 503 | Momentary overload; nothing was processed — retry. |
Quote the requestId when you contact support.
Test mode
- Test keys, test invoices, test payment links and test webhook endpoints live in a separate world: they never mix with live data.
- No address is created, nothing is charged and no fee is taken. Use
POST /v1/invoices/:id/simulatewithpaid,underpaidorexpiredto drive an invoice to any outcome — webhooks fire exactly as in production. simulateis refused (INVOICE_TEST_ONLY) on live invoices.- Test subscription plans are subscribed with
simulate_subscriptionand advanced withsimulate(SUBSCRIPTION_TEST_ONLYon live ones). Test subscriptions are never charged. - You can use test mode while your merchant account is still under review.
Fees & your funds
- The fee is 0.50% of each payment by default. Your account may have its own rate: it is on every invoice as
feeBps(50 = 0.50%). - There is no setup fee and no monthly fee. The customer pays the network fee of their own transfer; you pay none to receive.
- The net amount is credited to your PoweX wallet in the coin the customer paid with. Swap it, send it, or withdraw it — see fees for those operations.
- Customers with a PoweX account pay in one tap. On the hosted checkout, a logged-in customer can pay with their PoweX balance: no address, no blockchain network, no waiting — the invoice closes at once and the money is in your wallet. For these payments the fee is the lower of your invoice fee and the platform's balance-payment fee (0.25% by default), up to a per-payment ceiling (US$ 1,000 by default; above it the customer pays on-chain). Each payment in
GET /v1/paymentscarriesmethod:chain,balanceorsimulated. - Automatic conversion (optional). In the dashboard you can choose a coin, usually a stablecoin such as USDT: every live payment received in another coin is then converted to it through your wallet's regular swap (its usual fee applies), so you do not carry price swings. Only payments received after you turn it on are converted, and a failed conversion is retried while the funds stay safe in your wallet.
- Your account can have limits (per invoice and per day) set by PoweX; exceeding them returns
MERCHANT_LIMIT_EXCEEDED.
Go-live checklist
- Merchant account Active (not “Under review”) — required for live keys.
- A live key with only the scopes you use, stored in your secret manager (not in code or a browser).
- A webhook endpoint that verifies the signature on the raw body, deduplicates by event id and answers 2xx fast.
Idempotency-Key(your order id) on every invoice creation, so a retry never double-bills.- Handle
invoice.underpaidandlatepayments (refund, top-up request or manual review). - Confirm payment from the webhook or
GET /v1/invoices/:id— never from the customer's browser. - Reconcile daily with
GET /v1/paymentsor the CSV from the Statement. - Run the whole flow in test mode with
simulatebefore switching keys.
Endpoint reference
Generated from the live specification at /v1/openapi.json, so it always matches the API. A * marks a required field; the grey tag on the right is the key scope the call needs.