Skip to content
API Documentation

BluPay REST API reference

Every client web service you need to connect the gateway and confirm payments — with authentication, request and response shapes, and the signed payment webhook. For integration code examples, see the developer overview.

Conventions

Base URL

https://blupay.me/api

Every path below is relative to this base, e.g. POST https://blupay.me/api/v1/invoices/

Authentication

X-API-Key: <your per-shop key>

Create keys per shop in the dashboard. Each key carries scopes (e.g. invoices:write). Optionally restrict a key to specific source IPs (IPv4 or CIDR); leaving the list empty accepts any IP.

Success envelope

{ "error": false, "number": 0, "message": { ... } }

For JSON endpoints, your data is under message.

Errors & idempotency

{ "error": true, "number": 404, "message": "Invoice not found." }

number is the HTTP status. Use an Idempotency-Key on invoice creation and check each endpoint’s retry contract.

Sandbox is a shop-level setting: create a Sandbox shop and use its key. Sending isSandbox on invoice creation does not switch a live shop into test mode. Callback URLs require a registered website domain and must use that host or a subdomain. Keep all API keys on your server. Monetary values are decimal strings: do not convert them through binary floating-point. IRT amounts are tomans and IRR amounts are rials (1 IRT = 10 IRR); send the amount in its stated unit once. Conversion availability must come from the shop context, not a hard-coded currency list. Examples are illustrative subsets; preserve returned identifiers and URLs unchanged.

Currencies

Discover the assets and networks your shop can accept. Use the returned currencyNetworkId values when restricting an invoice with acceptedCurrencyNetworkIds.

GET/v1/currencies/{type}/currencies:read

List the enabled currencies of a given type.

Path parameters

type*stringEither "crypto" (assets you can be paid in) or "fiat" (display/quote currencies).

Example response

{
  "error": false,
  "number": 0,
  "message": [
    {
      "id": 1,
      "symbol": "USDT",
      "name": "Tether USD",
      "iconUrl": null,
      "precision": 6,
      "type": "Crypto",
      "networks": [
        {
          "currencyNetworkId": 3,
          "networkId": 1,
          "networkSymbol": "TRC20",
          "networkName": "Tron",
          "isNative": false,
          "contractAddress": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
          "minimumPayment": "1",
          "supportsMemo": false,
          "supportsWalletConnect": false,
          "requiredConfirmations": 19
        }
      ]
    }
  ]
}

Fiat items have the same top-level currency fields but omit networks. Crypto network rows also include minimumDeposit, feePayment, networkIconUrl, paymentable, and depositable. IDs and enabled networks are illustrative: discover current values, do not hard-code these examples. The list is platform-wide; a shop can have additional enabled-network restrictions.

Conversion diagnostics

Read-only availability and indicative checkout amounts for the API key shop. These calls do not create an invoice, reserve funds or lock a payable quote. Availability depends on enabled currencies, networks and fresh provider rates; never invent a fallback rate.

GET/v1/quotes/context/currencies:read

Read shop conversion availability.

Query parameters

currencySymbol*stringUppercase currency symbol, e.g. IRT. No other query fields are accepted.

Example response

{
  "error": false,
  "number": 0,
  "message": {
    "currencySymbol": "IRT",
    "supported": true,
    "available": false,
    "reason": "rate_unavailable",
    "mode": "live",
    "rateMode": "live",
    "targets": [],
    "rate": null
  }
}

For available targets the response includes currencyNetworkId, currencySymbol, networkSymbol and rate provenance. A supported symbol does not guarantee a usable rate. Use targets from this shop, not hard-coded IDs.

POST/v1/quotes/preview/currencies:read

Preview a checkout amount without financial writes. Returns 200 when available.

Request body

currencySymbol*stringUppercase source currency symbol.
currencyAmount*stringPositive decimal string, max 12 integer digits and 8 decimals.
currencyNetworkId*integerEnabled shop target from context. Between 1 and 2147483647.

Example request

{
  "currencySymbol": "IRT",
  "currencyAmount": "250000",
  "currencyNetworkId": 7
}

Example response

{
  "error": true,
  "number": 503,
  "message": "Rate is unavailable."
}

