Developer docs

RENOVAX Payments API

REST API for merchant operations (payment links, refunds) and reseller operations (user provisioning, credit sales). All responses are JSON. Authentication uses Bearer tokens.

Sample integrations on GitHub Ready-to-use plugins and modules for WooCommerce, WHMCS, DHRU Fusion, Magento 2, PrestaShop, Saleor, Shopify, Wix, WebX and BILLmanager.
View on GitHub

Overview

The API exposes two independent surfaces, each with its own token type:

  • Merchant API — scoped to a single merchant. Use for creating payment links, fetching invoices, and issuing refunds. Authenticated with a merchant token.
  • Reseller API — scoped to a user with the reseller, distributor, or admin role. Use for provisioning end-user accounts and selling credits. Authenticated with a user token.

Base URL: https://payments.renovax.net/api/v1

Endpoints are versioned under /api/v1/. The unversioned /api/ paths are kept for backward compatibility but new integrations should use v1.

Authentication

Generate tokens from the dashboard:

  • Merchant token: Merchant → Edit → API Tokens. Shown once; store it securely.
  • User token: Profile → API Tokens. Shown once; store it securely.

Send the token as a Bearer header on every request:

Authorization: Bearer <your_token>
Accept: application/json
Content-Type: application/json

Merchant tokens can only call /merchant/* endpoints. User tokens can only call /reseller/* and legacy /invoices endpoints.

Payment methods / drivers

RENOVAX Payments integrates the providers below. Every driver supports charge by default; the Refund and Capture / Void columns indicate whether the matching Merchant API endpoints work end-to-end with that driver. Drivers without automated refund or capture can still be operated manually from the provider's own portal.

Cards & traditional banking (fiat)
DriverTypeRefund (API)Capture / Void (API)
StripeFiat
PayPalFiat
BraintreeFiat
VenmoFiat
RevolutFiat
KlarnaFiat
WiseFiat
SkrillFiat
VoletFiat
PayseraFiat
AdyenFiat
Amazon PayFiat
LATAM (fiat)
DriverTypeRefund (API)Capture / Void (API)
Mercado PagoFiat
Asaas (Brasil)Fiat
PIX (Brasil)Fiat
Cuba (fiat)
DriverTypeRefund (API)Capture / Void (API)
EnZonaFiat
TransfermóvilFiat
Crypto
DriverTypeRefund (API)Capture / Void (API)
Crypto (native on-chain)Crypto
QvaPayCrypto
NodexPayCrypto
Binance PayCrypto
Bybit PayCrypto
Gate PayCrypto
Coinbase CommerceCrypto
NOWPaymentsCrypto

A dash () means the driver does not expose the corresponding API operation in RENOVAX Payments. Native on-chain crypto is irreversible by design — refunds require an out-of-band return transfer to the payer address. For any driver missing a check mark, the operation may still be available from the provider's own portal.

Merchant API

All endpoints below require a merchant token.

GET /merchant/me

Returns the authenticated merchant.

curl https://payments.renovax.net/api/v1/merchant/me \
  -H "Authorization: Bearer $TOKEN"

Response 200:

{
  "id": 7,
  "name": "Acme Store",
  "default_currency": "USD",
  "public_uuid": "9c1f...-...-...",
  "customer_pays_fee": false,
  "fx_markup_percent": "1.000",
  "is_active": true
}
GET /merchant/invoices

Paginated invoice list. Query params: status, per_page (max 100).

curl "https://payments.renovax.net/api/v1/merchant/invoices?status=confirmed&per_page=20" \
  -H "Authorization: Bearer $TOKEN"

Response 200:

{
  "data": [
    {
      "id": "01J...",
      "merchant_id": 7,
      "amount": "49.90",
      "currency": "USD",
      "status": "confirmed",
      "client_remote_id": "order-42",
      "remote_namespace": "shopify",
      "success_url": "https://yoursite.com/thanks",
      "cancel_url": "https://yoursite.com/cart",
      "metadata": { "source": "checkout" },
      "fiat_enabled": false,
      "is_flexible": false,
      "flexible_suggestions": null,
      "expires_at": "2026-04-21T23:59:59+00:00",
      "paid_at": "2026-04-21T23:12:03+00:00",
      "pay_url": "https://.../pay/01J...",
      "created_at": "2026-04-21T23:00:00+00:00"
    }
  ],
  "meta": { "current_page": 1, "last_page": 3, "per_page": 20, "total": 48 }
}
POST /merchant/invoices

Creates a payment link. Returns the invoice with a pay_url to share.

Fees and gas: the amount you pass is what the merchant receives net of any on-chain costs. RENOVAX Payments calculates the platform fee and (for crypto) the network gas reserve server-side and shows the final total to the customer at checkout. Webhooks return amount_received (what the customer paid), fee (platform fee) and amount_net (what the merchant got). Internal fields like gas_reserve_usdt or expected_amount_usdt are not exposed in the API — they're applied transparently at the checkout step.

curl -X POST https://payments.renovax.net/api/v1/merchant/invoices \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 49.90,
    "currency": "USD",
    "client_remote_id": "order-42",
    "remote_namespace": "shopify",
    "success_url": "https://yoursite.com/thanks",
    "cancel_url": "https://yoursite.com/cart",
    "metadata": { "source": "checkout" },
    "expires_in_minutes": 60,
    "fiat_enabled": true
  }'

currency defaults to the merchant's default_currency when omitted. expires_in_minutes ranges 1–43200 (30 days). fiat_enabled opts the invoice into fiat payment methods (Stripe / PayPal / etc.); defaults to the merchant's setting when omitted. remote_namespace (optional, max 64 chars): prevents client_remote_id collisions if you migrate platforms. When omitted, it auto-detects from the domain of success_url.

Optional anti-fraud fields — pass any of these to improve AVS matching on fiat drivers, prefill the checkout, and have the values echoed back to you in the webhook payload. All top-level fields are optional. billing.country / shipping.country must be ISO-3166-1 alpha-2 (two uppercase letters) when sent — we validate alpha + size:2 and normalize to uppercase server-side.

Required-with rule — if you send ANY of billing.address_line1, billing.city, billing.postal_code or billing.country, the four are required together (same rule for shipping.*). Half-filled addresses are useless for AVS and we'd rather reject them with a 422 than have the provider 400 the request opaquely. Names and phone stay optional.

shipping_same_as_billing — pass true at the top level (boolean) and leave shipping empty to copy the billing object into shipping server-side. If you also send a shipping object, the explicit object wins (no copy). The flag is consumed at request time and is not persisted on the invoice.

Whitelist — only the keys listed below are persisted (first_name, last_name, address_line1, address_line2, city, state, postal_code, country, phone). Any other key inside billing or shipping is silently dropped before persistence.

curl -X POST https://payments.renovax.net/api/v1/merchant/invoices \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 49.90,
    "currency": "USD",
    "client_remote_id": "order-42",
    "remote_namespace": "shopify",
    "customer_name":  "John Doe",
    "customer_email": "[email protected]",
    "shipping_same_as_billing": true,
    "billing": {
      "first_name":    "John",
      "last_name":     "Doe",
      "address_line1": "350 5th Ave",
      "address_line2": "Suite 7800",
      "city":          "New York",
      "state":         "NY",
      "postal_code":   "10118",
      "country":       "US",
      "phone":         "+1-212-555-0142"
    }
  }'

Recommended minimum for AVS: billing.address_line1, billing.postal_code, billing.country. Card-not-present processors that don't receive billing data tend to score the transaction higher on their fraud model — leaving these blank silently increases false positives. The data is also echoed in every invoice.* webhook so your backend can reconcile it with your CRM/ERP without an extra GET.

Pass-through to providers — the following drivers forward your fields to the provider's API so they reach the provider's fraud engine and AVS check:

  • stripecustomer_email + shipping on Checkout Session and Wallet Intent. Billing address is collected by Stripe's UI and cannot be pre-filled via API.
  • adyenshopperEmail, shopperName, billingAddress, deliveryAddress on Pay by Link.
  • braintree / venmocustomer, billing, shipping on Transaction.sale (AVS + Kount).
  • paypalpayment_source.paypal.email_address + name; purchase_units[].shipping (shipping flips the experience from NO_SHIPPING to SET_PROVIDED_ADDRESS).
  • revolutcustomer{full_name,email,phone} + shipping_address on Orders API.
  • klarnabilling_address + shipping_address (Klarna uses billing for BNPL credit decisioning).
  • mercadopagopayer + shipments.receiver_address on Preferences API.
  • skrillpay_from_email, firstname, lastname, address, address2, phone_number, city, state, postal_code, country (ISO-3 — automatic mapping from ISO-2).
  • amazonpayaddressDetails (shipping) on Checkout Session v2. Falls back to billing when no shipping is provided.
  • payserap_email, p_firstname, p_lastname, p_street, p_city, p_state, p_zip, p_countrycode on SCI v1.6 (extra params don't break the signature).
  • nowpaymentscustomer_email (used for payer confirmation emails; crypto rails don't AVS).
  • voletac_email (payer email pre-fill on SCI).

Drivers without a row above (wise, asaas, pix, enzona, transfermovil, and the pure-crypto drivers crypto, qvapay, nodexpay, binancepay, bybitpay, gatepay, coinbase_commerce) still persist the data and echo it in the webhook payload, but their provider API does not accept customer-data pre-fill at checkout creation (the customer enters it on the hosted page, the rail is offline, or the gateway doesn't expose those fields).

Response 201:

{
  "id": "01J...",
  "merchant_id": 7,
  "amount": "49.90",
  "currency": "USD",
  "status": "pending",
  "client_remote_id": "order-42",
  "remote_namespace": "shopify",
  "success_url": "https://yoursite.com/thanks",
  "cancel_url": "https://yoursite.com/cart",
  "metadata": { "source": "checkout" },
  "fiat_enabled": true,
  "is_flexible": false,
  "flexible_suggestions": null,
  "expires_at": "2026-04-21T23:59:59+00:00",
  "paid_at": null,
  "pay_url": "https://.../pay/01J...",
  "created_at": "2026-04-21T23:00:00+00:00"
}

Idempotency key: (merchant_id, remote_namespace, client_remote_id). If a pending invoice with the same key exists, it is returned with status 200 (retry safe). If an invoice exists in any other status (confirmed, expired, cancelled, etc.), it is reactivated back to pending with a fresh expiry — no duplicates. Concurrent requests for the same key get 429 concurrent_request. If the same merchant has another pending invoice with the same amount+currency within the dedup window you'll get 409 duplicate_amount. If the merchant's user balance is below zero you'll get 402 negative_balance — top up credits to continue.

Flexible (open-amount) invoices

Set flexible: true to create an invoice without a fixed amount — the customer types the amount on the checkout page (donations, tips, "pay what you want", recharges, etc.). The amount the customer types is what the merchant receives; if customer_pays_fee is on, the platform fee is added on top before the customer signs.

Optional suggestions renders quick-pick buttons in the checkout (max 10 entries; each must be ≥ 1 or ≥ merchant.min_flexible_amount when set). When omitted, the customer just sees a free-input field.

curl -X POST https://payments.renovax.net/api/v1/merchant/invoices \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "flexible": true,
    "currency": "USD",
    "client_remote_id": "donation-2026-q2",
    "suggestions": [5, 10, 25, 50],
    "metadata": { "campaign": "annual-donation" }
  }'

Response 201:

{
  "id": "01J...",
  "amount": null,
  "currency": "USD",
  "status": "pending",
  "is_flexible": true,
  "flexible_suggestions": [5, 10, 25, 50],
  "pay_url": "https://.../pay/01J...",
  ...
}

Notes for flexible invoices:

  • amount stays null until the customer types it on the checkout. Once they submit, the invoice updates to that value and the flow continues identically to a fixed-amount invoice.
  • Idempotency for flexible reuses any pending invoice with the same client_remote_id and is_flexible: true (the typed amount can change between visits as long as no on-chain payment was detected yet).
  • The duplicate_amount check is skipped at creation; it kicks in when the customer submits the amount in the checkout (so two pending flexible invoices can coexist before either is paid).
  • You can also pre-fill the amount via the share URL by appending ?amount=N: https://payments.renovax.net/api/v1/pay/01J...?amount=25.
  • The customer can leave an optional message that arrives in the webhook as customer_note (max 500 chars, plain text).

Errors: 422 invalid_suggestion if a suggestion is below the merchant's minimum or non-numeric. The minimum is the global floor of 1 unless the merchant raised it via min_flexible_amount.

GET /merchant/invoices/{id}

Fetches a single invoice by id.

curl https://payments.renovax.net/api/v1/merchant/invoices/01J... \
  -H "Authorization: Bearer $TOKEN"

Response 200:

{
  "id": "01J...",
  "merchant_id": 7,
  "amount": "49.90",
  "currency": "USD",
  "status": "confirmed",
  "client_remote_id": "order-42",
  "success_url": "https://yoursite.com/thanks",
  "cancel_url": "https://yoursite.com/cart",
  "metadata": { "source": "checkout" },
  "fiat_enabled": false,
  "is_flexible": false,
  "flexible_suggestions": null,
  "expires_at": "2026-04-21T23:59:59+00:00",
  "paid_at": "2026-04-21T23:12:03+00:00",
  "pay_url": "https://.../pay/01J...",
  "created_at": "2026-04-21T23:00:00+00:00"
}
POST /merchant/invoices/{id}/refund

Refunds a confirmed payment. Leave amount empty for a full refund. Drivers with automated refund support via API: stripe, paypal, revolut, klarna, wise, adyen, nowpayments, paysera, amazonpay, braintree, qvapay, nodexpay, binancepay, asaas, bybitpay, gatepay, mercadopago, enzona. For drivers without automated refund (native on-chain crypto, coinbase_commerce, etc.) the merchant refunds manually from the provider's portal.

curl -X POST https://payments.renovax.net/api/v1/merchant/invoices/01J.../refund \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 10.00 }'

Response 200:

{
  "status": "success",
  "provider_refund_id": "re_1N...",
  "amount": "10.00",
  "currency": "USD",
  "message": "Refunded successfully"
}

Returns 422 with one of: no_confirmed_payment (the invoice has no captured payment yet), unsupported_driver (refunds aren't available for the original payment driver — e.g. crypto), or amount_exceeds (requested amount is greater than the captured amount). The provider may also return 422 if it rejects the refund (in that case status is "failed" and message explains why).

POST /merchant/invoices/{id}/capture

Captures funds for an invoice in authorized state (manual-capture flow on Stripe / PayPal / Revolut / Klarna / Adyen / Braintree). Leave amount empty to capture the full authorized amount, or send a smaller amount for a partial capture (e.g. partial shipment).

Pass an Idempotency-Key header to safely retry the same request — if the capture already succeeded under that key, you'll get the cached response back without a second call to the provider.

curl -X POST https://payments.renovax.net/api/v1/merchant/invoices/01J.../capture \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: 9f3a-0c1e-4b2d" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 80.00 }'

Response 200:

{
  "status": "captured",
  "provider_capture_id": "pi_3N...",
  "amount": "80.00",
  "currency": "USD"
}

Errors: 409 invalid_state (invoice not in authorized), 409 capture_in_progress (concurrent capture in flight), 422 amount_exceeds, 422 no_payment / unsupported_driver, 424 provider_error (the provider rejected the capture — see message), 501 if the driver does not support capture from RENOVAX Payments (capture from the provider portal instead). After a successful capture, the merchant receives invoice.paid via webhook when the provider sends the capture event. The HTTP response confirms RENOVAX Payments initiated the call — the invoice transitions to confirmed on webhook arrival.

POST /merchant/invoices/{id}/void

Voids the authorization for an invoice in authorized state. The funds held on the customer's payment method are released back — the customer never sees a charge. Optionally include a structured reason and free-text reason_note for your audit trail (these travel in the outbound invoice.voided webhook).

curl -X POST https://payments.renovax.net/api/v1/merchant/invoices/01J.../void \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: 9f3a-0c1e-4b2d" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "out_of_stock", "reason_note": "SKU 12345 sold out" }'

Response 200:

{
  "status": "voided",
  "provider_id": "pi_3N..."
}

Allowed reason values: customer_request, out_of_stock, fraud_suspected, other. Errors mirror the capture endpoint (409 invalid_state, 422 no_payment/unsupported_driver, 424 provider_error, 501). After a successful void, the merchant receives invoice.voided via webhook once the provider confirms the void. To distinguish from natural auth-expiry (where invoice.expired fires instead), the webhook payload includes voided_via = "api" and the void_reason you sent.

POST /merchant/invoices/{id}/sync

Polling pull — when the inbound webhook never arrived (typically because you didn't configure the webhook secret on the provider's side, the provider's notification endpoint is throttled, or your server rejected our delivery), call this endpoint and we will query the provider's API for the real payment state and apply it to the invoice. If the provider says the payment is captured, the invoice transitions to confirmed and you receive the regular outbound invoice.paid webhook (now signed with your own webhook_secret so you can verify it).

curl -X POST https://payments.renovax.net/api/v1/merchant/invoices/01J.../sync \
  -H "Authorization: Bearer $TOKEN"

Response 200:

{
  "status":          "synced",
  "invoice_status":  "confirmed",
  "provider_status": "confirmed",
  "amount":          "49.90",
  "currency":        "USD"
}

Errors: 404 no_payment (customer never selected a payment method on the checkout — there's nothing to sync yet), 422 unsupported_driver (the underlying driver doesn't implement polling — currently supported: stripe; more drivers being added). If the invoice is already terminal (confirmed/failed/expired/ voided/refunded), the endpoint is a no-op and returns the current state.

When to call: recommended cadence is polling every 60–120s for at most 30 minutes after the customer returned from the provider's hosted checkout. Avoid hot loops — the provider rate-limits and Stripe in particular returns 429 quickly. For volumes > 10k invoices/day, prefer configuring the webhook secret properly (this endpoint is the safety net, not the primary path).

Polling supported on: stripe, paypal, adyen, revolut, klarna, mercadopago, braintree, venmo. Other drivers return 422 sync_unsupported.

Automatic synchronous fallback (Return URL interceptor)

You usually don't need to call /sync manually. RENOVAX Payments automatically intercepts the customer's return from the provider's hosted checkout to close the loop synchronously when the inbound webhook didn't arrive (typically because you didn't configure the webhook secret on the provider's dashboard):

  1. The success/cancel URL we hand to providers is our own interceptor (/r/{invoice}/{driver}/return or /r/{invoice}/{driver}/cancel) — not your success_url / cancel_url directly.
  2. When the customer returns from the provider, the interceptor checks the invoice state:
    • Already terminal (webhook arrived first): redirects immediately to your real success_url — zero overhead.
    • Still pending and you have webhook_secret configured: short grace period (up to 3s, polling every 500ms) waiting for the async webhook to land. If it arrives, redirect immediately. If the grace expires, fallback to step 4.
    • Still pending and no webhook_secret: skip the grace, go to step 4 directly.
  3. Step 4 — call syncStatus() on the driver, persist the result as a synthesized webhook event, and run it through the regular inbound pipeline. This closes the invoice and fires the outbound invoice.paid webhook to you (now signed with your own secret if you configured one — same payload shape as a regular webhook).
  4. Finally, redirect the customer to your real success_url / cancel_url.

Net effect: the moment the customer returns to your site, the invoice is already confirmed and your backend has received the invoice.paid webhook — even if the provider never delivered theirs. Cancel paths skip the polling and just redirect (customer abandoned; we let the invoice expire naturally).

Reseller API

All endpoints below require a user token for an account with role reseller, distributor, or admin.

GET /reseller/me

Returns the authenticated reseller (balance + fee percent).

curl https://payments.renovax.net/api/v1/reseller/me \
  -H "Authorization: Bearer $TOKEN"

Response 200:

{
  "id": 4,
  "username": "mario",
  "email": "[email protected]",
  "role": "distributor",
  "credits": 398.5,
  "fee_percent": 15
}
GET /reseller/users

Lists users created by you. Supports per_page.

curl "https://payments.renovax.net/api/v1/reseller/users?per_page=20" \
  -H "Authorization: Bearer $TOKEN"

Response 200:

{
  "data": [
    { "id": 123, "username": "alice42", "email": "[email protected]", "name": "Alice", "role": "user", "credits": 98.0, "created_at": "2026-04-21T..." }
  ],
  "meta": { "current_page": 1, "last_page": 1, "total": 1 }
}
POST /reseller/users

Creates a new user. If password is omitted, a random one is generated and returned once in the response.

curl -X POST https://payments.renovax.net/api/v1/reseller/users \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "alice42",
    "email": "[email protected]",
    "name": "Alice",
    "role": "user"
  }'

role is optional and defaults to user. Resellers may also create accounts with role reseller (sub-resellers) when their own role allows it.

Response 201:

{
  "user": {
    "id": 123,
    "username": "alice42",
    "email": "[email protected]",
    "name": "Alice",
    "role": "user",
    "credits": 0,
    "created_at": "2026-04-21T..."
  },
  "password": "Xk3p9..."   // only present when generated on the server
}
POST /reseller/credits/sell

Transfers credits from your balance to another user. A percent fee is deducted according to your role. Recipient accepts username or email.

curl -X POST https://payments.renovax.net/api/v1/reseller/credits/sell \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "recipient": "alice42",
    "amount": 100,
    "note": "initial load"
  }'

Response 200:

{
  "gross": 100,
  "fee": 2.0,
  "net": 98.0,
  "balance_after": 398.5,
  "recipient": {
    "id": 123,
    "username": "alice42",
    "email": "[email protected]",
    "name": "Alice",
    "role": "user",
    "credits": 98.0,
    "created_at": "2026-04-21T..."
  }
}
GET /reseller/credits/history

Returns your transfer ledger (transfer_out, transfer_in, transfer_fee).

curl "https://payments.renovax.net/api/v1/reseller/credits/history?per_page=30" \
  -H "Authorization: Bearer $TOKEN"

Response 200:

{
  "data": [
    { "id": 501, "reason": "transfer_out", "delta": -100.0, "balance_after": 398.5, "note": "initial load", "created_at": "2026-04-21T..." }
  ],
  "meta": { "current_page": 1, "last_page": 1, "total": 1 }
}

Webhooks (outbound to your server)

When an invoice changes state, RENOVAX Payments POSTs a JSON body to the webhook_url configured in your merchant settings. Every request is signed with your merchant webhook_secret so you can verify it came from us.

Headers

Every delivery carries these headers:

Content-Type: application/json
X-Renovax-Event-Id:    evt_8f3a0c1e4b2d47a9b5e6f1c2d3e4a5b6
X-Renovax-Event-Type:  invoice.paid
X-Renovax-Signature:   sha256=3f2c9a1b8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a
Event types
EventWhen it fires
invoice.paidPayment confirmed on-chain (crypto) or captured by provider webhook (Stripe/PayPal/Klarna/Revolut). Status confirmed.
invoice.authorizedCustomer authorized payment but funds were not yet captured. Applies only when the merchant configured the payment method with auto_capture=false (manual capture flow on Stripe / PayPal / Revolut / Klarna / Adyen / Braintree). Status authorized. The merchant captures from RENOVAX Payments panel, API, Telegram bot, or the provider portal — once captured, invoice.paid fires with the same invoice_id.
invoice.partialPayment received but amount is below expected (within configured tolerance). Status confirmed.
invoice.overpaidPayment received but amount exceeds expected. Status confirmed. Payload is identical to invoice.paidamount_received and amount_net reflect the actual received amount (including the excess), which is forwarded to the merchant. For forwarder wallets (unique address per invoice), overpayments are always accepted regardless of the accept_overpayments setting.
invoice.failedInvoice auto-cancelled by anti-fraud (Tor/VPN/datacenter/Scamalytics). Status failed.
invoice.expiredTTL reached without payment, or the provider's authorization window expired before capture. Status expired.
invoice.partial_settledAn accumulated partial payment was forwarded/settled to the merchant's destination (HD wallet or external forward driver). Status partially_settled. Payload includes the partial block with cumulative transfers.
invoice.refundedTerminal event when a previously confirmed payment was fully refunded via POST /merchant/invoices/{id}/refund or from the merchant panel. Status refunded.
invoice.partially_refundedTerminal event when a previously confirmed payment was partially refunded (refunded amount < captured amount). Status partially_refunded.
invoice.voidedMerchant actively voided an authorized payment from the RENOVAX Payments panel/API/Telegram. Differs from invoice.expired (which is provider timeout). Payload includes void_reason and voided_via. Status voided.
Payload example (invoice.paid)
{
  "event_type":          "invoice.paid",
  "invoice_id":          "019dbc6b-33a9-7145-82c7-a53cfe533dc8",
  "merchant_id":         42,
  "status":              "confirmed",
  "invoice_amount":      "100.00",
  "invoice_currency":    "USD",
  "amount_received":     "100.00000000",
  "currency":            "USDT",
  "fee":                 "2.50000000",
  "amount_net":          "97.50000000",
  "amount_received_fiat":"1800.00",
  "amount_net_fiat":     "1755.00",
  "fx_rate_used":        "18.000000",
  "fx_pair":             "USD/MXN",
  "customer_paid_fee":   false,
  "driver":              "crypto:USDT:ethereum",
  "tx_hash":             "0x7a8b9c…",
  "network":             "ethereum",
  "confirmations":       12,
  "provider_id":         null,
  "paid_at":             "2026-04-23T15:29:58Z",
  "confirmed_at":        "2026-04-23T15:29:58Z",
  "authorized_at":       "2026-04-23T15:24:11Z",
  "auth_expires_at":     "2026-04-30T15:24:11Z",
  "captured_at":         "2026-04-23T15:29:58Z",
  "captured_via":        "ui",
  "voided_at":           null,
  "voided_via":          null,
  "void_reason":         null,
  "void_reason_note":    null,
  "payer_ip":            "203.0.113.42",
  "payer_asn":           15169,
  "payer_country":       "MX",
  "metadata":            { "user_id": 4321 },
  "customer_name":       "John Doe",
  "customer_email":      "[email protected]",
  "billing":             { "first_name": "John", "last_name": "Doe", "address_line1": "350 5th Ave", "city": "New York", "state": "NY", "postal_code": "10118", "country": "US" },
  "shipping":            { "first_name": "John", "last_name": "Doe", "address_line1": "350 5th Ave", "city": "New York", "state": "NY", "postal_code": "10118", "country": "US" },
  "is_flexible":         false,
  "customer_note":       null
}

tx_hash / network / confirmations are only set for crypto payments; provider_id is set for Stripe/PayPal/Revolut/Klarna/Adyen/Braintree captures. amount_received_fiat, amount_net_fiat, fx_rate_used, fx_pair are only present when the merchant's currency differs from the payment currency. amount_net depends on your customer_pays_fee setting. For invoice.expired / invoice.failed most payment fields will be null. customer_name, customer_email, billing and shipping are echoed verbatim from what the merchant sent at invoice creation (all null when none were sent).

Timeline & audit fields

Set only for fiat drivers with deferred-capture support (stripe, paypal, revolut, klarna, adyen, braintree). Crypto and other drivers report null. Use these to show your own countdown for pending captures, audit who captured/voided, and persist the void reason in your CRM.

FieldTypeDescription
authorized_atISO-8601|nullWhen the customer authorized the payment. Set on the first authorized webhook event.
auth_expires_atISO-8601|nullWhen the provider's authorization window times out (Stripe ~7d, PayPal 3d, Revolut variable, Klarna 28d). Use to show your own deadline UI.
captured_atISO-8601|nullWhen the funds were actually captured.
captured_viaenum|null"ui" (RENOVAX Payments panel), "api" (your backend integration via POST /merchant/invoices/{id}/capture), "telegram" (one-click capture from the bot), "provider_portal" (you captured from Stripe Dashboard / PayPal / etc.), or "auto_capture" (atomic capture by the provider when auto_capture=true).
voided_atISO-8601|nullWhen the authorization was voided. null for invoices that reached confirmed.
voided_viaenum|null"ui", "api", or "telegram" when the merchant actively voided. null when the provider expired the auth on its own — in that case the invoice is expired, not voided.
void_reasonenum|null"customer_request", "out_of_stock", "fraud_suspected", or "other". Sent only on invoice.voided events.
void_reason_notestring|nullFree-text note (max 500 chars) the merchant attached when choosing "other" as the reason.
Payload example (invoice.voided)

Fired when the merchant actively voids an authorized payment via panel / API / Telegram. Differs from invoice.expired (which is the provider expiring the auth on its own).

{
  "event_type":          "invoice.voided",
  "invoice_id":          "019dbc6b-33a9-7145-82c7-a53cfe533dc8",
  "merchant_id":         42,
  "status":              "voided",
  "invoice_amount":      "100.00",
  "invoice_currency":    "USD",
  "currency":            "USD",
  "driver":              "stripe",
  "provider_id":         "pi_3N…",
  "authorized_at":       "2026-04-23T15:24:11Z",
  "auth_expires_at":     "2026-04-30T15:24:11Z",
  "voided_at":           "2026-04-23T18:05:00Z",
  "voided_via":          "ui",
  "void_reason":         "out_of_stock",
  "void_reason_note":    "SKU 12345 sold out",
  "captured_at":         null,
  "captured_via":        null,
  "paid_at":             null,
  "metadata":            { "user_id": 4321 }
}

is_flexible is true when the invoice was created with flexible: true (open amount, customer typed it on the checkout). For flexible invoices, invoice_amount still reflects the final value the customer chose. customer_note contains an optional message the customer left at checkout (max 500 chars, plain text); null when none was provided or for non-flexible invoices.

Partial transfers block

When the customer pays in multiple on-chain transfers (common with crypto: customer sends 40 USDT, then 60 USDT to reach a 100 USDT invoice), every detected transfer is recorded on the invoice. The partial block exposes that history to the merchant.

It is null when no partial transfers were accumulated (single-transfer payment or no payment at all). It is populated on these events: invoice.partial, invoice.paid (when the final amount was reached by accumulation), invoice.expired (with partial funds pending — customer left money under the threshold) and invoice.partial_settled.

"partial": {
  "total":   "100.00000000",
  "missing": "0.00000000",
  "count":   2,
  "transfers": [
    {
      "tx_hash":     "0x7a8b9c…",
      "amount":      "40.00000000",
      "block":       19482301,
      "from":        "0xPayerAddress…",
      "detected_at": "2026-04-23T15:24:11Z"
    },
    {
      "tx_hash":     "0x1d2e3f…",
      "amount":      "60.00000000",
      "block":       19482358,
      "from":        "0xPayerAddress…",
      "detected_at": "2026-04-23T15:29:58Z"
    }
  ]
}
FieldTypeDescription
totalstringSum of all detected partial amounts, in the payment currency. Decimal string with 8 fractional digits.
missingstringHow much is still missing to reach invoice_amount. "0" once the invoice is fully covered. On invoice.expired, this is what the customer never sent.
countintegerNumber of detected transfers. Always equals transfers.length.
transfers[]arrayEach detected on-chain transfer, in detection order.
transfers[].tx_hashstring|nullTransaction hash on the network.
transfers[].amountstring|nullAmount of this single transfer, in the payment currency. Decimal string.
transfers[].blockinteger|nullBlock height where the transfer was mined.
transfers[].fromstring|nullSender address as reported by the network.
transfers[].detected_atstring|nullISO-8601 timestamp of when our crypto detector picked up the transfer.
Payer fields

Forensic data captured from the payer's first genuine interaction with the checkout (page load / scan / method selection). Frozen on the invoice from that point onward — so retries and re-deliveries always carry the same values. Source: MaxMind GeoIP2 Country and ASN databases. All three may be null when no payer interaction was ever recorded (e.g. invoice.expired with no scan).

FieldTypeDescription
payer_ipstring|nullIPv4 or canonical IPv6 of the payer.
payer_asninteger|nullAutonomous System Number (e.g. 15169 = Google). null for unassigned ranges.
payer_countrystring|nullISO-3166-1 alpha-2 country code in uppercase (e.g. MX, US).
Signature verification

The signature is sha256=HMAC_SHA256(webhook_secret, raw_body). Compute it over the raw request body (not the parsed JSON) and compare with X-Renovax-Signature using a constant-time comparison.

Node.js (Express):

import crypto from 'crypto';
import express from 'express';

const app = express();
const SECRET = process.env.RENOVAX_WEBHOOK_SECRET;

// IMPORTANT: capture the raw body — body-parser JSON would stringify it back.
app.post('/webhooks/renovax',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const sig = req.header('X-Renovax-Signature') || '';
    const expected = 'sha256=' + crypto.createHmac('sha256', SECRET)
      .update(req.body).digest('hex');
    if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
      return res.status(400).json({ error: 'bad_signature' });
    }
    const event = JSON.parse(req.body.toString());
    // handle event.event, event.invoice, event.payment …
    res.json({ ok: true });
  });

PHP:

$secret  = getenv('RENOVAX_WEBHOOK_SECRET');
$body    = file_get_contents('php://input');
$sig     = $_SERVER['HTTP_X_RENOVAX_SIGNATURE'] ?? '';
$expect  = 'sha256=' . hash_hmac('sha256', $body, $secret);
if (!hash_equals($expect, $sig)) { http_response_code(400); exit; }
$event = json_decode($body, true);
// …

Python (Flask):

import hmac, hashlib, os
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ['RENOVAX_WEBHOOK_SECRET'].encode()

@app.post('/webhooks/renovax')
def hook():
    body = request.get_data()
    expected = 'sha256=' + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, request.headers.get('X-Renovax-Signature', '')):
        abort(400)
    # request.get_json() is safe now
    return {'ok': True}
Retries & idempotency
  • Respond with any 2xx within 10 seconds to acknowledge. Anything else (non-2xx, timeout, connection error) triggers a retry.
  • Retries follow exponential backoff: 30s, 2m, 10m, 1h, 6h, 24h (6 attempts total). After that the delivery is marked failed — inspect it in your merchant dashboard under Webhooks.
  • The X-Renovax-Event-Id is stable across retries. Use it as an idempotency key on your side: persist the event-id the first time you process the event and skip duplicates.
  • We never send the same event twice successfully — but network retries, user-initiated redeliveries, and the Test webhook button can all replay the same event-id.

Rotate your secret anytime from the merchant's Webhooks tab. The next delivery after the rotation is signed with the new key.

Errors

Errors return a JSON body with an error key and appropriate HTTP status:

{ "error": "invalid_token_type", "message": "This endpoint requires a merchant token." }
  • 401 — missing or invalid token
  • 402 — negative credit balance
  • 403 — token type or role not authorized
  • 404 — resource not found
  • 409 — duplicate (e.g. pending invoice with same amount)
  • 422 — validation or business-rule failure