API reference
Base URL: https://app.mintagepay.com/v1 (sandbox: https://sandbox.mintagepay.com/v1). All request and
response bodies are JSON. Amounts are decimal strings.
A machine-readable description of every route is at
/v1/openapi.json. No key is needed to read it.
Authentication
Send your key as a bearer token:
Authorization: Bearer mp_live_YOUR_KEYCreate keys in the dashboard under Developers > API keys. A key is shown once; only its hash is stored. Revoked keys
get 401.
GET /v1/me works with any key. It tells an integration where it is and what the account can do:
mode:"live"(real money) or"test"(the sandbox);name: the business name;rails: what the account can be paid with here, for example[{ "id": "base-usdc", "network": "base", "token": "USDC" }];merchant:id,name,fee_bpsandunderpayment_tolerance_bps;api_key: the key'sidandscopes.
Scopes
Each key has scopes, chosen when it is created. A call the key's scopes do not allow gets 403 insufficient_scope,
and details.required names the missing scope.
| Scope | Allows |
|---|---|
invoices:read |
GET /v1/invoices, GET /v1/invoices/{id} (with its payments and refunds) |
invoices:write |
POST /v1/invoices, POST /v1/invoices/{id}/send, POST /v1/invoices/{id}/cancel |
links:read |
GET /v1/payment-links |
links:write |
POST /v1/payment-links |
orders:read |
GET /v1/orders/{id} (the customer's name, email and address) |
refunds:read |
GET /v1/invoices/{id}/refunds |
webhooks:manage |
/v1/webhook-endpoints (list, add, change, remove) |
The Developers page offers presets: Full access, Read only and AI agent: read + create invoices. Only team
members who may see customer details (owner, admin) can give a key orders:read.
Errors
Errors look like this:
{ "error": "not_found", "message": "Invoice not found" }Invalid input is 400 with "error": "validation_failed" and one details item per problem:
{ "error": "validation_failed", "message": "Invalid request",
"details": [{ "path": "redirect_url", "code": "invalid_url", "message": "Invalid URL" }] }pathis the request field as you sent it. Nested fields are joined with dots (shipping.postal_code).codeis stable, so you can write your own wording for it:required,too_short,too_long,too_small,too_big,invalid_email,invalid_url,invalid_format,invalid_choice,invalid_type,invalid_amountand others.messageis English, for developers.
| Status | Meaning |
|---|---|
| 400 | Invalid input |
| 401 | Bad key |
| 403 | The key lacks the scope (insufficient_scope), or the account may not use the feature (for example review_required) |
| 404 | Not found |
| 409 | Conflict |
| 410 | Expired |
| 429 | Rate limited. The limit is 120 requests a minute per key |
Create an invoice
POST /v1/invoices (scope invoices:write)
Send the header Idempotency-Key with your order id or any unique string. Repeating the same key returns the same
invoice (200), so a retry never creates a second one. Reusing the key with a different amount or order id returns 409.
So does reusing it with a different metadata.order_key, when both requests carry one: put a value that belongs to the
one order there if your order numbers can start again (a rebuilt store, or two stores on one account).
{
"amount": "15.00",
"currency": "USD",
"order_id": "1042",
"description": "Order #1042",
"buyer_email": "customer@example.com",
"redirect_url": "https://example.com/thank-you",
"cancel_url": "https://example.com/cart",
"metadata": { "any": "json" },
"expires_in_minutes": 60,
"send_email": false,
"send_receipt": true,
"locale": "es"
}| Field | Required | Notes |
|---|---|---|
amount |
Yes | In currency, above zero, at most 2 decimals (none for JPY, KRW, VND, CLP, PYG, ISK, XAF and XOF, VND, CLP, PYG, ISK, XAF and XOF) |
currency |
No | Default USD. See Prices in other currencies |
order_id |
No | Your reference, up to 200 characters |
description |
No | Up to 500 characters |
buyer_email |
No | Used for the receipt and for payment request emails |
redirect_url |
No | Shown as "Return to store" after payment |
cancel_url |
No | Where the page's "Cancel" link takes a customer who has not paid yet |
metadata |
No | Any JSON, up to 4,000 characters |
expires_in_minutes |
No | 5 to 10080. Default 60 |
send_email |
No | true emails the payment link to buyer_email now |
send_receipt |
No | Default true. false if your store sends its own receipt |
locale |
No | The customer's language. See Checkout language |
Returns 201 with an invoice object. Send the customer to its checkout_url.
With "send_email": true the response also has "email_sent": true, or false with an email_error. The invoice is
created either way, and you can retry with POST /v1/invoices/{id}/send.
Checkout language
The payment page and the customer emails come in English, Spanish, Portuguese (Brazil), French, German, Italian,
Turkish, Indonesian, Vietnamese, Thai, Arabic (right to left), Hindi and Russian. locale sets the customer's language:
en, es, pt-BR, fr, de, it, tr, id, vi, th, ar, hi or ru. Any language tag works
(es-MX, de_DE), so a store can pass its own language as it is. A language that is not offered never fails the
invoice; the page then follows the customer's browser.
On the page, the first match wins: the language the customer picks in the page's switcher, ?lang= in the URL, the
language they picked on an earlier visit, the invoice's locale, the browser language, then English.
Prices in other currencies
Customers always pay in US dollar stablecoins. You can still price in your own currency. Send currency with one of:
USD, EUR, GBP, CAD, AUD, CHF, JPY, MXN, BRL, INR, SGD, AED, PLN, SEK, NOK, DKK, NZD, ZAR, TRY, KRW, HKD, NGN, ARS, PHP, IDR, VND, PKR, EGP, BDT, KES, GHS, TZS, COP, CLP, PEN, UYU, PYG, BOB, DOP, GTQ, CRC, THB, MYR, TWD, LKR, NPR, KZT, UAH, ILS, SAR, QAR, CZK, HUF, RON, ISK, XAF, XOF.
- Conversion: when the invoice is created, your price is converted to the USD amount the customer pays, rounded up
to the cent, so you never receive less than your price. That USD amount is
amount. It does not change afterwards. - The rate: a live rate, checked against central bank reference rates. If the two differ by more than 1%, or no
recent rate is available, the request fails with
503and"error": "fx_unavailable", and nothing is created. Try again later or price in USD. - On the invoice:
price_currencyandprice_amountare your price.fx_rateis USD per 1 unit of your currency, exactly as applied.fx_sourceandfx_atsay where and when the rate came from. - At checkout: the customer sees your price, the amount to pay and the rate.
Other invoice calls
| Call | Does |
|---|---|
GET /v1/invoices/{id} |
Returns the invoice, with its payments and refunds |
GET /v1/invoices?order_id=1042&status=paid&limit=20 |
Lists invoices, newest first, at most 100 |
GET /v1/invoices/{id}/refunds |
Returns { "data": [refund, ...] } |
POST /v1/invoices/{id}/cancel |
Cancels an open, expired or partially_paid invoice while nothing is confirming. Otherwise 409. Money already received stays in your wallet |
POST /v1/invoices/{id}/send |
Emails the customer the payment request. Body {"email": "customer@example.com"}, or empty to use the invoice's buyer_email. Open invoices only |
Payment request emails are limited to 50 per store per hour and 10 per recipient per day (429 rate_limited).
The invoice object
{
"id": "Kp3...",
"object": "invoice",
"status": "open",
"amount": "15.00",
"currency": "USD",
"price_currency": "USD",
"price_amount": "15.00",
"fx_rate": "1",
"fx_source": null,
"fx_at": null,
"amount_paid": "0.00",
"amount_pending": "0.00",
"amount_remaining": "15.00",
"overpaid": "0.00",
"amount_short": "0.00",
"paid_late": false,
"held_for_review": false,
"order_id": "1042",
"description": "Order #1042",
"metadata": {},
"locale": "es",
"checkout_url": "https://pay.mintagepay.com/i/Kp3...",
"redirect_url": "https://example.com/thank-you",
"expires_at": "2026-10-01T19:00:00.000Z",
"paid_at": null,
"created_at": "2026-10-01T18:00:00.000Z",
"payments": [
{ "network": "base", "token": "USDC", "amount": "15.000000", "status": "confirmed",
"tx": "0x...", "explorer_url": "https://basescan.org/tx/0x...", "late": false,
"from_address": "0x..." }
],
"refunds": [],
"amount_refunded": "0.00",
"send_receipt": true,
"subscription": null
}| Field | Meaning |
|---|---|
amount |
The USD amount due |
amount_paid |
Confirmed on the network |
amount_pending |
Seen on the network, not final yet |
amount_remaining |
What is still due. "0.00" once paid |
overpaid |
Paid above amount |
amount_short |
Paid within your underpayment tolerance: how much less than amount arrived |
paid_late |
true if money arrived after the invoice expired or was cancelled |
held_for_review |
true if a payment came from a sanctioned address. Do not fulfil. See Webhooks |
payments[].from_address |
The sending address. It may be an exchange's address, not the customer's own |
Amounts inside payments and refunds have up to 6 decimals, for example "15.000000".
Status
| Status | Meaning |
|---|---|
open |
Waiting for payment |
partially_paid |
Some money arrived but not enough. The payment page shows the remainder |
paid |
Confirmed payments cover amount, or fall short by no more than your underpayment tolerance. Fulfil the order |
expired |
Time ran out before the invoice was fully paid. amount_paid is above zero if the customer paid part of it |
cancelled |
You cancelled it |
Payments are still watched for after an invoice expires or is cancelled. Money sent anyway still reaches your wallet. If
it completes the amount, the invoice becomes paid with paid_late: true. If it still falls short, the status stays
as it was, paid_late becomes true and invoice.partially_paid is sent.
Underpayments and overpayments
- Tolerance (dashboard, Settings > Payments, 0% to 5%, default 0%): an invoice short of
amountby no more than this share becomespaid, withamount_shortset. Some wallets and exchanges take their fee from what the customer sends; this covers that. - Overpayment:
overpaidshows the extra. You also get a notice with the amount and the address the customer paid from, so you can send the extra back. - Fees are always charged on what arrived.
Refunds
Mintage Pay never holds your money, so you send a refund yourself, from any wallet or exchange, on the network the customer paid on. Then record it in the dashboard: open the payment under Payments, choose Refund, enter the amount and address, send it, and paste the transaction hash.
A refund counts once it is final on the network and sends the right stablecoin, at least the amount, to that address.
Refunds can never add up to more than was paid. The Mintage Pay fee is not refunded. When a refund is verified, your
endpoint receives invoice.refunded.
A refund object:
{ "id": "...", "network": "base", "token": "USDC", "amount": "5.000000", "to": "0x...",
"amount_micro": "5000000", "amount_units": "5000000", "decimals": 6,
"status": "confirmed",
"tx": "0x...", "explorer_url": "https://basescan.org/tx/0x...", "created_at": "...", "confirmed_at": "..." }status is requested or confirmed. Add up amount_micro (millionths of a dollar) for totals.
Payment links
A payment link is a product or donation page hosted by Mintage Pay. Most stores create them in the dashboard; see Payment links and the buy button. They can also be created through the API.
POST /v1/payment-links (scope links:write) creates a link and returns it (201) with the page address in url.
| Field | Notes |
|---|---|
name |
Required |
description |
Optional |
kind |
product (default) or donation |
price_mode |
fixed (default) or open for an amount the customer chooses. Donations are open |
currency |
Default: your default currency |
unit_price |
The price, for fixed |
open_min, open_max |
The lowest and highest amount, for open |
shipping |
A shipping fee |
qty_min, qty_max, stock_limit |
Quantity limits and stock |
collect_name, collect_phone, collect_shipping |
What to ask the customer for. Email is always asked |
custom_field_label |
One extra question |
success_url |
An https page to return to after payment |
presets, allow_custom |
Donation amounts |
locale |
The page's language |
GET /v1/payment-links (scope links:read) returns { "data": [...] }, active links first.
The answer is 403 review_required until your account has been reviewed.
Orders from a link
Invoices created from a link carry a payment_link object: id, name, kind, order_id, quantity, currency,
unit_price and shipping.
GET /v1/orders/{invoice id} (scope orders:read) returns the order with a customer object: email, name,
phone, custom, message and shipping_address. Customer details are kept for 2 years, then removed.
Bank transfer
Stores approved for business-to-business sales can also offer bank transfer (ACH or wire) on the payment page, after adding a bank account in the dashboard.
- When the customer chooses it, the page shows your account details and a reference unique to the invoice.
invoice.awaiting_bank_transferis sent, the status staysopen, andexpires_atmoves out to your payment terms (1 to 30 days, 7 by default). Hold the order; do not cancel it. - Mintage Pay never sees your bank. When the money arrives, record it in the dashboard under Bank transfers. Each
amount counts as a confirmed payment, so the invoice becomes
partially_paidorpaidand the usual events are sent. - Single invoice reads and webhooks carry a
bank_transferobject with thereference, theamountdue, theamount_receivedand thereceiptsyou recorded.
Not covered here
Subscriptions and payments by AI agents (x402) can be tried in the sandbox. They are not available on live accounts yet, so they are not part of this reference.
Last updated . This page as Markdown. Something missing or wrong? Write to hello@mintagepay.com.