This example shows an unavailable-rate error. Success returns indicative=true, currencyAmount, quoteCurrencySymbol, networkSymbol, cryptoAmount, finalCryptoAmount, serviceFeeCrypto, transferFeeCrypto, mode, rateMode and rate (ratio, providers, observation timestamps, expiresAt). Reject expired previews. No address or paymentUrl is issued. Other fields, including shopId, fee or rate overrides, are rejected. This endpoint does not use invoice Idempotency-Key semantics.

Shops

A shop holds your website, fee, sandbox and auto-convert settings. Each API key belongs to one shop. A signing secret is generated at shop creation and returned as webhookSecretPlain. Callback URLs are supplied per invoice, not saved on the shop.

GET/v1/shops/shops:read

List the shops owned by the authenticated merchant.

Example response

{
  "error": false,
  "number": 0,
  "message": [
    {
      "id": "c1a2…",
      "uuid": "b3f1c2a4-8b0c-4d32-9f19-6f8a76423051",
      "name": "Your store",
      "description": null,
      "status": "Enable",
      "websiteUrl": "https://merchant.example.com",
      "logoUrl": null,
      "telegramBotUsername": null,
      "webhookSecret": "********",
      "webhookSecretMasked": "9f2a****",
      "serviceFeeBy": "Merchant",
      "transferFeeBy": "Customer",
      "serviceFeePercent": "1",
      "autoConvertEnabled": false,
      "autoConvertTarget": null,
      "autoConfirmationTolerancePercent": "0",
      "isSandbox": false,
      "posUrl": null,
      "createdAt": "2026-06-09T12:00:00.000Z",
      "updatedAt": "2026-06-09T12:00:00.000Z"
    }
  ]
}
POST/v1/shops/shops:write

Create a new shop.

Create your first shop in the dashboard before issuing an API key. API creation requires an existing key with shops:write. Every new shop returns webhookSecretPlain once; save it securely. The authenticated dashboard can reveal or rotate that secret. At least one of websiteUrl or telegramBotUsername is required.

Request body

name*string2–120 characters.
descriptionstringUp to 2000 characters.
websiteUrlstring (url)Registered storefront URL (max 500 chars). Required for per-invoice callback URLs.
telegramBotUsernamestringAlternative to websiteUrl: 5–32 letters, digits or underscores, optionally starting with @.
serviceFeeBy"Customer" | "Merchant"Who pays the configured gateway fee; read serviceFeePercent from the response.
transferFeeBy"Customer" | "Merchant"Who pays the on-chain network fee.
autoConvertEnabledbooleanAuto-convert incoming funds.
autoConvertTarget"USDT"Asset to auto-convert into.
autoConfirmationTolerancePercentnumber0–3. Treat tiny underpayments as paid.
isSandboxbooleanCreate a sandbox (test) shop.

Example request

{
  "name": "Your store",
  "websiteUrl": "https://merchant.example.com",
  "serviceFeeBy": "Merchant",
  "transferFeeBy": "Customer"
}

Example response

{
  "error": false,
  "number": 0,
  "message": {
    "uuid": "b3f1c2a4-8b0c-4d32-9f19-6f8a76423051",
    "name": "Your store",
    "webhookSecretPlain": "<save the returned signing secret securely>",
    "isSandbox": false,
    "status": "Enable"
  }
}
GET/v1/shops/{shopId}/shops:read

Retrieve a single shop by its uuid.

Path parameters

shopId*string (uuid)The shop uuid.

Example response

{ "error": false, "number": 0, "message": {
    "id": "c1a2…",
    "uuid": "b3f1c2a4-8b0c-4d32-9f19-6f8a76423051",
    "name": "Your store",
    "description": null,
    "status": "Enable",
    "websiteUrl": "https://merchant.example.com",
    "logoUrl": null,
    "telegramBotUsername": null,
    "webhookSecret": "********",
    "webhookSecretMasked": "9f2a****",
    "serviceFeeBy": "Merchant",
    "transferFeeBy": "Customer",
    "serviceFeePercent": "1",
    "autoConvertEnabled": false,
    "autoConvertTarget": null,
    "autoConfirmationTolerancePercent": "0",
    "isSandbox": false,
    "posUrl": null,
    "createdAt": "2026-06-09T12:00:00.000Z",
    "updatedAt": "2026-06-09T12:00:00.000Z"
  } }
PUT/v1/shops/{shopId}/shops:write

Update a shop. All fields are optional (send only what changes).

Updates do not rotate the webhook signing secret. Callback URLs belong to individual invoices. Use the authenticated dashboard to manage signing secrets.

