Developer Documentation
DynoPay API Reference
Everything you need to accept crypto payments, manage customer wallets, and track transactions programmatically.
Base URL
https://dynopay.com/api/user
create-payment.sh
POST /v1/payments
Authorization: sk_live_•••
{
"amount": 79.00,
"currency": "USD",
"settle": "USDC"
}201 · checkout_url returned
Checkout Payments
Hosted payment page — redirect customers to complete crypto payments in a few clicks.
Learn more →
Direct Crypto API
Full control over the UI. Get wallet addresses and QR codes via API and build your own flow.
Learn more →
Customer Wallets
Create customer wallets, add funds, debit balances, and track transactions.
Learn more →
Webhooks
Receive real-time notifications when payments are confirmed, pending, or underpaid.
Learn more →
Navigation
Overview
Dynopay provides a simple API to accept cryptocurrency payments, manage customer wallets, and track transactions. Payments are forwarded to your configured wallet as soon as they confirm on-chain, with transparent fees. Every endpoint on this page is a merchant endpoint — authenticated with your API key.
Base URL
All merchant endpoints are relative to the base URL below. A path shown as /createPayment is called at https://dynopay.com/api/user/createPayment.
bashhttps://dynopay.com/api/userWays to accept payments
Pick whichever fits your stack — every method settles the same way (crypto in, forwarded to your wallet, webhook fired on status change). No code? Start with a Payment Link or a Buy Button.
- Hosted Checkout — call
Create Checkout Payment, get acheckout_url, and redirect the buyer to a Dynopay-hosted page. Zero front-end work. See Payments. - Payment Links — create a reusable link in the dashboard (or via API) and share it by email, chat, or QR — e.g.
dynopay.com/aBc123. Great for invoices and one-off requests, no site needed. - Buy Button — drop a
<dynopay-buy-button>snippet on any page (Webflow, WordPress, plain HTML). A “Buy Now” button opens checkout in a modal. See Buy Button below. - Direct API — call
Create Direct Crypto Paymentto get an address + QR and render your own pay screen. Full control. See Payments. - Embedded Checkout (iframe) — mount the full checkout inside your page with
embed.js+ a server-created session. See Embedded Checkout. - Elements — render the pay UI (currency picker, address, QR, live status) directly in your DOM, no iframe. See Elements Inline Widget.
- Webhooks — required for reliable fulfilment: Dynopay POSTs your server the moment a payment’s status changes. See Webhooks.
Multiple brands, one account
Run several businesses or brands from a single Dynopay login — each with its own wallets, checkout and settlement. Pass a company_id when creating payments, links or keys to scope them to a specific brand, and switch between brands in the dashboard with one click.
When to use each section
- Customers — optional. Create a customer to track payments and balances per buyer. Skip it for one-off “userless” checkouts.
- Payments — the core of the API. Create a hosted checkout or a direct crypto payment and the buyer pays in crypto.
- Wallets — top up, debit, and check a customer’s wallet balance.
- Customer Wallet Adjustments — credit or debit a customer’s store-credit balance programmatically (refunds, rewards, fees).
- Transactions — look up payment and transaction history.
- Currencies — list the cryptocurrencies you can accept.
- Webhooks — get notified the moment a payment’s status changes.
A typical payment, end to end
Create a payment
Call Create Checkout Payment with your API key and an amount. You get back a checkout URL.
Send the buyer to checkout
Redirect the buyer to the returned URL. They pick a cryptocurrency and pay.
Funds forward to your wallet
Once the payment confirms on-chain, funds are forwarded to your configured wallet, minus transparent fees.
Get notified via webhook
Dynopay posts a webhook to your server with the final status, so you can fulfil the order automatically.
Quick Integration
Most integrations only need two API calls: Create Customer → Create Payment. The customer pays in crypto, and funds forward to your wallet once the payment confirms.
Getting Started
Integrate Dynopay in just two steps — no customer creation needed:
Get your API Key
Go to your Dynopay dashboard → API section → "Create New Key". You'll receive an API key for authenticating requests.
Create a Payment
Use the Checkout Payment or Direct Crypto Payment endpoint with just your API key. No customer creation needed. The customer pays in crypto and funds forward to your wallet once the payment confirms.
Quick Example (Userless — API Key Only)
bash# Create a checkout payment — just your API key, no customer setup!
curl -X POST https://dynopay.com/api/user/createPayment \
-H "x-api-key: your_api_key" \
-H "Content-Type: application/json" \
-d '{"amount": 50, "redirect_uri": "https://yoursite.com/thanks"}'
# Or create a direct crypto payment with QR code:
curl -X POST https://dynopay.com/api/user/cryptoPayment \
-H "x-api-key: your_api_key" \
-H "Content-Type: application/json" \
-d '{"amount": 25, "currency": "BTC", "redirect_uri": "https://yoursite.com/done"}'
# Optional: Create a customer for per-customer tracking
curl -X POST https://dynopay.com/api/user/createUser \
-H "x-api-key: your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "Jane Smith", "email": "jane@example.com"}'Try It Live
Run this request right now — no signup and no API key setup. It hits our public sandbox endpoint, returns an ephemeral payment link and never touches real funds or your data. Rate-limited per IP.
Request
bashcurl -X POST "https://dynopay.com/api/public/sandbox/payment-links" \
-H "Authorization: Bearer dyno_sk_sandbox_demo_9f621db8" \
-H "Content-Type: application/json" \
-d '{
"amount": 49.99,
"currency": "USD",
"description": "Pro Plan – Monthly",
"customer_email": "customer@example.com"
}'Sample response
json{
"object": "payment_link",
"id": "plink_sandbox_2727324c61ebfb8b",
"livemode": false,
"sandbox": true,
"status": "awaiting_payment",
"amount": 49.99,
"currency": "USD",
"checkout_url": "https://dynopay.com/pay/…",
"expires_at": "2026-07-05T16:14:12.536Z",
"supported_chains": ["USDT-TRC20", "USDT-ERC20", "USDT-BEP20"]
}The sandbox key dyno_sk_sandbox_demo_9f621db8 is public and only works on this endpoint. When you are ready to go live, create your own API key in the dashboard and switch to the real endpoints below — the request shape is the same. You can also open the live checkout demo to see what your customers experience.
Authentication
Your API key is all you need. Every server-side endpoint authenticates with a single x-api-key header — no customer login and no token exchange. Two other key types exist only for specific cases:
API Key — all you need
Send your secret x-api-key header with every request. It powers everything — payments, checkout, wallets and transactions. Keep it server-side; never expose it in a browser.
x-api-key: dpk_live_Ab3k…Keys look like dpk_live_… / dpk_test_… and are shown once when created or regenerated — Dynopay stores only a one-way hash, so a key can never be recovered from our systems. Lost it? Regenerate from Developer › API keys; the old key stops working immediately.
Publishable Key (browser)
Only for Embedded Checkout and Elements, which run in your customer's browser. Use your domain-restricted publishable key (pk_live_…) there — never your secret API key.
# Browser only
pk_live_xxxxxxxxxxxxCustomer Token (optional)
Advanced and rarely needed. A few endpoints accept an optional customer Bearer token to scope wallet balances and history to one customer. You obtain it from POST /createUser. Omit it and the API runs in userless mode.
x-api-key: your_api_key
# Optional (advanced):
Authorization: Bearer {token_from_createUser}Customers
/api/user/createUser
Create Customer
API KeyPayments
Payment lifecycle
A crypto payment returns a single-use deposit address (and QR). The customer sends the crypto; once the network reaches the required confirmations for that asset the payment moves pending → confirmed → settled and funds are forwarded to your wallet. The deposit window is 24h — after that an unpaid payment goes expired.
- Underpaid — the address stays open for the remainder;
payment.underpaidreports how much arrived and how much is still owed. - Overpaid — the payment settles and the entire excess is credited to you (the fee is charged once, on the expected amount).
- XRP / RLUSD — you must display the
destination_tagalongside the address, or the deposit cannot be credited. - Confirm fulfilment from the
payment.settledwebhook (or re-verify withGET /getPaymentStatus/:payment_id) — never the browser redirect.
/api/user/createPayment
Create Checkout Payment
API Key/api/user/cryptoPayment
Create Direct Crypto Payment
API KeyEmbedded Checkout
Embedded Checkout mounts the Dynopay payment UI inside your page. Two steps: create the session on your server (your secret key stays server-side), then mount it in the browser with the returned client_secret.
javascript// 1) SERVER — create a session with your SECRET api key
const r = await fetch('https://dynopay.com/api/user/embed/session', {
method: 'POST',
headers: { 'x-api-key': process.env.DYNOPAY_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 49.99, currency: 'USD', webhook_url: 'https://you.com/webhooks/dynopay' }),
});
const { client_secret } = (await r.json()).data; // safe to send to the browser
// 2) BROWSER — load embed.js from your checkout origin, then mount inline
Dynopay.initEmbeddedCheckout({
fetchClientSecret: async () => client_secret,
}).then((checkout) => checkout.mount('#dynopay-checkout'));
// ...or Dynopay.openCheckout({ fetchClientSecret }) to open it as a modal.Confirm on the webhook, not the browser
The onComplete event is UX only — a customer can close the tab. Fulfil the order from the payment.settled webhook. Sessions expire after 24h; the client_secret is safe for the browser, your x-api-key is not.
/api/user/embed/session
Create Embedded Checkout Session
API KeyElements Inline Widget
Elements is an inline widget you style into your own checkout. Flow: create an intent on your server, mount the widget in the browser with your publishable key (pk_live_…), and the customer picks a currency — which reserves a deposit address (select-currency, idempotent). The SDK polls elements/status roughly every 5s; the intent expires after 24h.
Publishable key in the browser
Mount Elements with your domain-restricted pk_live_… publishable key — never your secret x-api-key. As always, confirm fulfilment from the payment.settled webhook, not the widget completion event.
/api/embed/public/elements/intent
Create Elements Payment Intent
Publishable Key/api/embed/public/elements/select-currency
Select Currency (Elements)
Publishable Key/api/embed/public/elements/status?intent_id=pi_...
Poll Elements Intent Status
Publishable KeyWallets
/api/user/addFunds
Add Funds to Wallet
API Key/api/user/useWallet
Debit from Wallet
API Key/api/user/getBalance
Get Wallet Balance
API KeyTransactions
/api/user/getTransactions
List Transactions
API Key/api/user/getSingleTransaction/:id
Get Transaction Details
API Key/api/user/getCryptoTransaction/:address
Verify Crypto Payment
API Key/api/user/getPaymentStatus/:payment_id
Verify Payment by ID (recommended)
API KeyTesting (Sandbox)
Test vs live
Your key prefix decides the environment — nothing else in your integration changes. A dpk_test_ key creates sandbox payments (no real crypto, no on-chain settlement); a dpk_live_ key creates real ones. Test and live payments, webhook secrets and event logs are fully isolated.
The test loop
- Create a payment with your
dpk_test_key (e.g.POST /cryptoPayment). It starts inpending. - Advance it: call
POST /simulatePayment/:payment_id(below), or click Simulate in Developer › API keys. It walkspending → confirmed → settled. - Your endpoint receives the real, signed
payment.pending / payment.confirmed / payment.settledwebhooks — the samewhsec_secret and V2 signature spec as live.
Sandbox limits
Test keys can be capped for safety — an optional max amount and an allowed-currency list. A request above the cap, or a currency outside the list, is rejected with 400 sandbox_restriction. Raise the limits on the key, or use a dpk_live_ key for production amounts.
Going live
Swap your dpk_test_ key for a dpk_live_ key from Developer › API keys — the request shape and endpoints are identical. Live payments can never be advanced with Simulate (it returns 403).
/api/user/simulatePayment/:payment_id
Simulate Payment (test mode only)
API KeyWebhook Events
/api/user/events
List Webhook Events
API Key/api/user/events/:id/resend
Resend a Webhook Event
API KeyCurrencies
/api/user/getSupportedCurrency
Get Supported Currencies
API KeyCustomer Wallet Adjustments
/api/user/customers/:customerId/credit
Credit Customer Wallet
API Key/api/user/customers/:customerId/debit
Debit Customer Wallet
API KeyPayment Statuses
Every crypto payment carries a normalized payment_status. You get it back from every create call, from GET /getPaymentStatus/:payment_id, and inside the payment.* webhooks. Fulfil the order once it reaches settled — the convenience flag is_paid is true only when the status is settled.
| Status | In your wallet? | Final? | What it means |
|---|---|---|---|
waiting | No | No | Payment created — waiting for the customer to send crypto. Nothing detected on-chain yet. |
pending | No | No | Crypto has been detected on-chain and is awaiting the required network confirmations. |
confirmed | On-chain | No | Confirmations reached — the crypto is received on-chain; forwarding to your merchant wallet comes next. |
processing | In transit | No | Settlement in progress — funds are being forwarded (and auto-converted, if enabled) to your wallet. |
settled | Yes | Yes | Success. Funds have been delivered to your merchant wallet and is_paid is true. Safe to fulfil the order. |
underpaid | Partial | No | The customer sent less than the requested amount. Collect the remainder or issue a refund. |
failed | No | Yes | The payment could not be completed after all retries. |
expired | No | Yes | The payment window elapsed before sufficient funds arrived. |
refunded | No | Yes | Funds were returned to the sender. |
Terminal statuses (settled, failed, expired, refunded) never change again — stop polling once you see one. Everything else is transitional.
Underpayments: how much is left to pay
When a customer sends less than the requested amount the payment goes underpaid and the deposit address stays open for the remainder during the grace period. GET /getPaymentStatus/:payment_id (and the payment.underpaid webhook) report exactly how much arrived and how much is still owed:
| Field | What it is |
|---|---|
amount | Full crypto amount expected for the payment. |
amount_received | Crypto received so far. Equals the full amount once paid; the partial amount while underpaid. |
amount_remaining | Crypto still owed. 0 once fully paid; non-zero only while underpaid. |
paid_amount | Alias of amount_received (crypto received so far). |
amount_received_base | amount_received expressed in the payment's base_currency (USD, EUR, …). |
amount_remaining_base | amount_remaining expressed in the payment's base_currency (USD, EUR, …). |
The customer can complete the payment by sending amount_remaining in the same coin to the same address before the grace period ends — no new payment is needed.
Confirming a payment: webhooks vs. polling
- Prefer webhooks. Subscribe to
payment.confirmedandpayment.settled(pluspayment.underpaidandpayment.settlement_failed) to react in real time. Only fulfil the order once the status issettled. - Reconcile with the API.
GET /getPaymentStatus/:payment_idis keyed on the immutablepayment_idand returns the authoritative status from the database — still correct after the checkout session expires. Use it to re-verify a webhook, or catch up on anything you missed withGET /events. - If you must poll, call
getPaymentStatusevery few seconds, respect the 100 requests/minute limit (back off on429), and stop at the first terminal status. - Do not rely on the browser alone. The post-payment redirect /
onCompleteevent is UX only — a customer can close the tab. Server-side webhooks (or a reconciliation poll) are the source of truth.
Buy Button
The Buy Button is the fastest way to sell without writing back-end code. Create a button once in the dashboard, then paste a small HTML snippet anywhere — a landing page, Webflow, WordPress, a blog, even an email-linked page. Your shopper clicks Buy Now and the Dynopay hosted checkout opens in a modal (or inline). The amount is fetched securely from our server by button-id, so it can never be tampered with in the browser.
1 · Create a button
- Go to Developers → Buy Buttons in your dashboard.
- Set a label, a fixed price (or a min/max range so the customer chooses), and the currencies you accept.
- Copy the generated snippet. It already includes your publishable key (
pk_live_…) and button id (btn_…).
2 · Paste the snippet
html<!-- Dynopay embed SDK — load once per page -->
<script src="https://checkout.dynopay.com/v1/embed.js"></script>
<!-- Paste the button anywhere on your page -->
<dynopay-buy-button
publishable-key="pk_live_your_key"
button-id="btn_xxxxxxxx"
mode="modal"
theme="dark"
></dynopay-buy-button>Attributes
| Attribute | Required | Description |
|---|---|---|
publishable-key | Yes | Your browser-safe key (pk_live_… or pk_test_…). Domain-locked and amount-capped — never your secret API key. |
button-id | Yes | The btn_… id of the button you created. The price is resolved server-side from this id. |
amount | No | Only for range/customer-chooses buttons — the default amount to pre-fill. Ignored for fixed-price buttons. |
currency | No | Pre-select a single crypto (e.g. BTC). Omit to let the buyer choose from your accepted coins. |
mode | No | "modal" (default) opens checkout in an overlay; "redirect" sends the buyer to the hosted page. |
theme | No | "dark" or "light" — match your page. |
Fulfil on the webhook, not the button
The button is UX only. Always confirm the final payment via the payment.confirmed webhook (see below) before you deliver the product or mark the order paid.
Webhooks
Dynopay sends webhook notifications to your configured URL when payment events occur. You set the webhook_url when creating a payment, or configure a default in your company settings.
Event Types
| Event | Description | Action |
|---|---|---|
payment.pending | Crypto deposit detected on the blockchain (unconfirmed) | Show "payment received" to user — wait for confirmation |
payment.confirmed | Payment fully confirmed (sufficient blockchain confirmations) | Fulfill the order / deliver the product |
payment.underpaid | Partial payment received (less than expected amount) | Notify customer or wait for remainder during grace period |
Webhook Payload
All webhook events are sent as POST requests with a JSON body to your configured URL.
json{
"event": "payment.confirmed",
"payment_id": "pay_abc123def456",
"transaction_id": "txn_789xyz",
"amount": 0.00042,
"currency": "BTC",
"base_amount": "25.00",
"base_currency": "USD",
"status": "completed",
"customer_email": "jane@example.com",
"merchant_id": 38,
"destination_tag": null,
"meta_data": { "order_id": "ORD-12345" },
"timestamp": "2025-07-15T12:00:00.000Z"
}Webhook Headers
| Header | Description |
|---|---|
Content-Type | application/json |
X-DynoPay-Event | The event type (e.g. payment.confirmed) |
X-Dynopay-Signature-V2 | (Recommended) t=<unix>,v1=<hex> — HMAC-SHA256 over "<t>.<rawBody>" (the exact bytes sent). Only present when a webhook secret is configured. |
X-DynoPay-Signature | (Legacy) HMAC-SHA256 over a re-serialised body — kept for backward compatibility. |
X-DynoPay-Timestamp | Unix timestamp of when the webhook was sent |
X-DynoPay-Webhook-Id | Unique webhook delivery ID for idempotency |
Signature Verification
Verify the signature header to ensure webhook requests are authentic and haven't been tampered with. Prefer X-Dynopay-Signature-V2: it is signed over the exact bytes on the wire, so you can verify it in any language without re-serialising the parsed JSON. Both headers are sent while you migrate; the legacy X-DynoPay-Signature will be removed after the migration window closes. Signatures are sent only when your endpoint has a signing secret — saving a webhook URL (or creating an API key) auto-generates a whsec_… secret (shown once); endpoints without one receive the payload unsigned.
Signing spec
- Signed message — the exact string
{t}.{rawBody}: the timestamp, a literal dot, then the raw request-body bytes. No webhook id, no re-serialisation. - HMAC key — your signing secret used verbatim: the full
whsec_…string (its UTF-8 bytes). Do not strip thewhsec_prefix and do not base64-decode it. - Algorithm — HMAC-SHA256.
- Encoding — lowercase hex (this is the
v1=value). - Timestamp
t— Unix seconds (the same value as theX-DynoPay-Timestampheader). Reject anything outside ±300s to stop replays.
Which secret signs which endpoint
Each endpoint is signed with its own secret. Your account / dashboard endpoint is signed with your account webhook secret (the whsec_… shown once when you save the URL). A per-payment webhook_url is signed with the secret in effect on that payment. Always verify a request with the secret that belongs to the endpoint receiving it.
Secret rotation & multiple signatures
When you rotate a signing secret we co-sign each webhook with both the new and the previous secret for a ~24-hour grace window, so one delivery's X-Dynopay-Signature-V2 header can carry several v1= values, e.g. t=…,v1=<new>,v1=<previous>. Your verifier must scan every v1= token and accept if any one matches — never parse just one (a naive Object.fromEntries on the comma-split keeps only the last, which is the old secret, and fails every webhook). The first v1= is always the current secret.
V2 (recommended)
javascriptconst crypto = require('crypto');
// Verify against the RAW request body. Reject anything older than ±300s (replay).
function verifyWebhookV2(rawBody, headerV2, secret, toleranceSec = 300) {
const tokens = String(headerV2).split(',').map((s) => s.trim());
const t = Number((tokens.find((p) => p.startsWith('t=')) || '').slice(2));
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const exp = Buffer.from(expected, 'hex');
// During a secret rotation DynoPay sends MULTIPLE v1= values (current +
// previous, ~24h grace). Match if ANY verifies — always scan them all,
// never just parse a single v1 (Object.fromEntries would drop all but the last).
return tokens
.filter((p) => p.startsWith('v1='))
.some((p) => {
const got = Buffer.from(p.slice(3), 'hex');
return got.length === exp.length && crypto.timingSafeEqual(got, exp);
});
}
// Express — capture the raw body so the bytes are byte-identical to what was signed.
app.post('/webhooks/dynopay', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body.toString();
const v2 = req.headers['x-dynopay-signature-v2'];
if (v2 && !verifyWebhookV2(rawBody, v2, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(rawBody);
// Fulfill based on event.event (payment.confirmed, payment.pending, ...)
res.status(200).json({ received: true });
});Test vector
Feed these exact inputs to your HMAC routine — if you get the same v1, your signing is byte-correct and any remaining failure is header parsing (see rotation note above).
textsecret = whsec_test_secret_example
t = 1700000000
raw_body = {"event":"payment.settled","payment_id":"pay_123","amount":19.99}
signed string = 1700000000.{"event":"payment.settled","payment_id":"pay_123","amount":19.99}
v1 (hex) = f49a2735cfb74e9d7a03537ff1af830cf6876f25cd1cb42fe4143bf2e5cb1c66Replay & idempotency
Reject any delivery whose t is more than ±300s from now to block replays. The same event can also arrive more than once (delivery retries, or a manual resend), so treat X-DynoPay-Webhook-Id as an idempotency key: record the ids you have processed and skip duplicates.
V1 (legacy)
javascriptconst crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
return signature === expectedSignature;
}
// In your Express webhook handler:
app.post('/webhooks/dynopay', (req, res) => {
const signature = req.headers['x-dynopay-signature'];
const isValid = verifyWebhook(req.body, signature, process.env.WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
switch (req.body.event) {
case 'payment.confirmed':
// Fulfill order
break;
case 'payment.pending':
// Show pending status
break;
case 'payment.underpaid':
// Notify customer
break;
}
// Always return 200 to acknowledge receipt
res.status(200).json({ received: true });
});Retry Policy
If your endpoint returns a non-2xx status code (or times out), Dynopay retries delivery on a fixed schedule — 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, then 24 h (7 retries over ~24 hours). After the final attempt the event is marked failed. You can list past deliveries with GET /events and re-send any of them with POST /events/:id/resend (or from the dashboard).
Webhook URL Priority
| Priority | Source | When to Use |
|---|---|---|
1st | Per-payment webhook_url field | Different webhook per payment or product |
2nd | API Key webhook settings | Different webhook per API integration |
3rd | Company default settings | Same webhook for all payments |
Rate Limits
Dynopay enforces rate limits to ensure platform stability. Limits are applied per IP address and per API key.
| Endpoint Category | Limit | Window |
|---|---|---|
| Payment creation | 30 requests | 1 minute |
| General API | 100 requests | 1 minute |
| Authentication (login) | 10 requests | 15 minutes |
| Webhook delivery | 200 requests | 5 minutes |
When rate limited, the API returns HTTP 429 Too Many Requests with a Retry-After header indicating when you can retry.
Error Handling
All errors return the human-readable message plus a machine-readable error object — a stable type and code you can branch on, the offending param, a doc_url, and a request_id (also returned as a Request-Id response header — quote it when contacting support). The legacy success/message/errors fields are unchanged, so existing integrations keep working.
json{
"success": false,
"message": "Amount must be greater than or equal to 5",
"errors": [{ "key": "amount", "error": "Invalid amount" }],
"error": {
"type": "invalid_request_error",
"code": "amount_below_minimum",
"message": "Amount must be greater than or equal to 5",
"param": "amount",
"doc_url": "https://dynopay.com/documentation#errors",
"request_id": "req_8f3c2a1b"
}
}error.type is one of invalid_request_error, authentication_error, rate_limit_error or api_error.
| Status | Meaning |
|---|---|
400 | Bad Request — missing or invalid parameters |
401 | Unauthorized — invalid or missing API key / token |
403 | Forbidden — authenticated but not allowed (e.g. wrong role, or a publishable key used from a disallowed origin) |
404 | Not Found — resource does not exist |
429 | Too Many Requests — rate limit hit; retry after the Retry-After header |
500 | Server Error — something went wrong on our side |
Error codes
Branch on error.code — these stay stable even if we reword a message.
| code | HTTP | When it fires |
|---|---|---|
api_key_missing | 401 | No x-api-key header on the request |
api_key_invalid | 401 | API key not recognized or revoked |
amount_invalid | 400 | amount is missing or not greater than 0 |
amount_below_minimum | 400 | amount is below the minimum (createPayment: 5) |
currency_required | 400 | currency was omitted |
currency_not_available | 400 | Requested currency has no wallet configured (see param) |
no_wallet_configured | 400 | The merchant has no crypto wallet set up yet |
parameter_missing | 400 | A required field is absent — see error.param |
rate_unavailable | 500 | Could not fetch a conversion rate — safe to retry |
sandbox_restriction | 400 | A test (dpk_test_) key exceeded its sandbox limits |
invalid_idempotency_key | 400 | Idempotency-Key header is empty or too long |
idempotency_key_reused | 409 | Idempotency-Key reused with a different request body |
idempotency_request_in_progress | 409 | The first request with this key is still processing |
Idempotent retries
Send an Idempotency-Key header on any POST (a unique value per logical request, e.g. a UUID) to make it safe to retry. If a network blip makes you retry, the same key + same body replays the original response — with an Idempotent-Replay: true header — so a payment is never created twice. Reusing a key with a different body returns 409 idempotency_key_reused; keys are remembered for 24 hours.
bashcurl https://api.dynopay.com/api/user/createPayment \
-H "x-api-key: $DYNOPAY_API_KEY" \
-H "Idempotency-Key: 8f14e45f-ce2a-4b7d-9a1e-1b2c3d4e5f60" \
-H "Content-Type: application/json" \
-d '{"amount": 25, "redirect_uri": "https://your-site.com/thanks"}'From 0.5% · no chargebacks
Ready to ship?
Non-custodial, from 0.5%, no chargebacks — create your API key and take your first payment today.