zerofive

Developers

API reference

A small REST API with JSON in and out. Create invoices and subscriptions, give your users deposit addresses, pay them out from your balance, refund, and withdraw. Base URL: https://z5.gg/api/v1

On this page▾

Getting started

Introduction

Most integrations need two things:

  1. Create an invoice for an amount in dollars and send the customer to its hosted_url. They pick a coin, pay, and come back to your redirect_url.
  2. Listen for webhooks. We POST invoice.paid once the payment has enough confirmations. That is the moment to ship the order.

That covers one-off sales. There are three ways to get paid, and you can use any mix of them:

  • Buy. One payment for one order: an invoice and the hosted checkout.
  • Subscriptions. A plan that renews. Customers pay ahead, and we email a renewal invoice before their paid time runs out.
  • Wallet. For casinos, exchanges and apps with balances: each user gets permanent deposit addresses, and you send them payouts from your balance. The casino & wallet guide walks through the whole loop.
the whole integration
// 1. on checkout: create an invoice and send the customer to it
const res = await fetch("https://z5.gg/api/v1/invoices", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ZEROFIVE_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ amount: "49.00", order_id: order.id }),
});
const invoice = await res.json();
redirect(invoice.hosted_url);

// 2. on your webhook endpoint: fulfil when it's paid
if (event.type === "invoice.paid") fulfil(event.data.invoice.order_id);

Live demo

The real hosted pages, from a demo store in test mode. Nothing is charged and no coins move: press the simulate button and the payment confirms block by block, exactly as your customers will see it, receipt included.

Hosted checkout

An $89 order from a demo clothing shop: a hoodie and a cap. Pick a coin, then press “Pay exact”.

Subscription

A $12 / month plan. Pay ahead for a discount, then skip to the renewal.

Deposit page

Permanent addresses for a casino or wallet user. Simulate a deposit.

Authentication

Every request carries a secret key in the Authorization header as a bearer token. Create keys in Dashboard → Developers. A key is shown once when you create it, so store it somewhere safe; you can revoke it at any time.

  • sk_test_… keys work on test data only: test invoices, test balances, simulated payments.
  • sk_live_… keys move real money. Only the account owner can create them, after confirming their email, and every new live key (or change to one) is emailed to the owners.

A key belongs to the person who made it: it stops working if they leave the team or their account is disabled. The only endpoint that needs no key is GET /v1/networks.

bash
curl https://z5.gg/api/v1/balances \
  -H "Authorization: Bearer sk_test_4eC39HqLyjWDarjtT1zdp7dc…"

Permissions

Every key can read everything in its mode. Anything that creates or changes something needs the matching permission, picked when you create the key and changed later by the owner under the key's Access. New keys start with Invoices only. A key with nothing ticked is read-only: the safe kind to hand an accountant or a reporting tool (it can still make exports). A request the key isn't allowed to make gets a 403 with type permission.

Permissions

Invoicesinvoices
Create and change invoices (including quote and cancel), customers, plans, subscriptions and tips.
Payouts and refundspayouts
Pay your users (POST /v1/payouts), refund invoices, and approve or reject payouts your rules held.
Withdrawalswithdrawals
Send your balance to your own saved payout addresses.
Payout rulesrules
Change your payout rules. Also needs a live key created by the store owner: a test key gets a 400 and a team member's key a 403.

IP allowlist and coins

  • Allowed IPs. A key can be limited to up to 50 IPv4 addresses, IPv4 ranges such as 203.0.113.0/24, or IPv6 addresses. A request from anywhere else gets a 403 permission error that names the IP it came from.
  • Accepted coins. A key can be limited to some coins, e.g. one site's key only takes USDT on Tron and Base. Invoices, tips and customers made with it only offer those, inside what your store accepts under Settings → Payments. They keep that set even if you edit the key later.
Secret keys are for your server. Never put them in a browser, an app bundle or a public repository.

Test mode

Test and live are two separate worlds with the same API. Everything made with a test key (invoices, customers, payments, balances, withdrawals) is invisible to live keys and the other way round. The dashboard has a test/live switch that shows one or the other.

Test invoices get real-looking addresses, but nothing on chain is watched for them. Instead the hosted checkout shows a Simulate the payment panel (exact, partial or overpayment). A simulated payment confirms block by block over about 20 seconds, credits your test balance minus the fee, fires your webhooks and sends the emails, exactly like a real one. Its simulated is true and its tx_hash starts with sim_. Customer deposits can be simulated the same way from the customer's page in the dashboard or their hosted deposit page.

Test payouts, refunds and withdrawals are marked sent within a few seconds with a fake sim_… hash. Your payout rules still apply, so you can try the review flow. The live-only safeguards don't: sanctions screening, saved payout addresses and the platform's review of large amounts.

Test emails only go to your own people: your team and the addresses you added and confirmed under Settings → Emails → Recipients. A receipt for a test customer with any other address isn't sent; it's kept in the email log on that page, where you can open it and see exactly what the customer would get. Test emails are marked [TEST] and capped at 100 a day per store.

Never send real coins to a test-mode address. They aren't watched and won't be credited.

Errors

Errors use ordinary HTTP status codes and always have the same body. The message is written to be shown to a person, e.g. how much balance you have when a payout is too big. Branch on type and the status code, not on the message text.

Error types

invalid_request400
Something in the request is missing or wrong: a bad field, a body that isn't a JSON object, an unknown starting_after, not enough balance. The message says what.
authentication401
No key, a malformed key, a revoked one, or one whose creator no longer has access.
permission403
The key doesn't have the permission for this, or the request came from an IP the key doesn't allow. Changing payout rules also needs a key the store owner created.
store_suspended403
The store is suspended: reads, cancels and rejects still work, anything that collects or moves money doesn't.
not_found404
No such object for this key's store and mode. A test key can't see live objects, and the other way round.
rate_limited429
Too many requests. Back off and try again.
region_restricted451
The request came from a country or region zerofive isn't available in.
server500
Our fault. Safe to retry.
400 Bad Request
{
  "error": {
    "type": "invalid_request",
    "message": "amount must be between 0.50 and 1,000,000.00 USD."
  }
}

Pagination

List endpoints return the newest first as {"object": "list", "data": [...], "has_more": true}. Pass limit (1–100, default 20) and, for the next page, starting_after with the id of the last object you got. Keep going while has_more is true. Payments are ordered by when they were first seen, everything else by when it was created.

Every list takes a date range: created[gte], created[gt], created[lte] and created[lt], each an ISO 8601 date or time (2026-10-01, 2026-10-01T12:00:00Z) or Unix seconds. /v1/invoices also filters by status, customer and order_id; /v1/payments by status, invoice, customer, network and asset.