Path parameters

shopId*string (uuid)The shop uuid.

Request body

…partialAny field from “Create a new shop”, all optional.

Example request

{ "serviceFeeBy": "Customer", "transferFeeBy": "Customer" }

Example response

{ "error": false, "number": 0, "message": { "uuid": "b3f1c2a4-8b0c-4d32-9f19-6f8a76423051", "serviceFeeBy": "Customer" } }

Invoices

The core of the integration. Create an invoice, redirect the buyer to its paymentUrl, then confirm the result via webhook and/or by polling the invoice.

POST/v1/invoices/invoices:write

Create an invoice (the gateway charge). Returns 201.

Use an enabled currencySymbol returned by the currencies API. The payable crypto amount is quoted at asset/network selection, not invoice creation. Save the returned link against your order and redirect to paymentUrl unchanged. Persist an Idempotency-Key before the request and reuse it with the identical body for retries. A different payload or an in-flight attempt returns 409.

Request body

currencyAmount*stringPositive decimal string, max 12 integer digits and 8 decimals; no exponent notation. Example: "49.00".
currencySymbol*stringCurrency the amount is quoted in, e.g. "USD".
shopIdstring (uuid)Optional. Must equal the API key’s shop UUID if supplied; a different shop returns 403.
orderIdstringYour order reference (≤120 chars).
orderDescriptionstringShown on the checkout (≤2000 chars).
customerEmailstring (email)Buyer email for receipts.
redirectUrlstring (url)Return URL (max 500 chars) on the registered website host or a subdomain. Never proves payment.
webhookUrlstring (url)Payment event URL (max 500 chars) for this invoice, on the registered website host or a subdomain. Set it to receive webhooks; no shop URL is configured.
timeForPaymentnumberMinutes until expiry (5–180).
rateMode"Fixed" | "Dynamic"Stored policy flag: Fixed (default) or Dynamic. No crypto quote exists at invoice creation; use the checkout quote and its expiry.
serviceFeeBy"Customer" | "Merchant"Override who pays the gateway fee.
transferFeeBy"Customer" | "Merchant"Override who pays the network fee.
acceptedCurrencyNetworkIdsnumber[]Restrict payable assets to these currencyNetworkId values.
autoConvertEnabledbooleanOverride the shop auto-convert setting.
autoConvertTarget"USDT"Supported auto-convert target.
autoConfirmationTolerancePercentnumber0–3, permitted underpayment tolerance. Use Paid status rather than requiring a zero remainder.

Example request

{
  "shopId": "b3f1c2a4-8b0c-4d32-9f19-6f8a76423051",
  "currencyAmount": "49.00",
  "currencySymbol": "USD",
  "orderId": "order-1024",
  "orderDescription": "Pro plan — monthly",
  "customerEmail": "buyer@example.com",
  "redirectUrl": "https://merchant.example.com/thanks",
  "webhookUrl": "https://merchant.example.com/blupay/webhook",
  "timeForPayment": 60,
  "acceptedCurrencyNetworkIds": [3]
}

Example response

{ "error": false, "number": 0, "message": {
    "link": "AbCd1234_-xy",
    "status": "Created",
    "source": "API",
    "currencyAmount": "49.00",
    "currencySymbol": "USD",
    "remainingCurrencyAmount": "49.00",
    "customerEmail": "buyer@example.com",
    "orderId": "order-1024",
    "orderDescription": "Pro plan — monthly",
    "expiresAt": "2026-06-09T13:00:00.000Z",
    "serviceFeeBy": "Merchant",
    "transferFeeBy": "Customer",
    "serviceFeePercent": "1",
    "autoConvertEnabled": false,
    "autoConvertTarget": null,
    "isSandbox": false,
    "paymentUrl": "https://blupay.me/pay/#AbCd1234_-xy",
    "shop": { "uuid": "b3f1c2a4-8b0c-4d32-9f19-6f8a76423051", "name": "Your store", "logoUrl": null, "websiteUrl": "https://merchant.example.com" },
    "createdAt": "2026-06-09T12:00:00.000Z"
  } }
GET/v1/invoices/invoices:read

List invoices with filters and pagination.

Query parameters

