Skip to content
Developers

Integrate in minutes

A single dependency-free PHP SDK: create invoices, redirect to the hosted checkout, and verify signed webhooks. REST is available for any language.

API at a glance
  • Base URLapi.payora.money
  • AuthenticationHMAC-SHA256
  • FormatJSON · HTTPS
  • SDKsPHP · Node.js · Python
  • SpecificationOpenAPI 3.0
quick start

From invoice to webhook

  1. 01

    Create an invoice

    One API call with an order ID and a fiat or crypto amount returns a hosted pay URL.

  2. 02

    Customer pays

    They pick a coin, scan the QR or open a wallet, and the checkout confirms live on finality.

  3. 03

    You get a signed webhook

    Verify the HMAC signature, credit the order once, and watch the payment land on your balance.

PHP SDK

One file, no dependencies. createInvoice(), getInvoice() and verifyWebhook() — drop it into any project.

HMAC auth

Requests and webhooks are HMAC-SHA256 signed with timestamp skew and idempotency keys.

REST API

Plain JSON over HTTPS — integrate from any stack, not just PHP.

checkout
<?php
require 'Payora.php';
$payora = new Payora('https://api.payora.money', getenv('PAYORA_KEY'), getenv('PAYORA_SECRET'));

// 1 — create an invoice (fiat-priced; the payer picks a coin)
$inv = $payora->createInvoice([
  'order_id'   => 'ORDER-42',
  'mode'       => 'fiat',
  'amount'     => '9.99',
  'return_url' => 'https://shop.example/thanks',
  'lang'       => 'en',
], 'ORDER-42'); // Idempotency-Key: a retry never creates a second invoice
header('Location: ' . $inv['pay_url']);

// 2 — webhook endpoint: ALWAYS verify the signature first
$event = $payora->verifyWebhook(file_get_contents('php://input'), getallheaders());
if ($event === null) { http_response_code(400); exit; }
if ($event['status'] === 'paid') {
  creditOrderOnce($event['id'], $event['order_id'], $event['amount'], $event['currency']);
}
http_response_code(200); // 2xx stops retries

Get your API key and secret by creating an account. The full SDK is in your dashboard.

authentication

Authentication

Every call except /v1/health carries three signed headers. The signature proves the request comes from the holder of your api_secret and was not changed on the way.

HeaderValueDescription
X-Payora-Keypk_…Your public api_key. Identifies the store.
X-Payora-Timestampunix secondsThe moment you signed the request, in Unix seconds.
X-Payora-Signaturehex · 64Lowercase hex HMAC-SHA256 of timestamp + "." + raw body, keyed with your api_secret.
X-Payora-Test1Marks an invoice as a test of your integration: it is real and can receive coins, but stays out of demand and conversion reports.
Idempotency-KeystringOptional on POST /v1/invoice and POST /v1/payout. A retry with the same key returns the first result instead of creating a duplicate; the same key with a different body answers 409.
Content-Typeapplication/jsonRequired on requests that carry a body.
X-Payora-Signature=hex( HMAC-SHA256( api_secret, timestamp + "." + raw_body ) )

Sign the exact bytes you send. Re-encoding the JSON after signing — even reordering keys or adding a space — breaks the signature.

For a GET request the body is empty, so the signed string is the timestamp followed by a dot.

The API rejects a timestamp more than 120 seconds away from its clock. Keep your server in sync with NTP.

Keep api_secret on your server. It signs requests and webhooks alike: whoever holds it can create invoices in your name and forge callbacks.

sign-request
<?php
$body = json_encode(['order_id' => 'ORDER-42', 'mode' => 'fiat', 'amount' => '9.99'], JSON_UNESCAPED_SLASHES);
$ts   = (string) time();
$sig  = hash_hmac('sha256', $ts . '.' . $body, $apiSecret); // lowercase hex

$ch = curl_init('https://api.payora.money/v1/invoice');
curl_setopt_array($ch, [
  CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    'X-Payora-Key: ' . $apiKey,
    'X-Payora-Timestamp: ' . $ts,
    'X-Payora-Signature: ' . $sig,
    'Idempotency-Key: ORDER-42',
    'Content-Type: application/json',
  ],
]);
$invoice = json_decode(curl_exec($ch), true);
POST /v1/invoice

Invoice parameters