The few lists that return everything at once (/v1/balances, /v1/networks, a customer's /addresses) have the same shape with has_more: false.

bash
curl "https://z5.gg/api/v1/invoices?limit=50&starting_after=inv_3fTq9WbYk2Lm8nPx4RcZ" \
  -H "Authorization: Bearer $ZEROFIVE_KEY"

Rate limits & retries

  • Rate limit. 300 requests a minute per key. Past that you get a 429 rate_limited error; wait a few seconds and retry. For payment status, prefer webhooks over tight polling loops.
  • Safe retries for money. POST /v1/payouts, POST /v1/withdrawals and POST /v1/invoices/:id/refund take an external_id, unique per store and mode. Sending the same request again returns the first object instead of paying twice. Reusing an external_id for a different amount, coin, network, address, customer or invoice is refused with a 400.
  • Invoices. An order_id can only be used once per store and mode; a second invoice with it gets a 400. So a retry after a timeout can't make a duplicate; if it gets that 400, the first one went through and is in GET /v1/invoices.
  • Customers. POST /v1/customers with an external_id that already exists returns that customer, so it's safe to call on every login.

Accepting payments

Hosted checkout

Every invoice has a hosted_url (https://z5.gg/pay/inv_…). Send the customer there, or show it as a link or QR code (try it in the live demo). The page:

  1. lets the customer pick a coin and network;
  2. locks a quote for 30 minutes and shows the invoice's own deposit address with a QR code;
  3. tracks the payment live: detected, confirming (x of y), paid;
  4. sends the customer to your redirect_url when it's paid, and emails them a receipt if you set customer_email.

Paid invoices also have a public receipt at receipt_url that the customer can print, and download as a PDF at receipt_url + /pdf. Short payments, overpayments and late payments are covered in the payment lifecycle.

Payment lifecycle

What happens between "the customer picked a coin" and "the money is in your balance", and the edge cases you'll meet in production.

  1. Quote. Picking a coin locks a rate for 30 minutes (cut short by the invoice's own expiry, but always at least 10 minutes, so a quote taken just before the invoice expires can outlast it) and gives the invoice its own deposit address for that network. The invoice goes from open to pending. Amounts are rounded up to a precision people can type.
  2. Detected. As soon as a transfer to the address is seen, the invoice is confirming and invoice.payment_detected fires. Don't ship yet.
  3. Confirmed. Once it has enough confirmations (fewer for payments under $1,000, see networks), the payment is credited to your balance minus the fee and the invoice becomes paid or partially_paid. invoice.paid is the signal to fulfil.

Edge cases

Short paymentpartially_paid
Up to 0.5% short still counts as paid. More than that leaves the invoice Partially paid, fires invoice.partially_paid and emails the customer what's left. They can send the rest to the same address; payments add up, and the invoice turns paid once the total is covered. A partially paid invoice doesn't expire on its own.
Overpaymentoverpaid: true
More than 1% over sets overpaid. The whole amount is credited to your balance, and the customer can ask for the extra back themselves (see refunds).
Quote ran outpending
A payment mined after quote_expires_at is valued at the rate when it arrived, not the locked one, so a volatile coin may leave it partially paid. Call POST /v1/invoices/:id/quote (or let the customer pick again on the checkout) for a fresh rate.
Late paymentexpired
Open and pending invoices expire at expires_at (invoice.expired). The address stays watched for 7 more days: a payment in that time is still credited, and an expired invoice it covers becomes paid.
Large paymentconfirming
Live payments of $1,000 or more can stay confirming for up to about 60 minutes after their confirmations, then are screened again before they're credited. See screening & holds.
Flagged senderheld, declined
Money from a sanctioned or frozen wallet isn't credited: the payment is held (payment.held), and the invoice stays unpaid. See screening & holds.
Refunded invoicepaid
Once an invoice has a refund, it can't be quoted or paid again. Anything sent to it later is still credited to you, but never makes it paid; create a new invoice instead.

Popup & embedded checkout

The same checkout can stay on your site. Add one script, then open an invoice's hosted_url in a popup over your page or put it inside the page itself. Coins, quotes, confirmations and receipts work exactly as on the full page.

html
<script src="https://z5.gg/js/z5.js"></script>

Popup. Opens over your page; the customer can close it with the ×, Escape or a click outside. Any link with data-zerofive does this by itself. Zerofive.open returns { close } if you want to close it from code.

popup
// a link: <a href="https://z5.gg/pay/inv_…" data-zerofive>Pay with crypto</a>
// or from code, after your server created the invoice:
const popup = Zerofive.open(invoice.hosted_url, {
  onPaid: ({ invoice }) => showThanks(invoice), // invoice is the id, e.g. "inv_3fTq…"
  onClose: ({ paid }) => { if (!paid) console.log("closed before paying") },
});

Embedded. Inside any element on your page. The frame grows and shrinks with the checkout, so there's never a scrollbar inside it. Zerofive.mount returns { unmount }.

embedded
// <div id="pay"></div>
Zerofive.mount("#pay", invoice.hosted_url, {
  onStatus: (status) => console.log(status), // "pending", "confirming", "paid"…
  onPaid: ({ invoice, redirect_url }) => (location.href = "/thanks"),
});

Options (both)

onPaidfunction
Called once with { invoice, redirect_url } when the invoice is paid. invoice is its id.
onStatusfunction
Called with the invoice status when the checkout loads and each time it changes.
onClosefunction
Popup only. Called with { paid } when it closes.
onDepositfunction
Deposit pages only. Called with { payment, status, network, asset, amount, usd } when a transfer shows up (confirming) and again when it's credited.

An invoice's hosted_url, a tip page link or a customer's deposit_page_url can be opened this way (a deposit page shows the customer's permanent addresses with a coin and network picker, and their recent deposits). Without onPaid, the page goes to the invoice's redirect_url once it's paid. Either way, the browser is only a hint: confirm the payment on your server with the invoice.paid webhook or GET /v1/invoices/:id before you ship anything.

Your own payment page

Don't want our checkout at all? Pick the coin yourself and show the address, amount and QR code in your own design. Payments are still detected, confirmed and credited the same way, and you get the same webhooks.

One-off payments. Pass network and asset when you create the invoice (or later with POST /v1/invoices/:id/quote, which also gets a fresh rate once a quote runs out). The payment object then has everything to show:

bash
curl https://z5.gg/api/v1/invoices \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"amount": "25.00", "network": "tron", "asset": "USDT", "order_id": "A-1042"}'
payment
{
  "network": "tron",
  "asset": "USDT",
  "address": "TQn9Y2…kD7uWe",
  "amount_due": "25",
  "quote_expires_at": "2026-10-21T10:30:00.000Z",
  "uri": "TQn9Y2…kD7uWe",
  "qr_url": "https://z5.gg/pay/inv_…/qr"
}

Show amount_due and address, and qr_url as an image (<img src="…">, an SVG with the Z5 mark in the middle). It opens the wallet with the amount filled in; add ?type=address for a code with just the address, for exchanges and wallets that can't read payment links. uri is the same payment link as text, for your own QR code or an "Open in wallet" button. It carries what's still owed:

  • bitcoin:, litecoin:, dogecoin: and bitcoincash: (BIP21, ?amount= in coins)
  • ethereum: (EIP-681 with the chain id, for ETH and tokens on every EVM network; amounts in base units)
  • solana: (Solana Pay, with spl-token= for tokens) and ton://transfer/ (with jetton= for USDT)
  • Tron has no payment-link standard wallets agree on, so it's the plain address there.

Show the network's warning next to the address (e.g. "TRC-20 only", see networks), and watch status with webhooks or by polling the invoice. When quote_expires_at passes, get a fresh quote before showing the amount again.

Wallets and deposits. Each customer's permanent addresses come from GET /v1/customers/:id/addresses, each with its own uri and qr_url. Those never change, so you can show them anywhere, for as long as you like. Or skip building it: open the customer's deposit_page_url with Zerofive.open and listen for onDeposit.

Tips & donations

Turn on a tip page under Dashboard → Tips and your audience can tip or donate in crypto at https://z5.gg/tip/yourname: preset or custom amounts, a name, a message, an optional goal with a progress bar, and a list of recent supporters if you want one. Every tip is an invoice with source: "tip", so it lands in your balance like any payment and the supporter can get a receipt.

Ready-made: links, popup, embed

Prefill a link with ?amount=25, &name= and &message= (the link generator in the dashboard builds these, with a QR code); the supporter can still change everything. With z5.js the tip page opens as a popup or sits inside your own page:

html
<script src="https://z5.gg/js/z5.js"></script>

<a href="https://z5.gg/tip/yourname?amount=10" data-zerofive>Tip $10</a>   <!-- popup -->
<div id="tips"></div>
<script>Zerofive.mount("#tips", "https://z5.gg/tip/yourname")</script>   <!-- inline -->

Your own tip page, no server

From a static site, post straight to your tip page's endpoint (open to any origin, no key). Your tip page must be turned on, and its rules apply: smallest tip, required name or message. Add network and asset and the response already has the address, the exact amount and a QR code to show in your own design; leave them out and send the supporter to hosted_url. Then poll status_url until status is paid. Errors here are a plain { "error": "…" }, and each visitor can start 20 tips per 10 minutes.

browser
const tip = await fetch("https://z5.gg/api/tip/yourname", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ amount: 5, name: "Satoshi", message: "GG", network: "tron", asset: "USDT" }),
}).then((r) => r.json());

// tip.payment: { address, amount_due, uri, qr_url, quote_expires_at, ... }
img.src = tip.payment.qr_url;
const { status } = await fetch(tip.status_url).then((r) => r.json()); // "pending" … "paid"

Add "test": true while you build: you get a test tip you can pay with the simulate button on its checkout.

From your server: the tips API

POST/api/v1/tips

Parameters

amountstring | numberrequired
In USD, from 0.50 to 100,000.
namestring
Who it's from, up to 40 characters. Shown publicly unless you hide the tip.
messagestring
Up to 500 characters.
emailstring
For the supporter's receipt; never shown.
network, assetstring
Pick the coin now: the response then has the address, amount, uri and qr_url.
redirect_urlstring
Where the checkout goes after paying (default: your tip page).
metadataobject
Your own data, e.g. which stream or creator it's for.

Works with the tip page on or off, and your own rules apply (the page's required fields and minimum don't). A tip stays open for an hour. Returns the tip object (its id is an invoice id, so the invoice endpoints work on it too):

The tip object
{
  "id": "inv_7Hq2…",
  "object": "tip",
  "livemode": true,
  "status": "paid",
  "amount": "25.00",
  "currency": "USD",
  "name": "Satoshi",
  "message": "Love the streams, keep going!",
  "email": null,
  "hidden": false,
  "metadata": { "stream": "friday" },
  "payment": {
    "network": "tron",
    "asset": "USDT",
    "address": "TQ5v…",
    "rate_usd": 1,
    "amount_due": "25",
    "amount_paid": "25",
    "quote_expires_at": "2026-10-21T18:31:40.000Z",
    "uri": "TQ5v…",
    "qr_url": "https://z5.gg/pay/inv_7Hq2…/qr"
  },
  "hosted_url": "https://z5.gg/pay/inv_7Hq2…",
  "receipt_url": "https://z5.gg/receipt/…",
  "expires_at": "2026-10-21T19:01:40.000Z",
  "paid_at": "2026-10-21T18:02:11.000Z",
  "created_at": "2026-10-21T18:01:40.000Z"
}
GET/api/v1/tips

Your tips, newest first. status=paid for the ones that came in; paginated like every list.

GET/api/v1/tips/:id
POST/api/v1/tips/:id

{ "hidden": true } keeps a tip off your tip page, the public feed and the overlay (or false to show it again).

Supporter lists, alerts and webhooks

  • GET https://z5.gg/api/tip/yourname/feed: recent paid tips as JSON ({ store, total, count, goal, tips: [{ id, name, message, amount, at }] }), open to any origin, for your own supporter list. Optional limit (up to 50), since (milliseconds timestamp) and test=1 for test tips. Public when your tip page shows supporters; otherwise add your overlay key. Hidden tips are left out.
  • Stream alerts: the overlay link from Dashboard → Tips, as a browser source in OBS or Streamlabs. Each new tip pops up with name, amount and message.
  • Webhooks: tip.received with the tip object when a tip is paid, as well as the usual invoice.paid.

Invoices

An invoice asks for an amount in US dollars. The customer chooses what to pay with on the hosted checkout, or you pick it for them. payment is null until a coin is picked; its amount_paid counts against the current quote. amount_paid_usd is everything received so far.

Statuses

openstatus
Created, no coin picked yet.
pendingstatus
Coin picked, quote locked, waiting for the payment.
confirmingstatus
A payment was seen and is gathering confirmations.
partially_paidstatus
Payments confirmed, but not enough yet. The customer can send the rest.
paidstatus
The full amount has confirmed. Final.
expiredstatus
Nothing arrived before expires_at. A late payment within 7 days can still make it paid.
canceledstatus
You canceled it before any payment, its subscription closed it, or the only payment on it was declined after screening.
The invoice object
{
  "id": "inv_3fTq9WbYk2Lm8nPx4RcZ",
  "object": "invoice",
  "livemode": false,
  "status": "paid",
  "amount": "49.00",
  "currency": "USD",
  "subtotal": "49.00",
  "tax": null,
  "customer_country": null,
  "customer_tax_id": null,
  "amount_paid_usd": "49.00",
  "description": "Pro plan, 1 month",
  "order_id": "A-1042",
  "customer": null,
  "customer_email": "[email protected]",
  "subscription": null,
  "metadata": { "plan": "pro" },
  "payment": {
    "network": "tron",
    "asset": "USDT",
    "address": "TQ5v…",
    "rate_usd": 1,
    "amount_due": "49",
    "amount_paid": "49",
    "quote_expires_at": "2026-10-20T10:42:00.000Z",
    "uri": "TQ5v…",
    "qr_url": "https://z5.gg/pay/inv_3fTq9WbYk2Lm8nPx4RcZ/qr"
  },
  "overpaid": false,
  "hosted_url": "https://z5.gg/pay/inv_3fTq9WbYk2Lm8nPx4RcZ",
  "receipt_url": "https://z5.gg/receipt/9Hk…",
  "redirect_url": "https://shop.example.com/thanks",
  "merchant": { "id": "mer_…", "name": "Sam's Shop" },
  "expires_at": "2026-10-20T11:12:00.000Z",
  "paid_at": "2026-10-20T10:14:31.000Z",
  "created_at": "2026-10-20T10:12:00.000Z"
}
POST/api/v1/invoices

Creates an invoice and returns it.

Parameters

amountstring | numberrequired
In USD, e.g. "49.00". Between 0.50 and 1,000,000.
descriptionstring
Shown on the checkout and the receipt. Up to 300 characters.
order_idstring
Your reference, up to 120 characters. Unique per store and mode: a second invoice with the same order_id is refused.
customerstring
A cus_… id to attach the invoice to. Their email is used if you don't pass customer_email.
customer_emailstring
Where the receipt (and a partial-payment notice) goes.
customer_countrystring
Two-letter country code (DE, US…) for tax. If you charge tax and leave it out, the checkout asks.
customer_tax_idstring
The buyer's VAT or tax ID, printed on the receipt.
redirect_urlstring
An http(s) URL where the checkout sends the customer after paying.
networkstring
With asset: pick the coin now instead of on the checkout, for your own payment page. The response has the address, amount and QR.
assetstring
The symbol in capitals: USDT, USDC, ETH, BTC… Must be on for that network (GET /v1/networks), accepted by your store and allowed for the key.
metadataobject
Up to 4 KB of your own data, returned as is.
expires_in_minutesnumber
How long the invoice stays open. Default 60; values outside 10 to 10,080 (7 days) are clamped to that range.
bash
curl https://z5.gg/api/v1/invoices \
  -H "Authorization: Bearer $ZEROFIVE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "49.00",
    "description": "Pro plan, 1 month",
    "order_id": "A-1042",
    "customer_email": "[email protected]",
    "redirect_url": "https://shop.example.com/thanks",
    "metadata": { "plan": "pro" }
  }'