statusstringComma-separated set of Created, Pending, Paid, Partially, PartiallyExpired, Expired or Canceled.
shopIdstring (uuid)Filter by shop.
fromstring (ISO date)Created on/after this time.
tostring (ISO date)Created on/before this time.
searchstringMatch link, orderId, or customerEmail.
sourcestringDashboard · API · WooCommerce · POS · PaymentButton · PaymentLink.
pagenumberDefaults to 1.
pageSizenumberDefaults to 20 (max 200).

Example response

{
  "error": false,
  "number": 0,
  "message": {
    "items": [{
        "link": "AbCd1234_-xy",
        "status": "Created",
        "source": "API",
        "currencyAmount": "49.00",
        "currencySymbol": "USD",
        "remainingCurrencyAmount": "49.00",
        "customerEmail": "buyer@example.com",
        "orderId": "order-1024",
        "orderDescription": "Pro plan — monthly",
        "expiresAt": "2026-06-09T13:00:00.000Z",
        "serviceFeeBy": "Merchant",
        "transferFeeBy": "Customer",
        "serviceFeePercent": "1",
        "autoConvertEnabled": false,
        "autoConvertTarget": null,
        "isSandbox": false,
        "paymentUrl": "https://blupay.me/pay/#AbCd1234_-xy",
        "shop": { "uuid": "b3f1c2a4-8b0c-4d32-9f19-6f8a76423051", "name": "Your store", "logoUrl": null, "websiteUrl": "https://merchant.example.com" },
        "createdAt": "2026-06-09T12:00:00.000Z"
      }],
    "total": 137,
    "page": 1,
    "pageSize": 20
  }
}
GET/v1/invoices/{link}/invoices:read

Retrieve a single invoice and poll its status to confirm payment.

Path parameters

link*stringThe invoice link returned at creation.

Example response

{ "error": false, "number": 0, "message": {
    "link": "AbCd1234_-xy",
    "status": "Created",
    "source": "API",
    "currencyAmount": "49.00",
    "currencySymbol": "USD",
    "remainingCurrencyAmount": "49.00",
    "customerEmail": "buyer@example.com",
    "orderId": "order-1024",
    "orderDescription": "Pro plan — monthly",
    "expiresAt": "2026-06-09T13:00:00.000Z",
    "serviceFeeBy": "Merchant",
    "transferFeeBy": "Customer",
    "serviceFeePercent": "1",
    "autoConvertEnabled": false,
    "autoConvertTarget": null,
    "isSandbox": false,
    "paymentUrl": "https://blupay.me/pay/#AbCd1234_-xy",
    "shop": { "uuid": "b3f1c2a4-8b0c-4d32-9f19-6f8a76423051", "name": "Your store", "logoUrl": null, "websiteUrl": "https://merchant.example.com" },
    "createdAt": "2026-06-09T12:00:00.000Z"
  } }
GET/v1/invoices/{link}/pdf/invoices:read

Download a PDF copy of the invoice.

Path parameters

link*stringThe invoice link.

Example response

200 OK
Content-Type: application/pdf
(binary PDF stream)
GET/v1/invoices/export/csv/invoices:read

Stream all of the merchant’s invoices as CSV.

Example response

200 OK
Content-Type: text/csv; charset=utf-8
Content-Disposition: attachment; filename="invoices.csv"

Confirming payments & fulfilling orders

Crypto payments are confirmed server-to-server, never in the browser. Treat the buyer returning to your site as a UX convenience only — always confirm on your backend before you release an order.

Recommended

1 · Listen for the webhook

BluPay POSTs a signed invoice.paid event to your webhookUrl when the invoice reaches Paid under its configured payment/tolerance policy. Verify the signature, match the invoice to the stored order, and record it once (keyed on event_id), with a unique fulfilment record for that order, then fulfil. See Payment webhook.

Fallback

2 · Poll the invoice

From your server, call GET /v1/invoices/{link}/ and release the order when status === "Paid". Match the fetched invoice to the saved order and fulfil atomically once; poll from your server for reconciliation, never from an untrusted browser result.

Redirect (return) URL — what is sent

Set redirectUrl per invoice when creating it, on the shop's registered website domain. After the buyer finishes, the hosted checkout shows a “Return to merchant” button that links to that URL. BluPay does not append any status, invoice id, or signature to it, and the buyer may close the tab before clicking — so never fulfill an order based on the redirect alone. Use it only to bring the customer back into your flow.

Is there a “verify payment” call like card / bank gateways?