All optional except order_id and amount. Fiat mode lets the payer pick any coin enabled on your account; crypto mode also needs currency.

ParameterTypeDescription
order_idstring · requiredYour unique order reference (≤190 chars). A repeat rejects with 409 — safe against double-submits.
amountstring · requiredFiat amount (mode=fiat) or exact crypto amount (mode=crypto).
modefiat | cryptoDefault fiat: rate locked per coin at checkout. crypto fixes the coin + amount up front.
fiat_currencystringISO fiat code for fiat mode (default USD).
currencystringCoin for crypto mode (e.g. TON, TRX, USDT_TRON).
return_urlhttps URLWhere the checkout's “Return to store” button sends the customer back — your site. Absolute http(s) only.
langen·ru·uk·es·deCheckout language, so a customer from your Russian store sees the Russian checkout — not English. Also settable as ?lang=ru on the pay URL.
ttlsecondsHow long the invoice stays payable (120–86400 seconds, default 1800).
customer_emailemailOptional — receipt + emailed to the payer.
notesstringFree-form note stored on the invoice.

return_url and lang also work as query params appended to the pay_url the API gives you — <pay_url>&lang=ru&return_url=https://shop.example/thanks. Always send the buyer to the pay_url returned by the API: it carries a one-per-invoice access token, so a checkout page cannot be reached by guessing invoice numbers. Do not rebuild the link from the invoice id.

Request builder

Build a signed invoice request

Fill in the fields and copy a ready cURL command. It signs itself in your shell with $PAYORA_KEY and $PAYORA_SECRET — this page never sees your keys and sends nothing.

mode
POST /v1/invoice
BODY='{"order_id":"ORDER-42","mode":"fiat","amount":"9.99","fiat_currency":"USD","return_url":"https://shop.example/thanks","lang":"en","ttl":1800}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$PAYORA_SECRET" | cut -d' ' -f2)

curl -X POST https://api.payora.money/v1/invoice \
  -H "X-Payora-Key: $PAYORA_KEY" \
  -H "X-Payora-Timestamp: $TS" \
  -H "X-Payora-Signature: $SIG" \
  -H 'Idempotency-Key: ORDER-42' \
  -H 'Content-Type: application/json' \
  -d "$BODY"

Nothing is sent from this page.

webhooks

Webhooks

When an invoice or a payout changes state, Payora POSTs a signed JSON event to your callback URL. Verify the signature first, credit the order once, answer 2xx.

EventstatusWhen it fires
invoice.paidpaidThe payment is confirmed on-chain. Carries the amount, the coin and tx_hash.
invoice.underpaidunderpaidThe invoice expired after less than the expected amount arrived.
invoice.expiredexpiredThe invoice expired with nothing received.
invoice.cancelledcancelledThe invoice was cancelled. Money that had already arrived, if any, is described in the event.
invoice.cancel_settled—Money that arrived on a cancelled invoice has been dealt with: credited or sent back.
invoice.late_payment—Money that arrived on an invoice after it was cancelled. Sent once for each such deposit; result says whether it was credited or is being returned.
payout.sentsentA payout batch was signed and broadcast. Carries every item with its tx_hash.

Headers of every delivery

HeaderValueDescription
X-Payora-IduuidUnique event id, the same as id in the body. Store it to ignore repeats.
X-Payora-Eventinvoice.paidEvent name.
X-Payora-Timestampunix secondsWhen this attempt was signed.
X-Payora-Signaturehex · 64Same scheme as requests: HMAC-SHA256 of timestamp + "." + raw body with your api_secret.

invoice.paid payload

FieldTypeDescription
iduuidEvent id for de-duplication.
eventstringEvent name.
invoice_idintPayora invoice number.
order_idstringYour order reference, as sent when the invoice was created.
statusstringpaid for invoice.paid.
currencystringCoin the payer used, e.g. USDT_TRON.
amountdecimal stringAmount received.
amount_unitsinteger stringThe same amount in the coin's smallest units — use it for exact comparisons.
expecteddecimal stringAmount the invoice asked for.
fiat_amountdecimal string | nullFiat price of the invoice as you created it; null for crypto-mode invoices.
fiat_currencystring | nullISO code of fiat_amount; null for crypto-mode invoices.
tx_hashstringOn-chain transaction hash.
paid_atunix secondsWhen the event was created.
testbooltrue for sandbox invoices.
invoice.paid
{
    "id": "6f1c2a4e-8b0d-4c1e-9a57-2f3d9e1b7c40",
    "event": "invoice.paid",
    "invoice_id": 1042,
    "order_id": "ORDER-42",
    "status": "paid",
    "currency": "USDT_TRON",
    "amount": "9.99",
    "amount_units": "9990000",
    "expected": "9.99",
    "fiat_amount": "9.99",
    "fiat_currency": "USD",
    "tx_hash": "3f9a…c21e",
    "paid_at": 1758038400,
    "test": false
}