node
const invoice = await fetch("https://z5.gg/api/v1/invoices", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ZEROFIVE_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ amount: 49, order_id: "A-1042" }),
}).then((r) => r.json());

console.log(invoice.hosted_url);
GET/api/v1/invoices

Lists invoices, newest first, tips and subscription invoices included. Filter with status; paginate with limit and starting_after.

GET/api/v1/invoices/:id

Returns one invoice. Poll this if you can't receive webhooks.

POST/api/v1/invoices/:id/quote

Picks the coin for an invoice with network and asset (both required), or gets a fresh rate once a quote ran out. Works on open, pending and partially_paid invoices before they expire; a partly paid one is quoted for what's left. Asking again for the same coin while the quote holds returns it unchanged. Returns the invoice with payment filled in: address, amount, uri and qr_url. See your own payment page.

POST/api/v1/invoices/:id/cancel

Cancels an open or pending invoice. Invoices that already received money can't be canceled.

Subscriptions

Crypto can't be charged later the way a saved card can, so subscriptions here are paid ahead. A plan is a price per interval. A subscription is paid up to paid_through, and every paid invoice moves that date on by the periods it covered.

A subscription can be for several plans at once, each with a quantity (say Pro × 1 and Extra seat × 3), billed together on one invoice. These are its items. amount is one period of all of them together, and plan is the first item's plan.

  1. Create a subscription and send the customer to pay_url. When that first payment confirms it becomes active.
  2. About three days before paid_through (half a period on shorter plans) we email the customer a renewal invoice and send you subscription.payment_due.
  3. Unpaid by paid_through, it's past_due and we remind them. After the plan's grace_days it's canceled.

Customers can pay several periods at once, at the plan's prepay discount, from their subscription page (manage_url). It also lets them cancel, and it's linked in every renewal email. Give access while the status is active or past_due.

Statuses

incompletestatus
Created, first payment not made yet.
activestatus
Paid up to paid_through.
past_duestatus
The paid time ran out and the renewal is open. Still inside the grace period.
canceledstatus
Ended: canceled by you or the customer, or not renewed in time (cancel_reason says which). A requested cancellation is final; one for not paying comes back to active if the overdue invoice is paid late, with a new billing anchor.
In test mode, the subscription's page in the dashboard has a button that skips to the next step (renewal invoice, then past due, then the end of the grace period), so you can try the whole cycle in a minute.
The subscription object
{
  "id": "sub_F74I3tdTjDDJ2HqlEPNs",
  "object": "subscription",
  "livemode": false,
  "status": "active",
  "plan": {
    "id": "plan_HVeqUYWyAdcS6Rv2qgxC",
    "object": "plan",
    "livemode": false,
    "name": "Pro plan",
    "description": null,
    "amount": "20.00",
    "currency": "USD",
    "interval": "month",
    "interval_count": 1,
    "prepay": [
      { "periods": 6, "percent_off": 10, "amount": "108.00" },
      { "periods": 12, "percent_off": 15, "amount": "204.00" }
    ],
    "grace_days": 7,
    "active": true,
    "custom": false,
    "switchable": true,
    "created_at": "2026-10-01T09:00:00.000Z"
  },
  "items": [
    // each item has the full plan object, as in "plan" above
    { "plan": { "id": "plan_HVeqUYWyAdcS6Rv2qgxC", "name": "Pro plan", … }, "quantity": 1 },
    { "plan": { "id": "plan_Q2xmT8bLwR4nVc7sKd9E", "name": "Extra seat", … }, "quantity": 3 }
  ],
  "amount": "35.00",
  "interval": "month",
  "interval_count": 1,
  "pending_change": null,
  "customer": null,
  "customer_email": "[email protected]",
  "paid_through": "2027-04-20T22:19:05.000Z",
  "periods_paid": 6,
  "started_at": "2026-10-20T22:19:05.000Z",
  "cancel_at_period_end": false,
  "canceled_at": null,
  "cancel_reason": null,
  "latest_invoice": "inv_PVfbm7wQKnS55qsch1jz",
  "pay_url": null,
  "manage_url": "https://z5.gg/s/aEA1dMy6HfEz53AAKzh…",
  "metadata": { "user": "42" },
  "created_at": "2026-10-20T22:15:40.000Z"
}
POST/api/v1/plans

Creates a plan. The price can't change later: make a new plan and move people over.

Parameters

namestringrequired
Shown on the checkout, the customer's page and emails.
amountstring | numberrequired
Price per period in USD.
intervalstringrequired
day, week, month or year.
interval_countnumber
Intervals per billing period, e.g. 3 with month for quarterly. Default 1; at most 365 days, 52 weeks, 36 months or 3 years.
prepayarray
Up to 5 options of { periods, percent_off }: 2 to 36 periods, 0 to 50% off.
grace_daysnumber
Days a lapsed subscription stays past_due before it's canceled. 0 to 30, default 7.
descriptionstring
Optional, shown on the customer's page.
switchableboolean
Customers on another plan can switch to this one themselves from their subscription page (only subscriptions with one item). Default false.

GET /api/v1/plans lists them (?active=true or false to filter), GET /api/v1/plans/:id gets one, and POST /api/v1/plans/:id changes the name, description, prepay options, grace_days or switchable, or archives it with { "active": false } (true brings it back). Archived plans take no new subscriptions; existing ones keep renewing.

Plans made from a custom price on a subscription have custom: true. They belong to that one subscription and are left out of the list unless you ask for ?custom=true.

bash
curl https://z5.gg/api/v1/plans \
  -H "Authorization: Bearer $ZEROFIVE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Pro plan",
    "amount": "20.00",
    "interval": "month",
    "prepay": [
      { "periods": 6, "percent_off": 10 },
      { "periods": 12, "percent_off": 15 }
    ],
    "grace_days": 7
  }'
POST/api/v1/subscriptions

Creates a subscription in incomplete with its first invoice. Send the customer to pay_url. Pass either plan (with an optional quantity) or items.

All items have to renew on the same period (same interval and interval_count), since they're billed together. An item with a price instead of a plan makes a custom plan just for this subscription. The prepay options on offer are the lengths every item's plan has.

Parameters