BluPay does not require a separate capture call for these invoices. Verify a signed webhook or fetch the invoice status on your server with invoices:read. Match the returned invoice link, order reference, currency and original amount to your saved order, and fulfil it only once when the status is Paid. A browser redirect, Pending or Partially state is not payment confirmation.

Payment confirmation webhook

On every invoice state change, BluPay sends a signed POST to the invoice’s webhookUrl. This is how you confirm a payment server-to-server. Verify the signature, enforce a 5-minute replay window, then respond 200. Failed deliveries retry with exponential backoff.

Request headers

X-BluPay-Event-IdUnique event id (e.g. evt_…).
X-BluPay-TimestampUnix time in milliseconds (string).
X-BluPay-SignatureHMAC-SHA256, 64-char lowercase hex.

event_type values

invoice.created · invoice.pending · invoice.paid · invoice.partially_paid · invoice.partially_expired · invoice.expired · invoice.canceled

invoice.paid means Paid under the configured payment/tolerance policy; do not require a zero remainder.

Example payload

{
  "event_id": "evt_3kf9…",
  "event_type": "invoice.paid",
  "created_at": "2026-06-09T12:10:33.000Z",
  "data": {
    "invoice_link": "AbCd1234_-xy",
    "status": "Paid",
    "order_id": "order-1024",
    "currency_amount": "49.00",
    "currency_symbol": "USD"
  }
}

Verify the signature (Node.js)

import crypto from 'node:crypto';

// persistEvent must atomically store the event in a durable database inbox
// with a UNIQUE event_id. Duplicate delivery must be a successful no-op.
// A worker processes that inbox and updates/fulfils each order only once.
export function createBluPayWebhookHandler({ secret, persistEvent }) {
  if (typeof secret !== 'string' || !secret || typeof persistEvent !== 'function') {
    throw new Error('Configure the shop signing secret and a durable inbox');
  }
  return async (req, res) => {
    const ts = req.header('X-BluPay-Timestamp');
    const sig = req.header('X-BluPay-Signature');
    const eventId = req.header('X-BluPay-Event-Id');
    const millis = Number(ts);
    if (typeof ts !== 'string' || !/^[0-9]{10,16}$/.test(ts) ||
        !Number.isSafeInteger(millis) || Math.abs(Date.now() - millis) > 300_000) {
      return res.status(400).end('stale');
    }
    if (!Buffer.isBuffer(req.body) || typeof sig !== 'string' ||
        !/^[a-f0-9]{64}$/i.test(sig)) {
      return res.status(400).end('bad-signature');
    }
    const expected = crypto.createHmac('sha256', secret)
      .update(ts, 'utf8').update(req.body).digest(); // exact bytes, no separator
    if (!crypto.timingSafeEqual(Buffer.from(sig, 'hex'), expected)) {
      return res.status(400).end('bad-signature');
    }
    let event;
    try { event = JSON.parse(req.body.toString('utf8')); }
    catch { return res.status(400).end('bad-json'); }
    if (!event || typeof event !== 'object' || Array.isArray(event) ||
        typeof event.event_id !== 'string' || !event.event_id ||
        event.event_id !== eventId || typeof event.event_type !== 'string') {
      return res.status(400).end('bad-event');
    }
    try {
      await persistEvent(event); // acknowledge only after the DB commit
      return res.status(200).end('ok');
    } catch {
      return res.status(503).end('retry');
    }
  };
}

// Express: register this route BEFORE app.use(express.json()).
// app.post('/blupay/webhook', express.raw({ type: 'application/json', limit: '1mb' }),
//   createBluPayWebhookHandler({ secret: process.env.BLUPAY_WEBHOOK_SECRET, persistEvent }));

Register the raw-body route before any JSON parser. Implement persistEvent with a durable inbox and a database UNIQUE event_id; return 200 for an already committed duplicate. Its worker must match data.invoice_link, order_id, currency_amount and currency_symbol against the saved order using decimal-safe amount comparisons before an atomic, once-only fulfilment for invoice.paid with status Paid. Extra payload fields vary by event; the example shows a subset. Versioned invoice events may also include schema_version and aggregate.revision; retain these for ordering. A retry keeps event_id but can have a fresh delivery timestamp and signature. Never deduplicate on the signature.

Need a key or stuck on something? Contact support with the invoice reference and a description of the issue.