When the amount received differs from the invoice, amount_received and amount_expected are added.

Delivery and retries

Anything other than 2xx, or no answer within 15 seconds, is retried with exponential backoff capped at one hour between attempts — up to 12 attempts. Every attempt is signed again with a new timestamp; the body and the event id stay the same, so de-duplicate on id.

After the last attempt the event is marked failed and the store owner gets an email. Redirects are never followed, and the callback URL must be reachable from the internet — no localhost or private addresses.

Before you credit an order

  • Read the raw body before any JSON parsing.
  • Recompute the signature and compare in constant time: hash_equals, crypto.timingSafeEqual, hmac.compare_digest.
  • Reject timestamps outside your window — the SDKs accept ±300 seconds.
  • Check the status and the amount against your order, then credit once per event id.
  • Answer 2xx quickly and do slow work after responding.
webhook
<?php
require 'Payora.php';
$payora = new Payora('https://api.payora.money', getenv('PAYORA_KEY'), getenv('PAYORA_SECRET'));

$raw   = file_get_contents('php://input');           // raw, before any parsing
$event = $payora->verifyWebhook($raw, getallheaders()); // HMAC + ±300 s window, constant-time
if ($event === null) { http_response_code(400); exit; } // bad / forged / stale

if ($event['event'] === 'invoice.paid' && !alreadyProcessed($event['id'])) {
  creditOrder($event['order_id'], $event['amount'], $event['currency'], $event['tx_hash']);
  rememberProcessed($event['id']);
}
http_response_code(200); // 2xx stops retries
Playground

Webhook signature playground

Enter a secret, a timestamp and the raw body to see the exact signature Payora would send — or check one you received.

Runs in your browser with WebCrypto. Nothing you type leaves this page. Use a test secret, not a live one, on a shared computer.

X-Payora-Timestamp
Signed string
1758038400.{…}
Headers Payora sends
headers
X-Payora-Timestamp: 1758038400
X-Payora-Signature: …

Paste a signature to compare

mass payouts

Mass Payout API

Send crypto to up to 500 wallets in one signed, idempotent request — for payroll, affiliate payouts or withdrawals. The batch total plus per-item fee is held on your internal balance instantly; the operator signs and broadcasts, then a signed payout.sent webhook fires. Same HMAC auth as everything else.

payout
# create a payout batch — funds held instantly, sent by the operator
curl -X POST https://api.payora.money/v1/payout \
  -H 'X-Payora-Key: pk_…' -H 'X-Payora-Timestamp: 1700000000' \
  -H 'X-Payora-Signature: <hmac-sha256>' -H 'Idempotency-Key: payroll-2026-01' \
  -d '{"currency":"USDT_TRON","items":[
        {"address":"TR7…","amount":"250.00"},
        {"address":"TX9…","amount":"99.50","tag":"batch-A"}]}'

# → 201 Created
{ "batch_id": 812, "status": "pending", "currency": "USDT_TRON",
  "item_count": 2, "total": "349.50", "fee": "…", "items": [ … ] }

# poll status, or receive a signed payout.sent webhook when it is sent
curl https://api.payora.money/v1/payout/812 -H 'X-Payora-Key: …' …

Validation is strict: any bad address or over-precise amount rejects the whole batch (HTTP 422) with the offending item indexes — a payroll run never silently drops a recipient. Cancel a still-pending batch with POST /v1/payout/{id}/cancel to release the hold. See the architecture write-up.

reference

Endpoint reference

Every endpoint speaks plain JSON over HTTPS at api.payora.money. All except /v1/health use the same HMAC-SHA256 request auth. The machine-readable spec lives at openapi.json.