planstring
A plan_… id. Required unless you send items.
quantitynumber
With plan: how many of it, 1 to 1,000. Default 1.
itemsarray
Up to 20 of { plan, quantity } or { price: { name, amount, interval, interval_count }, quantity }. Quantity 1 to 1,000, default 1.
customer_emailstringrequired
Renewal invoices and reminders go here. Optional when the customer you pass has an email.
customerstring
A cus_… id to link it to.
periodsnumber
How many periods the first payment covers: 1, or a prepay option every item's plan offers.
redirect_urlstring
Where the checkout sends them after the first payment. Renewals go back to their subscription page.
metadataobject
Up to 4 KB of your own data.
bash
curl https://z5.gg/api/v1/subscriptions \
  -H "Authorization: Bearer $ZEROFIVE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "plan": "plan_HVeqUYWyAdcS6Rv2qgxC",
    "customer_email": "[email protected]",
    "periods": 6,
    "redirect_url": "https://app.example.com/billing",
    "metadata": { "user": "42" }
  }'

# several plans, or a custom price just for this customer
curl https://z5.gg/api/v1/subscriptions \
  -H "Authorization: Bearer $ZEROFIVE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "plan": "plan_HVeqUYWyAdcS6Rv2qgxC", "quantity": 1 },
      { "plan": "plan_Q2xmT8bLwR4nVc7sKd9E", "quantity": 3 },
      {
        "price": { "name": "Acme deal", "amount": "49.00", "interval": "month", "interval_count": 1 },
        "quantity": 1
      }
    ],
    "customer_email": "[email protected]"
  }'
GET/api/v1/subscriptions

Lists subscriptions, newest first. Filter with status or customer. GET /api/v1/subscriptions/:id gets one.

POST/api/v1/subscriptions/:id/cancel

Stops renewals: it stays active with cancel_at_period_end: true until paid_through, then ends, and any open renewal invoice is canceled. Send { "at_period_end": false } to end it now; one that was never paid (incomplete) always ends now. Paid time isn't refunded automatically; refund the invoice if you want to. POST /api/v1/subscriptions/:id/resume undoes a cancel that hasn't happened yet.

POST/api/v1/subscriptions/:id/change

Changes what a subscription is for: another plan, more or fewer seats, or a custom price. Send items (same shape as when creating) or plan with quantity. Returns { subscription, invoice }, where invoice is the one to pay for the change, or null.

Parameters

itemsarray
The new items. Replaces all current ones.
planstring
Shortcut for a single item. Required unless you send items.
quantitynumber
With plan, 1 to 1,000. Default 1.
timingstring
now or period_end. Default: now when it costs more per period or has a different period length, period_end when it's cheaper.

Now, while active. The unused part of the current paid time counts as credit, at what the customer actually paid per period (prepay discounts included). We create an invoice for the rest of the current period at the new price, minus that credit. Once it's paid the items change and the renewal date stays where it was. Under $0.50 due, it changes right away with no invoice.

A different period length (monthly to yearly, say) works the same way, except the invoice is the new plan's first full period minus the credit, and a new period starts when it's paid. If the credit is worth more than that whole period, switch at period_end instead.

The change invoice is open for up to 3 days, and never past the end of the paid time. If it isn't paid by then, the change lapses and nothing else happens. Asking for now on a cheaper plan with the same period length is refused, since paid time isn't refunded.

At period end. The next renewal invoice is for the new items, and the change happens when that renewal is paid.

Not paid yet or past due. An incomplete or past_due subscription changes straight away, and invoice is the invoice to pay for the new items.

Only one change waits at a time; a new one replaces it, and you'll see it in pending_change. Canceling the subscription drops a waiting change. One that's set to cancel at period end can't be changed until you resume it. Each step (scheduled, requested, applied, called off) sends subscription.updated.

Example. A customer on Supporter ($5/month) moves to Pro ($15/month) 10 days into a 30-day month. 20 of 30 days are left, so $3.33 of what they paid is unused. The rest of the month on Pro costs $10.00, so the invoice is $10.00 − $3.33 = $6.67. Once it's paid they're on Pro, the renewal date doesn't move, and the next renewal is $15.
bash
curl https://z5.gg/api/v1/subscriptions/sub_F74I3tdTjDDJ2HqlEPNs/change \
  -H "Authorization: Bearer $ZEROFIVE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "plan": "plan_7KpWn3RtYc9LbXq2Md4S", "quantity": 1 }'
Response
{
  "subscription": {
    "id": "sub_F74I3tdTjDDJ2HqlEPNs",
    "object": "subscription",
    "status": "active",
    "amount": "5.00",
    "paid_through": "2026-11-20T22:19:05.000Z",
    "pending_change": {
      "id": "chg_8YtRk2NqVb5LmXc3Wd7P",
      "items": [{ "plan": { "id": "plan_7KpWn3RtYc9LbXq2Md4S", "name": "Pro", … }, "quantity": 1 }],
      "timing": "now",
      "amount_due": "6.67",
      "credit": "3.33",
      "invoice": "inv_Ux3Lr9TqZk6NwB2mHc8Y",
      "pay_url": "https://z5.gg/pay/inv_Ux3Lr9TqZk6NwB2mHc8Y",
      "created_at": "2026-10-30T22:19:05.000Z"
    },
    …
  },
  "invoice": { "id": "inv_Ux3Lr9TqZk6NwB2mHc8Y", "object": "invoice", "amount": "6.67", … }
}
POST/api/v1/subscriptions/:id/change/cancel

Calls off a waiting change (one waiting for its payment or for the next renewal) and its invoice if nothing was paid on it yet. Returns the subscription. Works while your store is suspended.

Customers & deposits

A customer has one permanent deposit address per address family: one EVM address serves Ethereum, Base, Arbitrum, Optimism, Polygon, BNB Chain, Avalanche, Plasma, Arc and Tempo; Tron, Bitcoin, Litecoin, Dogecoin, Bitcoin Cash, Solana and TON each have their own. Addresses are created the first time you ask for them.

Anything sent to them in a coin listed in the address's assets, any amount, any time, is credited to your balance minus the fee and reported as deposit.detected, then deposit.confirmed, with the customer's id. Transfers worth under ten cents are ignored as dust. If the customer has an email, they get a confirmation for each deposit.

A customer only gets addresses for networks your store (and the key that created them) accepts. Because one EVM address serves every EVM network, tell users which network to use: a coin sent on a network that's off isn't credited.

Each customer also gets a hosted deposit page at deposit_page_url (/d/<token>) listing their addresses with QR codes and their recent deposits. Link to it if you don't want to build your own.

POST/api/v1/customers

Parameters

emailstring
Gets a confirmation email for each deposit.
namestring
For your own reference, up to 120 characters.
external_idstring
Your user id, up to 120 characters. Creating a customer with an external_id that exists returns the existing one unchanged, so it's safe to call on every login.
metadataobject
Up to 4 KB of your own data.
GET/api/v1/customers

Newest first, paginated. ?external_id=user_812 finds a customer by your own id.

GET/api/v1/customers/:id
GET/api/v1/customers/:id/addresses

Every accepted network as {network, name, assets, address, uri, qr_url}, in a list. Networks of one family share the address. qr_url is an SVG of the address, for your own deposit page.

GET/api/v1/customers/:id/addresses/:network

One network, e.g. tron, btc, base (see networks), with the same fields plus confirmations_required, confirmations_required_small (deposits under $1,000) and a warning to show next to it. A network that isn't accepted for this customer gets a 400.

bash
# create (or fetch, with the same external_id)
curl https://z5.gg/api/v1/customers \
  -H "Authorization: Bearer $ZEROFIVE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"external_id": "user_812", "email": "[email protected]"}'

# their USDT-on-Tron address
curl https://z5.gg/api/v1/customers/cus_…/addresses/tron \
  -H "Authorization: Bearer $ZEROFIVE_KEY"
The customer object
{
  "id": "cus_Qm4p…",
  "object": "customer",
  "livemode": false,
  "email": "[email protected]",
  "name": null,
  "external_id": "user_812",
  "metadata": {},
  "deposit_page_url": "https://z5.gg/d/7Fh…",
  "deposits_count": 3,
  "deposits_usd": "250.00",
  "created_at": "2026-10-20T09:00:00.000Z"
}

Listing deposits

Deposits are payment objects that went to a customer's permanent address (not to an invoice, and not your own top-ups). Use these to reconcile or to show a user their history.

GET/api/v1/deposits

Filter with customer and status (any payment status, usually confirming or credited); paginated.

GET/api/v1/customers/:id/deposits
GET/api/v1/customers/:id/payouts

Everything sent to that customer, newest first: payouts, and refunds of invoices attached to them.

bash
# every customer deposit, newest first
curl "https://z5.gg/api/v1/deposits?limit=50" \
  -H "Authorization: Bearer $ZEROFIVE_KEY"

# one user's deposits and payouts
curl https://z5.gg/api/v1/customers/cus_…/deposits -H "Authorization: Bearer $ZEROFIVE_KEY"
curl https://z5.gg/api/v1/customers/cus_…/payouts  -H "Authorization: Bearer $ZEROFIVE_KEY"

Payments

A payment is one on-chain transfer into one of your addresses: to an invoice, a customer's deposit address or your top-up address. It starts as Confirming and becomes Credited once it has the confirmations its network needs: fewer for payments under $1,000 (see networks), and confirmations_required says which applied. Payments from the same sender, or into the same address, within a few hours count together for that, so one amount sent in pieces waits like it would in one go. Its amount_usd is fixed when it is first seen. fee and net_amount are set once it's credited; net_amount is what reached your balance.

Statuses

confirmingstatus
Seen on chain, waiting for confirmations (and, for large live payments, a short hold).
creditedstatus
In your balance. Final.
heldstatus
The sender is on a sanctions list, frozen by the coin's issuer, or banned. Not credited; waiting for review.
declinedstatus
A held payment that was refused after review. Never credited. Final.
droppedstatus
Its transaction never made it into a block (dropped, replaced or failed), or you dismissed it while it was still unconfirmed. Not credited; its invoice goes back to waiting. If the transaction is mined after all, it returns to confirming.
ignoredstatus
Dust (under $0.10), a gas top-up from our own wallet, or a transfer a chain reorg moved and that was counted at its new place. Never credited.

A payment to an invoice in another coin, on another chain, or after its quote ran out still counts, at its dollar value; mismatch lists which of chain, coin and late applied. It's in your balance either way. When the invoice can't settle it on its own, needs_attention says why: short (it didn't cover the invoice), canceled, refunded or extra (the invoice was already paid), and payment.needs_attention fires. You decide under Payments in the dashboard: mark the invoice paid, refund the payment, or keep it; resolution then says which.

GET/api/v1/payments

Filter with invoice, customer or status; paginated.

GET/api/v1/payments/:id
The payment object
{
  "id": "pay_7Yc2…",
  "object": "payment",
  "livemode": true,
  "status": "credited",
  "invoice": "inv_3fTq…",
  "customer": null,
  "network": "tron",
  "asset": "USDT",
  "amount": "49",
  "fee": "0.245",
  "net_amount": "48.755",
  "amount_usd": "49.00",
  "address": "TQ5v…",
  "from": "TLa2…",
  "tx_hash": "a3f9…",
  "explorer_url": "https://tronscan.org/#/transaction/a3f9…",
  "confirmations": 6,
  "confirmations_required": 6,
  "simulated": false,
  "detected_at": "2026-10-20T10:13:58.000Z",
  "credited_at": "2026-10-20T10:14:31.000Z",
  "mismatch": [],
  "needs_attention": null,
  "resolution": null,
  "dropped_reason": null
}

Taxes

Optional. Under Settings → Taxes, list the countries where you charge VAT, GST or sales tax and the rate for each (there's a button for the EU's standard VAT rates), a rate for everywhere else if you want one, and the business details your receipts need: legal name, address and tax ID.

Choose whether your prices include tax (a $49 invoice is $49, tax inside) or have it added on top ($49 plus tax). Pass customer_country when you create the invoice and the tax is worked out straight away. Leave it out and the checkout asks the customer first: until then tax.pending is true and no coin can be picked.

amount is always what the customer pays. The checkout, the receipt and the receipt email show the subtotal and tax line, and the receipt carries your details and the buyer's tax ID if they gave one. We apply your rates; what you owe and where is for you and your accountant.

bash
// with taxes on (prices include tax, DE at 19%)
POST /api/v1/invoices
{ "amount": "49.00", "customer_country": "DE" }

{
  "amount": "49.00",          // what the customer pays
  "subtotal": "41.18",
  "tax": {
    "name": "VAT", "rate": 19, "amount": "7.82",
    "inclusive": true, "country": "DE", "pending": false
  },
  "customer_country": "DE",
  "customer_tax_id": null
}

Your money

Balances

You hold a balance per coin and network, in the coin you were paid in. There is no conversion: 49 USDT on Tron stays 49 USDT on Tron (minus the fee) until you withdraw it. amount_usd is today's value for reference (null if there's no price right now), and the list is sorted by it, largest first. fee_free_liquidity is the part that came from top-ups. Coins with a zero balance are left out.

New stores: in your first 30 days of taking live payments, customer payments can be sent out 72 hours after they arrive (your first week) or 24 hours after (to day 30). on_hold is the part of amount still waiting, and on_hold_until when the next part frees up. Withdrawals, payouts and refunds can use the rest, and top-ups are never held. Asking for more returns a 400 that says how much is on hold. Test mode has no hold.

GET/api/v1/balances
GET /api/v1/balances
{
  "object": "list",
  "data": [
    {
      "object": "balance",
      "network": "btc",
      "asset": "BTC",
      "amount": "0.0213",
      "on_hold": "0",
      "on_hold_until": null,
      "fee_free_liquidity": "0",
      "amount_usd": "1490.12"
    },
    {
      "object": "balance",
      "network": "tron",
      "asset": "USDT",
      "amount": "1452.31",
      "on_hold": "0",
      "on_hold_until": null,
      "fee_free_liquidity": "200",
      "amount_usd": "1452.31"
    }
  ],
  "has_more": false
}

Top up liquidity

Your balance is your own hot wallet: payouts and refunds are paid from it, per coin and network. To pay out more than you've taken in, send coins to your top-up addresses. They're credited to your balance like any deposit, but with no fee, and fire topup.confirmed. That money is tracked as fee-free liquidity: payouts spend fee-paid money first, and if top-up money itself leaves the platform, 0.5% of that part is charged then. Every coin pays the fee once.

Top-up addresses take every coin the platform runs, whatever your store accepts for customers. Each mode has its own set; test top-ups can only be simulated. The same addresses are on the dashboard under Balance & payouts.

GET/api/v1/topup
bash
curl https://z5.gg/api/v1/topup \
  -H "Authorization: Bearer $ZEROFIVE_KEY"
200 OK
{
  "object": "topup",
  "livemode": true,
  "fee": "0",
  "addresses": [
    {
      "network": "tron",
      "name": "Tron",
      "assets": ["TRX", "USDT"],
      "address": "TXk3…",
      "uri": "TXk3…",
      "qr_url": "https://z5.gg/d/…/qr/tron"
    },
    …
  ]
}

Payouts

Sends money from your balance to anyone: a user cashing out, a reward, a supplier. The amount leaves your balance the moment you call, and the full amount arrives. Payouts are free; we pay the gas.

  • Liquidity. You can only pay out what your balance holds of that coin on that network. Otherwise you get a 400 invalid_request saying how much you have; top up first.
  • Bridged versions. USDC on Polygon and USDT on Optimism each also exist as an older bridged token (USDC.e, bridged USDT). Payouts send the standard one; only when the platform holds the amount in the bridged one instead does it go out as that, and bridged_token names it (otherwise it's null). Same value, but some exchanges only credit the standard token, so check before paying out to an exchange on those two networks.
  • Idempotent. Pass your own id as external_id. Sending the same external_id again returns the first payout instead of paying twice, so retrying after a timeout is safe.
  • Your rules. Every payout is checked against your payout rules: it goes out within a minute, waits in your review queue (status review, webhook payout.pending_review), or is refused with a 400.
  • Platform limit. Live payouts above the platform's automatic limit, per payout or per store over 24 hours, wait for the platform's OK (status pending). Refunds and withdrawals count towards the same limit.

Refused with a 400 before anything is reserved:

  • an address in the wrong format or checksum, a token's own contract, or an address that belongs to this platform;
  • below the network's minimum (see fees), or more than the balance holds;
  • in live mode, an address on a sanctions list, frozen by a stablecoin issuer, or banned (see screening);
  • in live mode, a Solana token to a wallet that has never held that token (ask the user to receive any amount of it once, or use another network).

Statuses

reviewstatus
Held by your payout rules; review_reasons says why. Approve or reject it. Fires payout.pending_review.
pendingstatus
Above the platform's automatic limit, waiting for the platform's review.
approvedstatus
Queued; goes out automatically, usually within a minute.
processingstatus
Signed and broadcast, waiting for the chain. On EVM chains, one stuck under a gas price the network moved past is re-sent with a higher fee (we pay it): same transfer, same nonce, so only one can land, and tx_hash changes to the new one.
sentstatus
On chain; tx_hash and explorer_url are set. Fires payout.sent.
failedstatus
Something went wrong after sending started; error says what, and we look at it by hand. Fires payout.failed.
rejectedstatus
Not sent: rejected by you or the platform, or reversed after a failure. The amount is back in your balance. Fires payout.rejected.
POST/api/v1/payouts

Parameters

networkstringrequired
A network key: tron, eth, base, btc, sol… (see networks).
assetstringrequired
e.g. USDT, USDC, BTC. Paid from your balance of that coin on that network.
amountstringrequired
Decimal amount of the coin, no more decimals than the coin has, or max for the whole balance.
addressstringrequired
Destination on that network. Format and checksum are checked; Bitcoin Cash needs the bitcoincash:q… form.
customerstring
The cus_… it's for, so it shows in their history and your per-user rules apply to it.
external_idstring
Your id for this payout, up to 120 characters. Unique per store and mode; makes the call safe to retry.
metadataobject
Up to 4 KB of your own data.
GET/api/v1/payouts

Payouts and refunds, newest first. Filter with customer, external_id, status or kind (payout, refund). Payouts have ids starting po_, refunds ref_.

GET/api/v1/payouts/:id
bash
curl https://z5.gg/api/v1/payouts \
  -H "Authorization: Bearer $ZEROFIVE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "network": "tron",
    "asset": "USDT",
    "amount": "120",
    "address": "TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7",
    "customer": "cus_Qm4p…",
    "external_id": "wd_99812"
  }'
The payout object
{
  "id": "po_8Hn2…",
  "object": "payout",
  "livemode": true,
  "status": "sent",
  "customer": "cus_Qm4p…",
  "invoice": null,
  "external_id": "wd_99812",
  "metadata": { "user": "812" },
  "network": "tron",
  "asset": "USDT",
  "amount": "120",
  "fee": "0",
  "amount_usd": "120.00",
  "address": "TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7",
  "bridged_token": null,
  "tx_hash": "c81e…",
  "explorer_url": "https://tronscan.org/#/transaction/c81e…",
  "error": null,
  "review_reasons": [],
  "reviewed_at": null,
  "created_at": "2026-10-20T12:00:00.000Z",
  "processed_at": "2026-10-20T12:00:21.000Z"
}

Payout rules

Your own rules for payouts to your users, set in Dashboard → Settings → Payout rules or over the API. They apply to POST /payouts in test and live mode. Your own withdrawals and invoice refunds skip them.

Each payout is checked in this order:

  1. Refused (400, nothing is created or reserved): blocked address, below the smallest or above the largest payout, a network you turned off, no customer when you require one, or an address that belongs to this platform.
  2. Held for review (status review, the amount is reserved): above the instant limit or a per-coin limit, over a per-user or store-wide limit, a user's first payout or a new address (if turned on), within the deposit cooldown, an address not on your allowed list, or the emergency hold.
  3. Sent: everything else, usually within a minute.
  4. Platform limit: after your approval, very large live payouts also wait for the platform admin (status pending).