EndpointAccessDescription
GET/v1/healthno authService status plus the list of enabled currencies.
POST/v1/invoiceauthCreate an invoice (fiat or crypto mode). Supports Idempotency-Key. Returns 201 with invoice_id, status, pay_url and expires; crypto mode adds the address, amount and deeplink.
GET/v1/invoice/{id}authInvoice status (own invoices only): status, remaining, and the charge — currency, address, expected/received amounts and tx_hash.
POST/v1/payoutauth · Pro+Create a mass payout batch (strict validation, funds held instantly). Supports Idempotency-Key. Returns 201 with the batch view: batch_id, status, totals, fee, items.
GET/v1/payoutauthList your recent payout batches (?limit=, default 50).
GET/v1/payout/{id}authPayout batch status and items (own batches only).
POST/v1/payout/{id}/cancelauthCancel a still-pending batch and release the held balance. Returns 409 if the batch is no longer cancellable.
downloads

SDKs & downloads

Single files with no dependencies. Every SDK signs requests and verifies webhooks the same way.

phpPHPPayora.php

PHP 7.4+ with cURL, one file. createInvoice, getInvoice and verifyWebhook.

jsNode.jspayora.js

Node.js with built-in https and crypto only. Invoices, balance, payouts and verifyWebhook.

pyPythonpayora.py

Python standard library only. Invoices, balance, payouts and verify_webhook.

{ }Postmanpayora.postman_collection.json

A Postman collection that signs every call for you.

{…}OpenAPI 3.0openapi.json

The machine-readable spec of every endpoint, for code generators and API clients.

api.payora.moneyOpen
Running WooCommerce, WHMCS, Magento or another CMS?

A ready module does all of the above for you: invoice, redirect and a verified webhook.

Browse plugins
FAQ

Frequently asked questions

Straight answers, including the uncomfortable ones: custody, fees and what happens when something goes wrong.

Help centre

How do I integrate Payora?

Install the single-file PHP SDK (no dependencies) or call the REST API directly. Create an invoice with createInvoice(), redirect the customer to its pay_url, and handle one webhook event: status=paid. A competent developer is live in 30–60 minutes.

Is there a REST API for non-PHP stacks?

Yes. Everything is plain JSON over HTTPS, so you can integrate from any language. The PHP SDK is a convenience wrapper around the same endpoints — create invoice, get invoice, and verify webhook signatures.

Can the customer return to my site after paying, in the right language?

Yes. Pass return_url when you create the invoice (or ?return_url= on the pay link) and the checkout shows a “Return to store” button back to your site on the success screen — and while paying. Pass lang (en, ru, uk, es, de), or ?lang= on the link, so a customer from your Russian store lands on the Russian checkout instead of English. Both are optional; return_url must be an absolute http(s) URL.

How is API authentication handled?

Requests and webhooks are authenticated with HMAC-SHA256 over the timestamp and raw body using your api_secret, with a ±120-second skew window and idempotency keys. Signatures are compared in constant time to prevent timing attacks.

How do I get API keys?

Create a free account. Your public api_key and secret api_secret appear in the dashboard, along with the downloadable SDK and full webhook reference. Rotate keys at any time from settings.

What does the webhook payload contain?

A signed JSON event with the invoice id, your order_id, the status (for example paid), the amount and currency, and a unique event id for idempotency. Always verify the signature before crediting an order, then record the event id to avoid double-processing on retries.

Is there an API for mass crypto payouts?

Yes. POST /v1/payout sends crypto to up to 500 wallets in one request — one {address, amount, optional tag} per item, in any coin enabled on your account. It uses the same HMAC-SHA256 auth and Idempotency-Key as the invoice API, so a retried request never creates a duplicate batch. The batch total plus per-item fee is reserved on your balance immediately; the operator signs and broadcasts, then a signed payout.sent webhook fires. Validation is strict — any invalid address or over-precise amount rejects the whole batch (422) so a payroll run never silently drops a recipient.

How do I automate crypto payroll or affiliate payouts?

Build the recipient list in your own system, then call POST /v1/payout once per run with a stable Idempotency-Key (for example payroll-2026-01). Poll GET /v1/payout/{id} or wait for the payout.sent webhook to confirm delivery and record each tx hash. Submitting the batch reserves funds but does not move them by itself — the operator signs and broadcasts, so nothing leaves on an API call alone.

Get your API keys

A single dependency-free PHP SDK: create invoices, redirect to the hosted checkout, and verify signed webhooks. REST is available for any language.

What this page answers