Limits count every payout that wasn't rejected, over a rolling 24 hours (or hour). A "user" is the customer; without one, the address. Checks run in the same transaction that reserves the money, so parallel requests can't slip past a limit together.

Rules

enabledboolean
Off: everything goes straight out (the hold still works).
holdAllboolean
Emergency switch: every new payout waits for review.
autoApproveMaxUsdnumber | null
Instant up to this; above waits for review. Default 1000.
minPayoutUsd / maxPayoutUsdnumber | null
Outside these the payout is refused.
perUserDailyUsd / perUserDailyCountnumber | null
Per user, rolling 24 hours; over waits for review.
storeDailyUsdnumber | null
All payouts, rolling 24 hours; over waits for review.
storeHourlyCountnumber | null
All payouts in the last hour; a burst waits for review.
reviewFirstPayoutboolean
A user's first payout waits for review.
reviewNewAddressboolean
An address the user hasn't been paid to before waits for review.
depositCooldownMinutesnumber | null
A payout this soon after the user's last deposit waits for review.
requireCustomerboolean
Payouts without a customer are refused.
blockedAddressesstring[]
Refused.
allowedAddressesstring[]
If set, any other address waits for review.
assetMaxUsdobject
Per coin, e.g. {"BTC": 5000}; above waits for review.
networksstring[]
Networks payouts may use; empty means all. Others are refused.
POST/api/v1/payouts/:id/approve

Sends a payout in review. Returns the payout: approved, or pending if it also needs the platform's OK. A payout in any other status gets a 400.

POST/api/v1/payouts/:id/reject

Parameters

reasonstring
Why, up to 200 characters. Shown as error on the payout and in the payout.rejected webhook.
GET/api/v1/payout-rules
POST/api/v1/payout-rules

Both return { "object": "payout_rules", ...rules }. Send only the rules you want to change; the rest stay as they are. Changing rules needs the Payout rules permission on a live key created by the store owner: a test key gets a 400 and a team member's key a 403, so a leaked key can't loosen live payouts. The rules apply to test and live payouts alike.

bash
# approve a payout your rules held back
curl -X POST https://z5.gg/api/v1/payouts/po_8Hn2…/approve \
  -H "Authorization: Bearer $ZEROFIVE_KEY"

# or refuse it; the amount returns to your balance
curl -X POST https://z5.gg/api/v1/payouts/po_8Hn2…/reject \
  -H "Authorization: Bearer $ZEROFIVE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Failed KYC"}'
change rules (live key)
curl https://z5.gg/api/v1/payout-rules \
  -H "Authorization: Bearer $ZEROFIVE_LIVE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "autoApproveMaxUsd": 500,
    "perUserDailyUsd": 2000,
    "reviewFirstPayout": true,
    "depositCooldownMinutes": 30,
    "blockedAddresses": ["TXyz…"]
  }'
payout.pending_review data
{
  "id": "po_8Hn2…",
  "object": "payout",
  "status": "review",
  "amount": "900",
  "asset": "USDT",
  "customer": "cus_Qm4p…",
  "external_id": "wd_99812",
  "review_reasons": [
    "Above the instant limit of $500.00",
    "First payout for this user"
  ]
}

Refunds

Refunds a paid (or partly paid) invoice from your balance, in the coin and network the customer paid with (the first one, if they paid in more than one). Leave out amount to refund everything they paid in that coin, less earlier refunds; pass it for a partial refund. Ask the customer for their address: the one they paid from may belong to an exchange. Needs the Payouts and refunds permission.

A refund is a payout with "object": "refund", an id starting ref_ and invoice set. It skips your payout rules, but the network minimum, address checks, screening and the platform limit apply as for payouts. It fires payout.* events, then invoice.refunded once sent, and emails the customer. The 0.5% fee on the original payment isn't returned. Once refunded, the invoice can't be paid again.

Overpayments. When a customer pays an invoice too much, the paid page offers them the extra back: they enter their address and it's refunded in the coin they paid with, less a fee you keep (1%, at least $1, by default). Set it to go out on its own, wait for your review, or not be offered at all under Settings → Payout rules. It's an ordinary refund with external_id overpay_inv_…, so the same webhooks fire. One request per invoice, within 30 days, and only when everything was paid in the quoted coin. Only the payer can ask: the browser they paid with, or the link in their receipt email (so pass customer_email when you can). Anyone else who opens the invoice just sees that there was an extra.

POST/api/v1/invoices/:id/refund

Parameters

addressstringrequired
Where the customer wants the refund, on the network they paid on.
amountstring
In the coin they paid with. Defaults to the whole amount still refundable; more than that is refused.
reasonstring
Kept with the refund (metadata.reason) and shown in the customer's email.
external_idstring
Your id; makes the call safe to retry.
bash
curl https://z5.gg/api/v1/invoices/inv_3fTq…/refund \
  -H "Authorization: Bearer $ZEROFIVE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "address": "TQ5vYp…",
    "reason": "Order canceled"
  }'

Withdrawals

Sends part or all of a balance to your own wallet. The amount is taken from your balance straight away and the full amount arrives: withdrawals are free. Needs a key with the Withdrawals permission.

In live mode the address must be one of your saved payout addresses (Balance & payouts), saved at least 24 hours earlier, and the account owners get an email for every withdrawal. Adding a payout address also emails them. Your payout rules don't apply to withdrawals.

Withdrawals under the platform's automatic limit (per withdrawal and per store over 24 hours) go out by themselves, usually within a couple of minutes. Larger ones wait for a quick manual review. Statuses: Pending (in review) → Approved → Processing → Sent, or Rejected with the amount returned to your balance. Events are withdrawal.*, not payout.*.

In the dashboard, live withdrawals, refunds and payouts can only be made by the owner or a team member the owner allowed to move money, and each one is confirmed with a code from an authenticator app or email. Over the API, the key's permissions decide, so give the Withdrawals permission only to keys that need it.

POST/api/v1/withdrawals

Parameters

networkstringrequired
e.g. tron, eth, btc, sol.
assetstringrequired
e.g. USDT, USDC, BTC.
amountstringrequired
A decimal amount of the coin, or max for the whole balance.
addressstringrequired
Your wallet on that network: in live mode one of your saved payout addresses. Checked for the right format and checksum.
external_idstring
Your id for this withdrawal; makes the call safe to retry.
GET/api/v1/withdrawals

Your withdrawals (ids starting wd_), newest first; paginated.

GET/api/v1/withdrawals/:id
bash
curl https://z5.gg/api/v1/withdrawals \
  -H "Authorization: Bearer $ZEROFIVE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "network": "tron",
    "asset": "USDT",
    "amount": "max",
    "address": "TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7"
  }'

Fees

0.5% of every payment, taken in the same coin when the payment is credited. A 49 USDT payment adds 48.755 USDT to your balance. That's the only fee.

  • Withdrawals, payouts and refunds: free. We pay the gas and the full amount arrives.
  • Top-ups to your own balance: free to add. Every coin pays the 0.5% once: payments on the way in, top-up money only if it leaves the platform. Payouts and withdrawals use fee-paid money first, so top-ups that just cover your users' payouts while deposits come in cost nothing. When top-up money itself goes out, 0.5% of that part is charged (shown as fee on the payout, and as fee_free_liquidity on GET /balances).
  • No monthly fee, no setup fee, nothing in test mode.

To keep gas from costing more than a transfer moves, each network has a minimum for payouts, refunds and withdrawals, in dollars converted to the coin at the current price. GET /v1/networks returns the current value as min_withdrawal_usd:

NetworkFeeMinimum payout
Ethereumfree$25.00
Basefree$1.00
Arbitrumfree$1.00
Optimismfree$1.00
Polygonfree$1.00
BNB Chainfree$1.00
Avalanchefree$1.00
Plasmafree$1.00
Arcfree$1.00
Tempofree$1.00
Tronfree$5.00
Bitcoinfree$20.00
Litecoinfree$2.00
Dogecoinfree$1.00
Bitcoin Cashfree$1.00
Solanafree$1.00
TONfree$1.00

Reports & exports

Balance transactions are every movement of your balance, one per event: payment, topup, withdrawal, payout, refund, withdrawal_reversal (a transfer that didn't go out, back in the balance) and adjustment. amount is what moved (negative going out), fee what you paid for it, net the change to your balance. USD values are at the time. available_at is when a payment could be sent on, later than created_at while a new store's payments are held. Filter with type, network, asset and the date range.

Exports are files of your records for a date range: balance_transactions, invoices, payments, payouts (withdrawals, payouts and refunds, with a kind column), customers or subscriptions, as csv (flat columns, UTF-8, opens in Excel) or json (the full API objects). from is inclusive, to exclusive. They're made in the background, usually within seconds; poll GET /v1/exports/:id and download with the same key. Files are kept for 7 days. Any key can make them, read-only ones included.

For accounting tools. balance_transactions also comes ready to import: xero (a bank statement in USD, fees on their own lines), quickbooks (QuickBooks Online bank upload: Date, Description, Amount in USD), koinly (Koinly's universal CSV in coin amounts, payments tagged other income) and cointracking (CoinTracking's CSV import: payments as Income, fees as Other Fee, top-ups and withdrawals as Deposit and Withdrawal, payouts and refunds as Spend). Dates are UTC. A movement with a fee can be two lines, so rows counts lines.

Receipts. A paid invoice has receipt_url and receipt_pdf_url; GET /v1/invoices/:id/pdf returns the same PDF with your key.

Monthly statements. Opening and closing balance per coin, everything in and out and the fees, for each calendar month (UTC), as a page, PDF and CSV under Reports in the dashboard. On the 1st, last month's live statement is emailed to everyone with Monthly statement on under Settings → Emails, including addresses like billing@ that don't have a login.

every balance movement in October
curl -g "https://z5.gg/api/v1/balance_transactions?created[gte]=2026-10-01&created[lt]=2026-11-01&limit=100" \
  -H "Authorization: Bearer $ZEROFIVE_KEY"
one balance transaction
{
  "id": "led_BlBzXmefGjd1rPxh7JCP",
  "object": "balance_transaction",
  "livemode": true,
  "type": "payment",
  "network": "tron",
  "asset": "USDT",
  "amount": "49",
  "fee": "0.245",
  "net": "48.755",
  "amount_usd": "49.00",
  "fee_usd": "0.25",
  "net_usd": "48.75",
  "source": "pay_pGP6WpQD9Om0abB6ggyL",
  "invoice": "inv_w6vTrRsgxmCazaAF0PCH",
  "customer": null,
  "external_id": null,
  "tx_hash": "8f3c…e1a0",
  "explorer_url": "https://tronscan.org/#/transaction/8f3c…e1a0",
  "description": "Invoice inv_w6vTrRsgxmCazaAF0PCH",
  "available_at": "2026-10-22T20:58:26.222Z",
  "created_at": "2026-10-22T20:58:26.222Z"
}
a CSV of last month's invoices
curl -X POST https://z5.gg/api/v1/exports \
  -H "Authorization: Bearer $ZEROFIVE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type": "invoices", "format": "csv", "from": "2026-09-01", "to": "2026-10-01"}'

# poll until "status": "ready", then
curl -o invoices.csv https://z5.gg/api/v1/exports/exp_.../download \
  -H "Authorization: Bearer $ZEROFIVE_KEY"

Guides

Casino & wallet setup

For anything where users hold a balance with you and come and go: casinos, sportsbooks, wallets, games, exchanges.

  1. One customer per user. POST /customers with external_id set to your user id. It returns the existing customer if there is one, so call it whenever the user opens the deposit screen.
  2. Show their address. GET /customers/:id/addresses/:network. It never changes, so users can save it. Or link to deposit_page_url.
  3. Credit on deposit.confirmed. The payment's net_amount is what reached your balance. Use the payment id so a resent webhook can't credit twice. Never credit on deposit.detected, and leave payment.held deposits alone.
  4. Pay out with POST /payouts. Pass the user's customer id and your withdrawal id as external_id. Mark it done on payout.sent; put the money back on payout.rejected. If your payout rules hold it, payout.pending_review fires: show the user "under review" until you approve or reject it.
  5. Keep liquidity. Deposits fill your balance; if users withdraw more than they put in (winners, a different coin than they deposited), top up. Watch GET /balances.
Run the whole loop in test mode first: simulate deposits on the hosted deposit page or in the dashboard, and test payouts complete instantly.
the whole loop (node)
const zf = (path, body) =>
  fetch("https://z5.gg/api/v1" + path, {
    method: body ? "POST" : "GET",
    headers: {
      Authorization: `Bearer ${process.env.ZEROFIVE_KEY}`,
      "Content-Type": "application/json",
    },
    body: body && JSON.stringify(body),
  }).then((r) => r.json());

// 1. a user opens "Deposit": one customer per user, safe to call every time
const customer = await zf("/customers", { external_id: user.id, email: user.email });
const { address } = await zf(`/customers/${customer.id}/addresses/tron`);
showDepositAddress(address);

// 2. webhook: credit the user when the deposit confirms
if (event.type === "deposit.confirmed") {
  const p = event.data; // payment object
  await creditUser(p.customer, p.net_amount, p.asset, { once: p.id });
}
// deposit.detected → show "pending"; payment.held → don't credit, it's under review

// 3. the user withdraws: pay them from your balance
await zf("/payouts", {
  network: "tron", asset: "USDT", amount: "120",
  address: user.withdrawAddress,
  customer: customer.id,
  external_id: withdrawal.id, // retry-safe
});

// 4. webhooks: payout.pending_review → tell the user "under review"
//              payout.sent → mark the withdrawal done
//              payout.rejected → put the money back on the user

Reference

Webhooks

Set an endpoint URL in Dashboard → Developers and we POST every event for your store to it as JSON: {id, type, livemode, created, data}, where created is a Unix timestamp in seconds. Test and live mode each have their own URL and their own signing secret: set the live one with the dashboard in live mode. Test events are only ever signed with the test secret, so keep the live secret out of dev and staging.

Each request is signed with the secret of its mode (whsec_… live, whsec_test_… test, shown in Developers). The ZeroFive-Signature header looks like t=1792491271,v1=5257a8…:

  1. Split the header on , and each part on = to get t (Unix seconds) and v1.
  2. Compute the hex HMAC-SHA256 of `${t}.${rawBody}` with your secret. Use the raw body: re-serialised JSON won't match.
  3. Compare it with v1 in constant time, and reject timestamps more than a few minutes old so a captured request can't be replayed.

Answer with any 2xx within 10 seconds; redirects aren't followed. Anything else, or a timeout, is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours, 24 hours: eight tries over almost two days. Every delivery is listed in the dashboard with its response and can be resent. Events can arrive more than once and out of order, so make your handler idempotent on id and check the object's current status. Endpoints must be public URLs without a user name or password in them, and https in live mode (http is accepted for test mode only): private and local addresses are refused.

data is the object, or several of them, as described per event below. Objects in events have the same shape as in the API, except invoice.refunded, where invoice is just the id.

Event types

invoice.createdinvoice
An invoice was created (API, dashboard, tip or subscription). data is the invoice.
invoice.payment_detectedinvoice
A payment to the invoice was seen on chain and is confirming. Don't ship yet. data: {invoice, payment}.
invoice.paidinvoice
The invoice is fully paid and confirmed. Fulfil the order. Fires once per invoice. data: {invoice, payment}.
invoice.partially_paidinvoice
A payment confirmed but the invoice isn't covered yet. data: {invoice, payment}.
invoice.expiredinvoice
Nothing arrived before expires_at. data is the invoice.
invoice.canceledinvoice
You canceled the invoice. data is the invoice.
invoice.refundedrefund
A refund on the invoice was sent. data: {invoice, refund}, where invoice is the invoice id and refund the refund object.
deposit.detectedpayment
A transfer to a customer's address was seen and is confirming. Don't credit yet. data is the payment.
deposit.confirmedpayment
A customer deposit is confirmed and credited. Credit your user. data is the payment.
topup.confirmedpayment
A top-up to your own balance is confirmed and credited, with no fee (0.5% applies only if that money later leaves the platform). data is the payment.
payment.heldpayment
A payment came from a sanctioned, frozen or banned wallet and is held for review. It isn't credited and the invoice stays unpaid; don't ship or credit. data is the payment.
payment.declinedpayment
A held payment was declined after review. It won't be credited, and an invoice it was the only payment on is canceled. data: {invoice, payment} for invoice payments, else the payment.
payment.droppedpayment
A confirming payment's transaction never made it into a block, or it was dismissed. Not credited; its invoice goes back to waiting. data: {invoice, payment} for invoice payments, else the payment.
payment.needs_attentionpayment
A credited payment its invoice couldn't settle on its own (short in another coin or late, or the invoice was canceled, refunded or already paid). data: {invoice, payment}.
payout.createdpayout
A payout or refund was created and debited from your balance. data is the payout.
payout.pending_reviewpayout
Your payout rules held a payout; review_reasons says why. Approve or reject it (dashboard or API). data is the payout.
payout.sentpayout
The payout or refund is on chain; tx_hash is set. data is the payout.
payout.failedpayout
Sending failed in a way that needs a person to look; we'll follow up. data is the payout.
payout.rejectedpayout
The payout or refund wasn't sent; the amount is back in your balance. data is the payout.
withdrawal.createdwithdrawal
A withdrawal was requested and debited from the balance. data is the withdrawal.
withdrawal.sentwithdrawal
The withdrawal left the platform wallet; tx_hash is set. data is the withdrawal.
withdrawal.failedwithdrawal
Sending failed in a way that needs a person to look; we'll follow up. data is the withdrawal.
withdrawal.rejectedwithdrawal
The withdrawal was rejected or reversed; the amount is back in your balance. data is the withdrawal.
subscription.createdsubscription
A subscription was created; its first invoice is open. data is the subscription.
subscription.activatedsubscription
The first payment confirmed (or a lapsed one was paid again): give access. data: {subscription, invoice, periods}.
subscription.renewedsubscription
A renewal or prepayment confirmed and paid_through moved on. data: {subscription, invoice, periods}.
subscription.payment_duesubscription
The renewal invoice was created and emailed to the customer. data: {subscription, invoice}.
subscription.past_duesubscription
The paid time ran out without a renewal. It's in the grace period. data is the subscription.
subscription.updatedsubscription
Set to cancel at the end of the paid time (or that was undone), or a change of items was scheduled, requested, applied or called off. data is the subscription.
subscription.canceledsubscription
The subscription ended; cancel_reason is requested or unpaid. Remove access. data is the subscription.
tip.receivedtip
A tip was paid (also sent as invoice.paid). data is the tip: name, message, amount, payment.
POST to your endpoint
{
  "id": "evt_Vb8n…",
  "type": "invoice.paid",
  "livemode": true,
  "created": 1792491271,
  "data": {
    "invoice": { "id": "inv_3fTq…", "status": "paid", "order_id": "A-1042", … },
    "payment": { "id": "pay_7Yc2…", "amount": "49", "asset": "USDT", … }
  }
}
verify the signature (node)
import crypto from "node:crypto";

// rawBody: the request body exactly as received, before any JSON parsing
export function verify(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(
    String(header ?? "").split(",").map((p) => p.trim().split("=")),
  );
  const t = Number(parts.t);
  if (!t || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false; // replayed
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
express
app.post("/zerofive", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verify(raw, req.get("ZeroFive-Signature"), process.env.ZEROFIVE_WHSEC)) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(raw);
  if (event.type === "invoice.paid") fulfil(event.data.invoice.order_id);
  res.sendStatus(200);
});
next.js route handler (app/api/zerofive/route.js)
export async function POST(req) {
  const raw = await req.text();
  const sig = req.headers.get("zerofive-signature");
  if (!verify(raw, sig, process.env.ZEROFIVE_WHSEC)) {
    return new Response("bad signature", { status: 400 });
  }
  const event = JSON.parse(raw);
  // handle it, idempotently on event.id, then answer fast
  return new Response(null, { status: 200 });
}

Screening & holds

Live payments are checked before they're credited, so stolen or sanctioned money doesn't end up in your balance (and your own coins don't get frozen with it). Test mode skips all of this.

  • What's checked. The sending wallet, against the OFAC sanctions list (refreshed daily), the freeze lists stablecoin issuers keep (Tether, Circle, Paxos and others, including frozen token accounts on Solana and locked USDT wallets on TON), and our own list of wallets that already sent flagged money. Then the payer: the customer's name, if you gave one, against the UN, EU and OFAC sanctions lists, and the invoice's customer_country.
  • Where we don't serve. Checkout pages aren't served to visitors in sanctioned countries and regions (including Crimea and the Donetsk and Luhansk regions), and an invoice with one of them as its customer_country is refused with a 400.
  • Your store. Store and team member names are screened against the same lists when they change and whenever the lists are updated. A match is reviewed by the platform, and the store can be suspended meanwhile.
  • A hit. The payment becomes held and payment.held fires. It isn't credited, the invoice stays unpaid, and no invoice.paid or deposit.confirmed follows. Don't ship or credit anything. The platform reviews it: if it's cleared, it's credited as usual and the normal events fire; if not, it becomes declined (payment.declined), is never credited, and an invoice it was the only payment on is canceled.
  • Large payments. Live payments of $1,000 or more wait about 60 minutes after their confirmations and are screened again before they're credited, since issuers often freeze stolen funds hours after the theft. They stay confirming meanwhile; no extra event fires.
  • Money going out. Live payouts, refunds and withdrawals to a sanctioned, frozen or banned address are refused with a 400.
For customers this mostly means a short wait on big payments. Tell them a large payment can take up to an hour longer to confirm, and fulfil on invoice.paid or deposit.confirmed, never on the detected events.

Networks & coins

The keys below are what network means everywhere in the API, and asset is the coin's symbol in capitals. Tokens are matched by contract address, never by name, so look-alike tokens are ignored. Dollar stablecoins count 1:1 with the dollar; EURC is priced at the euro rate, so its amount_usd moves with EUR/USD.

Read GET /v1/networks instead of hard-coding this list. The platform can switch single networks or coins off (and new ones on), and the endpoint always returns what can be paid with right now. Your store and each API key can narrow that further. Asking for a coin that's off gets a 400.

Payments under $1,000 need fewer confirmations than larger ones, so most customers are done in seconds. Each payment's confirmations_required says which applied. min_withdrawal_usd is the smallest payout, refund or withdrawal on that network; withdrawal_fee_usd is always 0.

bash
# public, no key needed
curl https://z5.gg/api/v1/networks
200 OK (one entry shown)
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "network": "tron",
      "name": "Tron",
      "assets": [
        { "symbol": "TRX", "contract": null, "decimals": 6 },
        {
          "symbol": "USDT",
          "contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
          "decimals": 6
        }
      ],
      "confirmations_required": 19,
      "eta": "about 1 minute",
      "confirmations_required_small": 6,
      "eta_small": "about 20 seconds",
      "small_payment_limit_usd": 1000,
      "withdrawal_fee_usd": 0,
      "min_withdrawal_usd": 5
    }
  ]
}

Networks

NetworkKeyCoinsUnder $1,000$1,000 and up
EthereumEthereum mainnet only. Gas is high here, a cheaper network saves you fees.ethETHUSDCUSDTDAIPYUSDUSD1EURC3about 40 seconds12about 3 minutes
BaseBase network only. Not Ethereum mainnet.baseETHUSDCUSDTEURC5about 10 seconds30about 1 minute
ArbitrumArbitrum One only. Not Arbitrum Nova or Ethereum mainnet.arbitrumETHUSDCUSDTPYUSD20about 5 seconds60under a minute
OptimismOP Mainnet only.optimismETHUSDCUSDT5about 10 seconds30about 1 minute
PolygonPolygon PoS only. Not Polygon zkEVM.polygonPOLUSDCUSDTDAI16about 30 seconds40about 1–2 minutes
BNB ChainBNB Smart Chain (BEP-20) only. Not opBNB or the old BEP-2.bscBNBUSDTUSDCUSD15a few seconds15under a minute
AvalancheAvalanche C-Chain only. Not the X-Chain or P-Chain.avalancheAVAXUSDCUSDTEURC6a few seconds12about 15 seconds
PlasmaPlasma mainnet only.plasmaXPLUSDTUSDC4a few seconds10about 10 seconds
ArcArc mainnet only.arcUSDCEURC1a few seconds2a few seconds
TempoTempo mainnet only.tempoUSDCUSDT1a few seconds2a few seconds
TronTRC-20 only. Not ERC-20 or BEP-20.tronTRXUSDT6about 20 seconds19about 1 minute
BitcoinBitcoin network only. No Lightning.btcBTC1about 10 minutes2about 20 minutes
LitecoinLitecoin network only. No MWEB.ltcLTC1about 3 minutes3about 8 minutes
DogecoinDogecoin network only. Not DOGE on BNB Chain or other networks.dogeDOGE3about 3 minutes10about 10 minutes
Bitcoin CashBitcoin Cash network only. Not BTC.bchBCH1about 10 minutes6about 1 hour
SolanaSolana network only. SPL tokens from Token-2022 aren't detected.solSOLUSDCUSDTUSD1EURCfinalizedabout 15 secondsfinalizedunder a minute
TONTON network only. No memo or comment needed.tonTONUSDTfinalizeda few secondsfinalizeda few seconds

Ethereum, Base, Arbitrum, Optimism, Polygon, BNB Chain, Avalanche, Plasma, Arc and Tempo are EVM networks and share one address per customer. Solana and TON count once a transaction is final rather than after a number of blocks.

Coins and contracts

Amounts in the API are decimal strings in the coin itself ("49.5", not base units). Where a symbol has two contracts on one network (native and bridged USDC on Polygon, two USDT contracts on Optimism), both are credited as that symbol; /v1/networks shows the first.

NetworkCoinDecimalsContract, mint or jetton master
EthereumETH18native
USDC60xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
USDT60xdAC17F958D2ee523a2206206994597C13D831ec7
DAI180x6B175474E89094C44Da98b954EedeAC495271d0F
PYUSD60x6c3ea9036406852006290770BEdFcAbA0e23A0e8
USD1180x8d0D000Ee44948FC98c9B98A4FA4921476f08B0d
EURC60x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33cpriced in euros
BaseETH18native
USDC60x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
USDT60xfde4C96c8593536E31F229EA8f37b2ADa2699bb2
EURC60x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42priced in euros
ArbitrumETH18native
USDC60xaf88d065e77c8cC2239327C5EDb3A432268e5831
USDT60xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9
PYUSD60x46850aD61C2B7d64d08c9C754F45254596696984
OptimismETH18native
USDC60x0b2C639c533813f4Aa9D7837CAf62653d097Ff85
USDT60x94b008aA00579c1307B0EF2c499aD98a8ce58e58
USDT60x01bFF41798a0BcF287b996046Ca68b395DbC1071
PolygonPOL18native
USDC60x3c499c542cEF5E3811e1192ce70d8cC03d5c3359
USDC60x2791Bca1f2de4661ED88A30C99A7a9449Aa84174
USDT60xc2132D05D31c914a87C6611C10748AEb04B58e8F
DAI180x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063
BNB ChainBNB18native
USDT180x55d398326f99059fF775485246999027B3197955
USDC180x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d
USD1180x8d0D000Ee44948FC98c9B98A4FA4921476f08B0d
AvalancheAVAX18native
USDC60xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E
USDT60x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7
EURC60xC891EB4cbdEFf6e073e859e987815Ed1505c2ACDpriced in euros
PlasmaXPL18native
USDT60xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb
USDC60x2d661C89D812261039AF9764eceaAee884f5F67F
ArcUSDC18native (the gas coin)
EURC60xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1priced in euros
TempoUSDC60x20C000000000000000000000b9537d11c60E8b50
USDT60x20C00000000000000000000014f22CA97301EB73
TronTRX6native
USDT6TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t
BitcoinBTC8native
LitecoinLTC8native
DogecoinDOGE8native
Bitcoin CashBCH8native
SolanaSOL9native
USDC6EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
USDT6Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB
USD16USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB
EURC6HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtrpriced in euros
TONTON9native
USDT6EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs

Things to tell your customers

  • Bitcoin Cash addresses are CashAddr (bitcoincash:q…). Payout addresses in the legacy 1… form are refused, because they look exactly like Bitcoin ones.
  • Solana: tokens sent through the Token-2022 program aren't detected. Token payouts only go to wallets that already hold that token.
  • TON: every invoice and customer has its own address, so no memo or comment is needed.
  • Bitcoin on-chain only, no Lightning. Litecoin without MWEB.
  • Arc: USDC is the network's own gas coin, with 18 decimals. Tempo has no coin of its own.
  • Each network's warning (in the table above, and in a customer address's warning) is worth showing next to the address.

All endpoints

Every endpoint, relative to https://z5.gg/api, with the key permission it needs. "Read" works with any key.