# Overview Source: https://docs.bitsby.app/index ## About the service Bitsby is a payment service that makes it easy to accept and process payments in popular cryptocurrencies. With our crypto payment processing, you can add crypto payments to an online service, e-commerce store, online school, Telegram bot, or any other platform. :::info 💎 The key feature that sets Bitsby apart from thousands of other services: **all funds go straight to your own crypto wallets, while payment processing stays fully automated**. This brings a number of advantages: * **Low fees**: 0.5–1%, charged only on successful payments. No additional fees. * **Security**: no one has access to your crypto, and no one can freeze it. * **Speed**: payments arrive directly in your crypto wallets, and you can spend them right away. Fast turnover of your funds. * **No KYC**: no identity, company, or store verification. Sign up and start working right away. * **Automation**: the service processes all incoming payments, matches them to invoices, and sends all the necessary notifications. * **API**: manage invoices and payments from your own systems and stores. * **Convenience**: the service renders equally well on all platforms and browsers, and comes with a PWA app and a Telegram bot. ::: ## Supported cryptocurrencies * USDC on Solana, Ethereum (ERC-20), BNB Smart Chain (BEP-20), and Base * USDT on Solana, Toncoin, Tron (TRC-20), Ethereum (ERC-20), and BNB Smart Chain (BEP-20) * BTC * ETH * SOL * TON ## How it works 1. The merchant creates an invoice. They specify the amount, the invoice currency, and a description of the product or service. Supported fiat currencies: USD, EUR, RUB. 2. The service converts the fiat amount into USDC, USDT, BTC, ETH, TON, and SOL amounts. It locks these amounts in for the invoice. 3. The merchant gets a link to the payment form and sends it to the customer. 4. The customer pays the invoice by transferring the specified amount to any of the merchant's wallets. 5. The service automatically detects the incoming transaction and looks for an unpaid invoice. 6. If the service finds an unpaid invoice with a matching amount, the invoice moves to Paid. 7. The service sends a paid-invoice notification to email, to the Telegram bot, and to the Webhook URL (the merchant's store). 8. If the service does NOT find an unpaid invoice (for example, the customer transferred the wrong amount), the invoice is not confirmed automatically. The invoice requires manual handling. 9. The merchant can manually match an unmatched payment to an unpaid invoice. ## Demo ## Dashboard screenshots
Invoice payment page: amount due, status, countdown timer, cryptocurrency selection, wallet address, and network
Payment form
Add wallet form: name, cryptocurrency, and address fields
Adding your own crypto wallet
Project settings: wallets, email and Telegram notifications, customer return URLs, and the Webhook URL
Editing a project
Paid invoice card: payment time, crypto amount, wallet, and transaction hash
Paid invoice details
Invoice table: ID, project, amount, date, and status of each invoice
List of invoices
Table of connected wallets: name, address, and a delete button
List of crypto wallets
Invoice creation form: project, amount, currency, payment deadline, and description
Creating an invoice
Statistics section: payment totals, breakdown by project, period, and cryptocurrency, and a daily chart
Payment statistics
## Telegram bot screenshots
Telegram bot notifications about an incoming payment and a paid invoice
Payment and invoice notifications
Creating an invoice in the Telegram bot: invoice parameters, data review, and the payment link
Creating an invoice
## Getting started 1. [Create an account](https://dash.bitsby.app/). 2. [Add](https://dash.bitsby.app/wallets/list) your crypto wallets. 3. [Add a project](https://dash.bitsby.app/projects/list). 4. Create invoices—manually in the dashboard or the Telegram bot, or automatically via an HTML form or the API. 5. The customer lands on the payment page: with manual creation you send them the link yourself; with the HTML form and the API your site redirects them automatically. 6. For automatic order fulfillment, set the Webhook URL in the project settings—it receives a notification for every paid invoice. 7. [Track all invoices](https://dash.bitsby.app/invoices/list) in a convenient table. ## Fees * The service fee is 0.5–1%. It is charged only on successful payments. * The system sets the fee rate automatically based on the project's turnover over the last 30 days. * You can choose who pays the fee—the merchant or the customer. ## Invoice creation methods * Manually in the [dashboard](./creating-invoices/in-dashboard.md) or the PWA app on your phone * Manually in the [Telegram bot](./creating-invoices/in-telegram-bot.md) * Automatically via an [HTML form](./creating-invoices/html-forms/index.md) * Automatically via the [API](./api/index.md) ## Custom domain You can connect any domain you own and brand the service as yours. In that case, the service replaces all logos and links with your project's. See the dedicated [section](./custom-domain.md) for details. ## Technical support Telegram: [@hbsupp\_bot](https://t.me/hbsupp_bot?start=BGseWV9E) Email: [support@bitsby.app](mailto:support@bitsby.app) --- # Quick start Source: https://docs.bitsby.app/quick-start A minimal payment integration: creating an invoice via the API and handling the payment notification. The final section covers order fulfillment. The examples are self-contained; no SDK is used. :::tip **Working with an AI agent?** Give it the [markdown version of this page](/quick-start.md). It has enough detail to build an integration for your site or store. ::: ## Prerequisites | Value | Where to get it | | --- | --- | | API key | The [Integrations](https://dash.bitsby.app/integrations/list) section | | Project ID | The [Projects](https://dash.bitsby.app/projects/list) section | | Webhook secret | Project settings | In the same project settings, set the **Webhook URL**—the address of the notification handler on your server. HTTPS only; redirects are not followed. The examples use test values—replace them with your own. ## Creating an invoice `POST https://api.bitsby.app/invoices/create`, the API key is passed in the `Authorization` header. | Parameter | Required | Value | | --- | --- | --- | | `projectId` | yes | Project ID, UUID | | `amountFiat` | yes | Invoice amount | | `currencyFiat` | yes | Currency: USD, EUR, RUB | | `timeToPay` | yes | Payment deadline in hours: 0.5, 1, 3, 6, 12 | | `description` | no | Description, shown to the customer on the payment page | | `serviceData` | no | Service data, not shown to the customer. We recommend passing the order number: it comes back in the payment notification | **cURL** ```bash showLineNumbers curl -X POST https://api.bitsby.app/invoices/create \ -H "Authorization: Token MSvL2ltaDZdWVjmZURURMVWhqSJLT2NURjhL2Fla1Z1T1IxQTltKs1T3Ay" \ -F "projectId=9deea1e2-0c08-41a3-bdc2-a34eada3892d" \ -F "amountFiat=49.90" \ -F "currencyFiat=USD" \ -F "timeToPay=1" \ -F "description=Order 4172" \ -F "serviceData=order-4172" ``` **PHP** ```php showLineNumbers true, CURLOPT_TIMEOUT => 10, CURLOPT_HTTPHEADER => ['Authorization: Token '.$apiKey], CURLOPT_POSTFIELDS => http_build_query([ 'projectId' => $projectId, 'amountFiat' => 49.90, 'currencyFiat' => 'USD', 'timeToPay' => 1, 'description' => 'Order 4172', 'serviceData' => 'order-4172', // your order id ]), ]); $response = json_decode(curl_exec($ch), true); curl_close($ch); if ($response['result'] !== 'success') { exit('API error: '.$response['data']); } $invoice = $response['data']; // $invoice['id'] — invoice id, store it with the order // $invoice['url'] — payment page for the customer ``` **TypeScript** ```typescript showLineNumbers const API_KEY = 'MSvL2ltaDZdWVjmZURURMVWhqSJLT2NURjhL2Fla1Z1T1IxQTltKs1T3Ay'; const PROJECT_ID = '9deea1e2-0c08-41a3-bdc2-a34eada3892d'; interface Invoice { id: string; uid: string; url: string; timeToPayDatetime: string; } const response = await fetch('https://api.bitsby.app/invoices/create', { method: 'POST', headers: { Authorization: `Token ${API_KEY}` }, body: new URLSearchParams({ projectId: PROJECT_ID, amountFiat: '49.90', currencyFiat: 'USD', timeToPay: '1', description: 'Order 4172', serviceData: 'order-4172', // your order id }), signal: AbortSignal.timeout(10_000), }); const result: { result: string; data: Invoice | string } = await response.json(); if (result.result !== 'success') { throw new Error(`API error: ${result.data}`); } const invoice = result.data as Invoice; // invoice.id — invoice id, store it with the order // invoice.url — payment page for the customer ``` **JavaScript** ```js showLineNumbers const API_KEY = 'MSvL2ltaDZdWVjmZURURMVWhqSJLT2NURjhL2Fla1Z1T1IxQTltKs1T3Ay'; const PROJECT_ID = '9deea1e2-0c08-41a3-bdc2-a34eada3892d'; const response = await fetch('https://api.bitsby.app/invoices/create', { method: 'POST', headers: { Authorization: `Token ${API_KEY}` }, body: new URLSearchParams({ projectId: PROJECT_ID, amountFiat: '49.90', currencyFiat: 'USD', timeToPay: '1', description: 'Order 4172', serviceData: 'order-4172', // your order id }), signal: AbortSignal.timeout(10_000), }); const result = await response.json(); if (result.result !== 'success') { throw new Error(`API error: ${result.data}`); } const invoice = result.data; // invoice.id — invoice id, store it with the order // invoice.url — payment page for the customer ``` **Python** ```python showLineNumbers import requests API_KEY = 'MSvL2ltaDZdWVjmZURURMVWhqSJLT2NURjhL2Fla1Z1T1IxQTltKs1T3Ay' PROJECT_ID = '9deea1e2-0c08-41a3-bdc2-a34eada3892d' response = requests.post( 'https://api.bitsby.app/invoices/create', headers={'Authorization': f'Token {API_KEY}'}, data={ 'projectId': PROJECT_ID, 'amountFiat': '49.90', 'currencyFiat': 'USD', 'timeToPay': '1', 'description': 'Order 4172', 'serviceData': 'order-4172', # your order id }, timeout=10, ) result = response.json() if result['result'] != 'success': raise RuntimeError(f"API error: {result['data']}") invoice = result['data'] # invoice['id'] — invoice id, store it with the order # invoice['url'] — payment page for the customer ``` Response: ```json { "result": "success", "data": { "id": "ade9550d-3dc7-4fd3-b94e-3b4c12aaaa0c", "uid": "MXNj4m8HhcM4", "createDatetime": "2026-09-14 10:12:03", "timeToPayDatetime": "2026-09-14 11:12:03", "commissionFiatUSD": 0.5, "amountFiatUSD": 49.9, "url": "https://dash.bitsby.app/invoices/pay/MXNj4m8HhcM4" } } ``` Store `data.id` with the order and send the customer to `data.url`—the payment page. Deliver the link any way you like: a redirect, an email, a bot message. The invoice is valid until `timeToPayDatetime` (UTC). ## Handling the notification When an invoice moves to Paid, the service sends a POST request to the project's Webhook URL. The body is JSON; the signature is in the headers: | Header | Value | | --- | --- | | `X-Timestamp` | Send time, unix time in seconds | | `X-Signature` | `sha256=` + hex HMAC-SHA256 of the string `.` | The signing key is the webhook secret. The signature is computed over the raw request body, so verify it before parsing the JSON. **PHP** ```php showLineNumbers 300) { http_response_code(400); exit; } $expected = 'sha256='.hash_hmac('sha256', $timestamp.'.'.$body, $secret); if (!hash_equals($expected, $signature)) { http_response_code(403); exit; } $invoice = json_decode($body, true)['invoice']; if ($invoice['status'] === 'paid') { // $invoice['serviceData'] — the order id passed at creation: 'order-4172' // $invoice['amountFiat'] — the original invoice amount: 49.9 // Issue the order here, see the next section } http_response_code(200); ``` **TypeScript** ```typescript showLineNumbers import { createHmac, timingSafeEqual } from 'node:crypto'; import express, { type Request, type Response } from 'express'; const SECRET = 'k7QwR2mZ9tXbN4vL8sJpH3dF6yA1cE0u'; const app = express(); // express.raw: the signature is computed over the raw request body app.post('/webhook', express.raw({ type: 'application/json' }), (req: Request, res: Response) => { const timestamp = req.get('X-Timestamp') ?? ''; const signature = req.get('X-Signature') ?? ''; const body = (req.body as Buffer).toString('utf8'); // Reject replayed requests: allow up to 5 minutes of clock drift if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) { return res.sendStatus(400); } const expected = 'sha256=' + createHmac('sha256', SECRET) .update(`${timestamp}.${body}`, 'utf8') .digest('hex'); const a = Buffer.from(expected); const b = Buffer.from(signature); if (a.length !== b.length || !timingSafeEqual(a, b)) { return res.sendStatus(403); } const { invoice } = JSON.parse(body); if (invoice.status === 'paid') { // invoice.serviceData — the order id passed at creation: 'order-4172' // invoice.amountFiat — the original invoice amount: 49.9 // Issue the order here, see the next section } res.sendStatus(200); }); app.listen(8080); ``` **JavaScript** ```js showLineNumbers import { createHmac, timingSafeEqual } from 'node:crypto'; import express from 'express'; const SECRET = 'k7QwR2mZ9tXbN4vL8sJpH3dF6yA1cE0u'; const app = express(); // express.raw: the signature is computed over the raw request body app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const timestamp = req.get('X-Timestamp') ?? ''; const signature = req.get('X-Signature') ?? ''; const body = req.body.toString('utf8'); // Reject replayed requests: allow up to 5 minutes of clock drift if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) { return res.sendStatus(400); } const expected = 'sha256=' + createHmac('sha256', SECRET) .update(`${timestamp}.${body}`, 'utf8') .digest('hex'); const a = Buffer.from(expected); const b = Buffer.from(signature); if (a.length !== b.length || !timingSafeEqual(a, b)) { return res.sendStatus(403); } const { invoice } = JSON.parse(body); if (invoice.status === 'paid') { // invoice.serviceData — the order id passed at creation: 'order-4172' // invoice.amountFiat — the original invoice amount: 49.9 // Issue the order here, see the next section } res.sendStatus(200); }); app.listen(8080); ``` **Python** ```python showLineNumbers import hashlib import hmac import time from flask import Flask, request SECRET = 'k7QwR2mZ9tXbN4vL8sJpH3dF6yA1cE0u' app = Flask(__name__) @app.post('/webhook') def webhook(): timestamp = request.headers.get('X-Timestamp', '') signature = request.headers.get('X-Signature', '') # The signature is computed over the raw request body body = request.get_data(as_text=True) # Reject replayed requests: allow up to 5 minutes of clock drift if abs(time.time() - int(timestamp or 0)) > 300: return '', 400 expected = 'sha256=' + hmac.new( SECRET.encode(), f'{timestamp}.{body}'.encode(), hashlib.sha256, ).hexdigest() if not hmac.compare_digest(expected, signature): return '', 403 invoice = request.get_json()['invoice'] if invoice['status'] == 'paid': # invoice['serviceData'] — the order id passed at creation: 'order-4172' # invoice['amountFiat'] — the original invoice amount: 49.9 # Issue the order here, see the next section pass return '', 200 ``` Respond with a 2xx code within 10 seconds. Any other code, a redirect, or a timeout counts as a failed delivery: the service retries at increasing intervals from 5 minutes up to 24 hours, then stops. Offload long processing to a queue: respond with 200 first, then work on the order. The full notification format and field reference are in the [Webhook URL](./webhook-url/index.md) section. ## Order fulfillment A verified notification with status `paid` confirms the payment. Fulfillment steps: 1. **Find the order** by `invoice.serviceData`—the value passed at invoice creation (`order-4172`). 2. **Check by `invoice.id` whether this invoice has already been fulfilled.** A notification with the same `invoice.id` can arrive more than once—fulfill the order once and store a processed flag. 3. **Verify the amount and currency** against `invoice.amountFiat` and `invoice.currencyFiat`—these are the original invoice values and never change. `amountFiatUSD` is overwritten with the amount actually received. Compare amounts as numbers, not strings: trailing zeros are dropped, so `49.90` arrives as `49.9`. If the values don't match the order, route the invoice to manual review instead of fulfillment—such cases are covered in [Payment matching and amount mismatches](./payment-matching.md). 4. **Fulfill the order and mark it as processed.** Do not check the payment deadline at fulfillment: the payment may have confirmed on-chain after `timeToPayDatetime`, and the merchant can match a payment to an invoice manually. The notification itself confirms the payment. ## What's next - [API reference](./api/index.md)—invoice lists, cancellation, statistics, balances - [Webhook signature verification](./webhook-url/signature-verification.md)—the signature mechanics in detail - [Payment matching and amount mismatches](./payment-matching.md)—underpayment, overpayment, unmatched payments - [HTML forms](./creating-invoices/html-forms/index.md)—accepting payments without the API --- # FAQ Source: https://docs.bitsby.app/faq ## Getting started
Do I need to verify my project or documents? Absolutely not. Connect any project.
Do I need programming skills? You can create invoices manually and send the payment page link to your customers, then track payments and deliver the product or service by hand. You can also have a developer automate all of this in your store using our API and Webhook URL notifications. The simplest and fastest way to add payments to your site is to embed an HTML form.
Is there a plugin for my online store? We are building plugins for various e-commerce engines and CMS platforms. See [Plugins for online stores and CMS](./cms-plugins.md) for details.
How do I test the integration without spending real money? There is no separate test mode—you test the integration with regular payments, and the funds arrive in your own wallet. The only cost is the service fee, and it is deducted from the starter balance credited at signup: it covers several test payments.
## Wallets and security
Which wallets can I connect? We recommend non-custodial wallets such as MetaMask, Trust Wallet, Exodus, and similar. Hardware wallets such as Ledger also work well. You can also connect personal exchange deposit addresses. The service only rejects highly active addresses with a large number of transactions. All you need to connect a wallet is its address.
Do I need to give access to my wallets? No, the service needs no access to your wallets. No keys are required. Just provide the address of the wallet you will accept payments to.
How secure is it? The service has no access to your funds: payments go straight to your wallets, and we never ask for their keys. Dashboard sign-in is protected with two-factor authentication—via email or Telegram.
Can I use one wallet in several projects? Yes, you can use the same address across several of your projects—the service correctly separates payments between them. However, avoid using this address for unrelated transfers: if the amount matches, the system may treat such a transfer as an invoice payment.
## Accepting payments
How quickly does the service see a payment? The service polls wallets every few minutes. Beyond that, the speed depends on the network and the selected processing mode: in Fast mode the payment counts after the first confirmation, in Secure mode—after the transaction reaches the network's recommended confirmation count.
Does a rate change after the invoice is issued matter? The crypto amount is locked in at invoice creation—the customer pays exactly that amount no matter how much time passes. The invoice amount in USD and the service fee, however, are calculated at the moment of payment, at the rate current at that time.
What if the customer paid the wrong amount? The invoice will not be marked paid automatically—the service matches a payment to an invoice only on an exact amount match. The payment itself is stored in the system, and you can manually match it to the invoice in the dashboard or via the API. See [Payment matching and amount mismatches](./payment-matching.md) for details.
What if the customer sent funds on the wrong network? The service looks for payments only on the wallets involved in the invoice. A transfer on a network that is not among them will not be detected automatically, and the invoice will stay unpaid. Funds must be sent strictly on the network shown on the payment form.
Does the service issue refunds to customers? No. Funds go directly to your crypto wallet; the service does not control them and has no access to them. You issue refunds to the customer yourself, from your own wallet.
## Fees and balance
What are the fees? 0.5–1%, charged only on successful payments. No additional fees. We calculate and deduct the fee once a day from your account balance. We recalculate the fee rate weekly, based on the project's turnover over the last 30 days.
What happens if the account balance goes negative? Payment acceptance keeps working: already issued invoices get paid and closed as usual. If the balance is still negative by Monday, only the creation of new invoices is blocked. Once the debt is paid off, invoice creation is unblocked immediately.
Are there any turnover limits? You can accept any amount; there are no limits.
--- # Wallets Source: https://docs.bitsby.app/wallets --- # Choosing a wallet type Source: https://docs.bitsby.app/wallets/wallet-types When setting up the payment service, you add cryptocurrency addresses (wallets) on different networks that will receive payments from your customers. We recommend considering two main approaches, each with its own advantages and drawbacks. ## Non-custodial wallets Non-custodial (self-custody) wallets are wallets where only you control the private keys and therefore fully own your funds. Recommended for maximum security. **Advantages:** * Full control over your funds—no one but you has access to your assets * Maximum security—the funds stay under your control **Drawbacks:** * Stablecoin funds (USDT, USDC) received on different networks end up spread across the corresponding addresses on those networks * Consolidating assets on a single network requires cross-chain bridges or exchanges, which involves fees * Managing funds across several networks takes more attention **Popular non-custodial wallets:** 1. **MetaMask**—one of the most popular wallets, supports many networks 2. **Trust Wallet**—a mobile wallet supporting a wide range of blockchains 3. **Ledger**—a hardware wallet with the highest level of security 4. **Trezor**—a reliable hardware wallet 5. **Exodus**—a convenient desktop and mobile wallet 6. **Coinbase Wallet**—a non-custodial wallet from Coinbase (separate from the exchange) 7. **Phantom**—a popular wallet for Solana and Ethereum **When to use:** if the security of your funds and full control over your assets are your priority. ## Exchange deposit addresses The alternative is to use crypto exchange deposit addresses for each supported network. **Advantages:** * Automatic asset aggregation—exchanges group incoming USDT and USDC into a single balance regardless of the receiving network * Simplified withdrawals—you can withdraw to any convenient network in a single operation * Low fees when converting to other cryptocurrencies or fiat * No need for cross-chain bridges to move funds between networks **Drawbacks:** * Custodial storage—the exchange controls the funds, not you * Dependence on the exchange's security and reliability * Possible restrictions or delays on withdrawals * Your funds may be subject to AML screening **Crypto exchanges with permanent deposit addresses:** 1. **Binance**—the largest crypto exchange by trading volume 2. **Coinbase**—one of the most regulated and trusted exchanges 3. **Kraken**—a reliable exchange with high security standards 4. **Bybit**—a popular exchange with a broad feature set 5. **OKX**—a global exchange supporting many networks 6. **Huobi (HTX)**—a major Asian exchange 7. **Bitstamp**—one of the oldest and most regulated exchanges **When to use:** if you regularly convert crypto to fiat or other assets, and convenience matters more to you than full control. ## Recommendations For most cases, we recommend non-custodial wallets to keep the received funds as secure as possible. However, if your business model involves frequent crypto conversion or working with several networks at once, deposit addresses at a reliable exchange can be a justified trade-off between security and convenience. You can also combine both approaches: use exchange addresses for day-to-day operations and move accumulated funds to non-custodial wallets for long-term storage. ## Exclusive use of addresses Do not use wallet addresses added to the service for anything unrelated to payments through it. If these addresses receive outside transactions (personal transfers, payments from other projects, and so on), the system may mistakenly identify them as payments for issued invoices when the amounts match. This leads to incorrect payment processing and confusion in your records. You can, however, safely use the same address in several different projects within our service—the system correctly separates payments between your projects. --- # Processing mode Source: https://docs.bitsby.app/wallets/processing-mode Each wallet's settings include a **Processing mode**: **Fast** or **Secure**. This parameter defines the filtering criteria for incoming on-chain transactions. The selected mode determines how quickly the service processes your transaction, imports it, and matches it to an invoice. Keep in mind: once a transaction passes the selected mode's filter and enters the database, it is **instantly** matched to the corresponding invoice (if one is found) and moves it to Paid. ## Fast mode **How it works:** the system counts a transaction as soon as it appears on the network with the minimum number of confirmations (usually 1 block or the Confirmed status). **When to use:** * For selling low-cost goods and services (digital content, subscriptions) * For micropayments * When instant product delivery is critical for your business **Specifics:** there is a very small risk of a blockchain reorg, where a transaction included in the first block is reversed by the network a few seconds later. In **Fast** mode, the system may confirm such a payment before the network rejects it. ## Secure mode **How it works:** the system deliberately ignores an on-chain transaction until it reaches the community-recommended number of confirmations (finality). For Bitcoin, for example, this may be 3 blocks. **When to use:** * For selling expensive goods and services * For topping up large user balances * For physical goods whose delivery costs money and cannot be recalled * For any operation where the financial risk of a reversed payment outweighs the inconvenience of waiting **Specifics:** the customer does not see the payment confirmed on your site right away—it appears a few minutes later (depending on the network), once the transaction is considered safe from the blockchain's perspective. --- # Sorting and display Source: https://docs.bitsby.app/wallets/sorting To set a specific display order for wallets on the payment page, use a numeric index in the wallet names. The service sorts wallets by name, so it displays them in ascending order of the numeric index.
List of wallets in the dashboard: names start with a numeric index from 1 to 8 Payment page: cryptocurrency buttons follow the same order as the indexes in the wallet names
--- # Creating invoices Source: https://docs.bitsby.app/creating-invoices --- # In the dashboard Source: https://docs.bitsby.app/creating-invoices/in-dashboard A simple and fast way to create an invoice in the browser via the dashboard. Once the invoice is created, you get a payment page link you can send to the customer right away.
![Invoice creation form in the dashboard: project, amount, payment deadline, description, and service data](/img/docs/hRybVWn7pN3znHWlCJWb-image.webp) ![A created invoice: the payment page link and the amounts due in each cryptocurrency](/img/docs/kyDONzVvwV6yGOFqqxJA-2025-02-18_15-43-41.webp)
--- # In the Telegram bot Source: https://docs.bitsby.app/creating-invoices/in-telegram-bot A simple and fast way to create an invoice in the Telegram bot. Once the invoice is created, you get a payment page link you can send to the customer right away. ## How to connect
![Profile menu in the dashboard: the settings item where Telegram is connected](/img/docs/t5xxBrqh3yhUHEtyVH3R-image.webp) ![Settings section: the Telegram card with a connect button, next to the PWA app and the API](/img/docs/TaNs3uwt0opa3v6pufRn-2024-09-09_16-00-46.webp)
## How to create an invoice * Select the Create invoice command. * Fill in all required fields marked with an asterisk. * Confirm that the data is correct. * Forward the last message with the invoice link to your customer. Invoice creation dialog in the Telegram bot: the bot asks for the project, amount, and payment deadline, then returns a link --- # HTML forms Source: https://docs.bitsby.app/creating-invoices/html-forms One way to add crypto payments to a site, an online store, or a bot. The customer clicks a button, and the payment page with the invoice opens. The form is submitted only via POST to `https://dash.bitsby.app/invoices/form` and contains two fields: | Field | What it holds | | ----------- | --------------------------------------------- | | `data` | base64url of the JSON with the invoice fields | | `signature` | HMAC-SHA256 of the `data` string, 64 hex characters | A project can have at most 50 unpaid invoices created this way at any moment. ## Form example ```html
``` The store's server fills in both values when rendering the page. The secret never appears in the markup. The `target="_blank"` attribute is optional: with it, the cart stays open in the original tab. ## How the container is built 1. Build a JSON object with the invoice fields. 2. Encode it as base64url. This is the `data` string. 3. Compute `signature = HMAC-SHA256(data, formSecret)`. The `data` string is signed as a whole, so the JSON key order, indentation, and Unicode escaping style do not matter. The receiver also accepts standard base64, with or without padding. Do not rebuild `data` after signing: if even one byte changes, the signature must be computed again. How to compute the signature, with ready-made examples in four languages, is covered in [HTML form signature](./form-signature.md). ## Keys inside data | Key | Required | What it holds | | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `projectId` | yes | Project ID from the settings in the dashboard, uuid | | `amountFiat` | yes | Amount from 1 to 100000, at most two decimal places. As a string `"10.50"` or a number `10.5` | | `currencyFiat` | yes | Fiat currency: USD, EUR, or RUB | | `timeToPay` | yes | Payment deadline in hours: 0.5, 1, 3, 6, 12. As a string or a number | | `description` | no | Description for the customer, up to 1000 characters | | `serviceData` | no | Order identifier on the store side, up to 1000 characters. Not shown to the customer but visible in the form's source code—do not put anything sensitive here | | `output` | no | `errors`—show the details of validation errors | An optional key can be omitted from the JSON—that is the same as an empty string. Keys can go in any order; the receiver ignores unknown keys. Values are JSON strings or numbers. Booleans, arrays, and nested objects do not count as field values and arrive as an empty string. Example of the `data` contents: ```json { "projectId": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "amountFiat": 10.5, "currencyFiat": "USD", "timeToPay": 1, "description": "Order #7, delivery", "serviceData": "order-7" } ``` Here the amount and payment deadline are passed as numbers; strings are accepted too. The optional `output` is not set at all. ## Amount format Digits and a decimal point only, at most two decimal places: `10`, `10.00`, `1000.50`. Surrounding spaces are trimmed. Any other format is rejected—thousands separators, a comma instead of a point, exponential notation, a sign before the number, a currency symbol. There is no rounding: a third decimal place is not truncated, it causes a rejection. ## Form secret The secret lives in the project settings in the dashboard, in the Form secret field. The server issues it when the project is created. You cannot set your own value; the secret can only be reissued—for example, if it is compromised. The secret must stay on the store's server. If it ends up in the storefront HTML, the signature becomes pointless. The form secret and the webhook secret are different keys; do not mix them up. ## Order identifier Always fill in `serviceData`: put your own order identifier there. The value comes back in the [payment notification](../../webhook-url/index.md)—the store uses it to find the order. A filled-in `serviceData` protects against duplicates. Resubmitting the form or double-clicking the button does not create a second invoice: the customer is sent to the existing unpaid one. The match is made on the order fields—project, amount, currency, description, and `serviceData`—not on the bytes of `data`. The value must be unique per order. With the same value, a second customer lands on the first customer's unpaid invoice. An empty `serviceData` removes this protection: there is nothing to recognize a repeat by, and every submit creates a new invoice. Duplicates use up the limit of 50 unpaid invoices, and when one is paid, the store has nothing to tie the payment to an order with. ## Errors and debug mode The form goes through two checks: first the signature and the container, then the field values. | What happened | What the service shows | | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------- | | The signature does not match, the container is unreadable, the project belongs to someone else or does not exist | A generic error, no reason given | | The signature matches, but a field is filled in incorrectly | A generic error. With `"output":"errors"`—detailed errors | | Both checks passed | The payment page with the invoice | The first check never reveals the reason, under any settings. The `"output":"errors"` pair in the JSON enables details for the second check: the service names the field, lists the allowed values, and reports when the ceiling of 50 unpaid invoices is reached. The key lives inside the signed container, so it cannot be injected from outside. While setting up the form, put `"output":"errors"` in the JSON; once set up, remove the key and rebuild the container. If the signature itself does not match, compare your `data` and signature against the reference set in [HTML form signature](./form-signature.md). ## What's next An invoice created by the form is processed the same way as one from the API or the dashboard: * **Payment confirmation** arrives at the [Webhook URL](../../webhook-url/index.md). Verify the [notification signature](../../webhook-url/signature-verification.md) with the webhook secret and fulfill orders only based on it. * **Returning the customer to your site** is configured via the Successful URL and Unsuccessful URL—see the “Returning the customer to the store site” section on the [Invoice lifecycle](../../invoice-lifecycle.md) page. Landing on these URLs does not confirm payment. * **Invoice statuses and the payment search window** are described in [Invoice lifecycle](../../invoice-lifecycle.md). * **The customer transferred the wrong amount**—see [Payment matching and amount mismatches](../../payment-matching.md). --- # HTML form signature Source: https://docs.bitsby.app/creating-invoices/html-forms/form-signature The customer can change any form field right in the browser—for example, set a lower amount. That is why the invoice fields travel in a signed container: the service recomputes the signature and, if it does not match, does not create the invoice. The store's server computes the signature. Only the finished `data` and `signature` pair goes to the browser. ## What is signed ``` data = base64url(json) signature = HMAC-SHA256(data, formSecret) ``` The HMAC is computed over the `data` string, not over the JSON bytes. The receiver verifies the signature against the submitted string as is, and only then decodes it. It follows that there is no canonicalization: key order, JSON indentation, and Unicode escaping style do not affect the signature. Any standard JSON encoder works. ## Reference set Plug these values into your code and compare the output. The example secret: ``` dvc3khto5WpjEPojZfYJsbJzFEPPVnRa ``` The amount and payment deadline as numbers; strings are accepted too: ```json {"projectId":"a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d","amountFiat":10.5,"currencyFiat":"USD","timeToPay":1,"description":"Order #7, delivery","serviceData":"order-7"} ``` The `data` string is base64url of those bytes. The padding is stripped here; the receiver also accepts it with padding: ``` eyJwcm9qZWN0SWQiOiJhMWIyYzNkNC01ZTZmLTRhN2ItOGM5ZC0wZTFmMmEzYjRjNWQiLCJhbW91bnRGaWF0IjoxMC41LCJjdXJyZW5jeUZpYXQiOiJVU0QiLCJ0aW1lVG9QYXkiOjEsImRlc2NyaXB0aW9uIjoiT3JkZXIgIzcsIGRlbGl2ZXJ5Iiwic2VydmljZURhdGEiOiJvcmRlci03In0 ``` The signature of this string with the secret above: ``` 09628020c47400c9231420450af642adfdb3c246f33e3e12cb8e28f96c2a785e ``` With these values, the examples below print exactly this `data` and signature. The same JSON with the `"output":"errors"` key at the end produces a different `data` string and a different signature. If `data` does not match while the JSON looks the same, the JSON encoder is the cause: a different key order or spaces after separators. The receiver will accept such a container, but it will not match the reference verbatim. ## Examples **PHP** ```php showLineNumbers $data, 'signature' => hash_hmac('sha256', $data, $formSecret), ]; } // Test values from the reference set. Before going live, // replace the secret with the form secret from your project settings $container = signedContainer([ 'projectId' => 'a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d', 'amountFiat' => 10.5, 'currencyFiat' => 'USD', 'timeToPay' => 1, 'description' => 'Order #7, delivery', 'serviceData' => 'order-7', ], 'dvc3khto5WpjEPojZfYJsbJzFEPPVnRa'); echo $container['data'], "\n"; echo $container['signature'], "\n"; ``` **TypeScript** ```typescript showLineNumbers import { createHmac } from 'node:crypto'; // Values are JSON strings or numbers, optional keys may be omitted export interface FormFields { projectId: string; amountFiat: string | number; currencyFiat: string; timeToPay: string | number; description?: string; serviceData?: string; output?: string; } export interface FormContainer { data: string; signature: string; } export const signedContainer = (fields: FormFields, formSecret: string): FormContainer => { const data = Buffer.from(JSON.stringify(fields), 'utf8').toString('base64url'); return { data, signature: createHmac('sha256', formSecret).update(data, 'utf8').digest('hex'), }; }; // Test values from the reference set. Before going live, // replace the secret with the form secret from your project settings const container = signedContainer({ projectId: 'a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d', amountFiat: 10.5, currencyFiat: 'USD', timeToPay: 1, description: 'Order #7, delivery', serviceData: 'order-7', }, 'dvc3khto5WpjEPojZfYJsbJzFEPPVnRa'); console.log(container.data); console.log(container.signature); ``` **JavaScript** This is code for the store's server, not the browser: the signature is computed where the secret is stored. ```js showLineNumbers import { createHmac } from 'node:crypto'; const signedContainer = (fields, formSecret) => { const data = Buffer.from(JSON.stringify(fields), 'utf8').toString('base64url'); return { data, signature: createHmac('sha256', formSecret).update(data, 'utf8').digest('hex'), }; }; // Test values from the reference set. Before going live, // replace the secret with the form secret from your project settings const container = signedContainer({ projectId: 'a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d', amountFiat: 10.5, currencyFiat: 'USD', timeToPay: 1, description: 'Order #7, delivery', serviceData: 'order-7', }, 'dvc3khto5WpjEPojZfYJsbJzFEPPVnRa'); console.log(container.data); console.log(container.signature); ``` **Python** ```python showLineNumbers import base64 import hashlib import hmac import json def signed_container(fields: dict, form_secret: str) -> dict: # compact separators: the default ones add spaces after ',' and ':' body = json.dumps(fields, separators=(',', ':')) data = base64.urlsafe_b64encode(body.encode('utf-8')).decode().rstrip('=') return { 'data': data, 'signature': hmac.new( form_secret.encode('utf-8'), data.encode('utf-8'), hashlib.sha256 ).hexdigest(), } # Test values from the reference set. Before going live, # replace the secret with the form secret from your project settings container = signed_container({ 'projectId': 'a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d', 'amountFiat': 10.5, 'currencyFiat': 'USD', 'timeToPay': 1, 'description': 'Order #7, delivery', 'serviceData': 'order-7', }, 'dvc3khto5WpjEPojZfYJsbJzFEPPVnRa') print(container['data']) print(container['signature']) ``` The `separators` argument is needed for a verbatim match with the reference. The receiver is fine with default JSON settings too. The example values are ASCII, but the code works with any Unicode unchanged. PHP and Python escape non-ASCII as `\uXXXX` by default, JavaScript sends raw UTF-8—the receiver accepts both. ## Two common mistakes **The JSON is signed instead of the container.** The HMAC is computed over the `data` string, that is, over the base64url. The mistake looks like “everything matches, but the signature does not.” **The container is rebuilt after signing.** If even one byte of `data` changes, the signature is invalid. Build the pair in one place in your code. ## If the signature does not match Walk the chain JSON → `data` → signature; the first link that diverges from the reference is the cause. Check `data` first: it does not depend on the secret. --- # Invoice lifecycle Source: https://docs.bitsby.app/invoice-lifecycle An invoice travels from creation to being closed or expiring. This section describes the states it can be in, how it moves between them, and what the merchant can do at each step. ## Invoice statuses | Status | API value | Description | Included in payment search | | -------- | ---------- | ------------------------------------------------------------------------------ | ------------------------------------ | | Unpaid | `unpaid` | The invoice is issued, the payment deadline has not passed, no payment received | Yes | | Paid | `paid` | A payment is found and matched to the invoice | No | | Expired | `expired` | The payment deadline has passed, no payment received | Yes, for one more hour after expiry | | Canceled | `canceled` | The invoice is canceled by the merchant manually or via the API | No | ## Status transitions * **Invoice creation.** The invoice immediately gets the Unpaid status. * **Unpaid → Paid.** A payment for the exact invoice amount arrived, or the merchant matched a payment manually. * **Unpaid → Expired.** The payment deadline passed with no payment received. * **Unpaid → Canceled.** The merchant canceled the invoice. * **Expired → Paid.** The payment arrived within an hour after expiry and was found automatically, or the merchant matched it manually. * **Canceled.** A final state; there is no way back. An invoice in the Paid status does not change state either—it cannot be paid again or canceled. ## Payment deadline The deadline is set at invoice creation and ranges from 30 minutes to 12 hours. Allowed values: 30 minutes, 1 hour, 3 hours, 6 hours, 12 hours. Until the deadline, the customer sees the payment form and can pay the invoice. After expiry, the invoice moves to Expired and the payment form no longer offers payment. Both extremes come at a price. A short deadline is risky on slow networks—the customer may not make it in time. A long one widens the gap between the rate at invoice creation and the rate at payment. ## Payment search window The payment search continues for one more hour after the payment deadline—to account for slow networks, where a transaction may confirm after the invoice has formally expired. All this time the customer sees the invoice as expired, but when a payment for the right amount arrives, it automatically moves to Paid with all the notifications. Hence the rule for integrators: do not reject a payment notification because the deadline has passed—that check cuts off a share of real payments. ## Canceling an invoice You can cancel an invoice if it contains a mistake. Cancellation requires two conditions: * The invoice is not paid—that is, it is in the Unpaid status * No one has opened the invoice's payment page; the view counter is zero The second condition protects against canceling an invoice the customer has already seen and possibly started paying. Cancellation is irreversible. A canceled invoice is excluded from the payment search, and a payment cannot be matched to it even manually. If payment is likely, wait for the deadline to pass instead of canceling—an expired invoice can still be closed with a payment. ## What is locked at creation and what is recalculated at payment At invoice creation, the service converts the fiat amount into crypto at the current rate and locks the resulting amounts in for the invoice. These are the amounts the customer sees, and these are what the service looks for on-chain. The rate no longer affects them: no matter how much time passes, the customer pays exactly the locked-in amount. At the moment of payment, the accounting amounts are recalculated: | Field | What happens | | ------------------- | -------------------------------------------------------------------------------------------------- | | `amountFiat` | The original invoice amount in fiat currency. Never changes | | `amountFiatUSD` | Overwritten with the amount actually received, converted to USD at the rate at the moment of payment | | `commissionFiatUSD` | Recalculated from the new `amountFiatUSD` value at the project's fee rate | Because of rate movement, these values can differ from the original ones even when the payment is exact. To reconcile with the order in your store, use `amountFiat`—the only amount that stays unchanged. ## Notifications along the invoice's life Notifications are configured per project. An invoice has two of them: * **Incoming payment.** Sent to email and Telegram when any incoming transaction is detected on your wallet, whether or not it matched an invoice. * **Invoice paid.** Sent to email, Telegram, and the Webhook URL the moment the invoice moves to Paid—on both automatic and manual matching. This gives a simple diagnostic rule: if an incoming payment notification arrived but no paid-invoice notification followed, the payment did not match because of an amount mismatch and needs manual handling. ## Returning the customer to the store site In the project settings, you can set two URLs the customer returns to from the payment page: * **Successful URL**—automatic redirect after the invoice is paid * **Unsuccessful URL**—automatic redirect when an expired or canceled invoice is opened Each URL is enabled separately. If a URL is not set, the customer stays on the payment page and sees the invoice status. The `uid` parameter—the invoice identifier for the customer—is appended to the URL: `https://example.com/order/success?uid=AFhygKX21ecd`. The store uses it to find the order and show the customer its own page. Landing on these URLs does not confirm payment—the customer can open the link manually. Fulfill orders based on the [webhook](./webhook-url/index.md) or after [checking the invoice status](./api/index.md) via the API. --- # Payment matching and amount mismatches Source: https://docs.bitsby.app/payment-matching This section describes the rule the service uses to match an incoming on-chain transaction to an issued invoice, what happens when the amount differs, and what the merchant can do in that case. Invoice statuses, deadlines, and state transitions are described in [Invoice lifecycle](./invoice-lifecycle.md). ## The amount match rule At invoice creation, the service converts the fiat amount into crypto and locks the resulting amounts in for the invoice. They become the payment's identifier: the customer sees them on the payment form, and the service looks on-chain for a transaction of exactly that amount. **The payment amount must match the invoice amount exactly. There is no tolerance.** The amount due is formed with 2 decimal places for the stablecoins USDT and USDC and 8 decimal places for other cryptocurrencies, and the incoming payment is compared against it precisely. Any deviation—underpayment or overpayment, however small—means no automatic matching will happen. **Example.** An invoice for 100.00 USD is issued with a locked-in amount of 100.12 USDT. | Incoming payment amount | Result | | ----------------------- | ---------------------------------------- | | 100.12 USDT | The invoice automatically moves to Paid | | 100.11 USDT | No automatic matching | | 100.50 USDT | No automatic matching | If several invoices for the same fiat amount are issued to one address at the same time, the service assigns each a slightly different crypto amount. Uniqueness is checked across all your unpaid invoices on that wallet, so one address can safely be used in several of your projects—the amounts will not collide. The most common causes of a mismatch are the customer rounding the amount by hand or the network fee being deducted on a withdrawal from an exchange. Warn the customer to transfer exactly the amount shown on the payment form. ## What happens on an amount mismatch The service records that the funds arrived but takes no action on the invoice: * **The invoice does not change status.** It stays unpaid until the payment deadline. The customer can still close it with the correct amount while the search window is open. * **No webhook is sent.** The notification to the Webhook URL goes out only at the moment the invoice moves to Paid. * **Partial payment is not tracked.** The service does not recognize underpayment and does not keep a balance due on the invoice. * **Payments are not added up.** If the customer follows an underpayment with a second transaction for the missing difference, the two payments are not summed. Both remain separate unmatched payments. * **Overpayment is not refunded automatically.** The funds go directly to your wallet; refunding the difference to the customer is handled outside the service. There are no intermediate statuses like Partially paid or Overpaid in the service. ## How the merchant learns about the problem If notifications are enabled in the project settings, the merchant gets an email and a Telegram bot message about every incoming payment—whether or not it matched an invoice. This gives a simple diagnostic rule: if an incoming payment notification arrived but no paid-invoice notification followed, the payment did not match because of an amount mismatch and needs manual handling. The rule works only within the search window. The service monitors wallets only while at least one invoice still has an active search. If the customer pays when no such invoices exist, the payment never enters the system at all—no notification, and no way to match it manually. You can also find unmatched payments programmatically. The `payments/list` method returns payments across all your wallets, and a payment not matched to an invoice has no `invoice` block in the response. These are the candidates for manual matching: compare the amount and time with the expected invoice and call `invoices/bindPayment`. ## Manual payment matching If you are certain which payment corresponds to which invoice, match them manually—in the dashboard on the invoice page or in the payment list, or via the API with the `invoices/bindPayment` method. ### Matching conditions Matching is possible only when all of the conditions hold at once: 1. **The invoice is Unpaid or Expired.** Canceled and already paid invoices cannot be matched. 2. **The payment is not yet matched to another invoice.** One payment can be tied to only one invoice. 3. **The payment arrived at a wallet involved in this invoice.** 4. **The payment falls within the available payment window**—24 hours before and 24 hours after invoice creation. The window is wider than the invoice's validity, which opens two possibilities: you can match a payment to an already expired invoice, and you can match an invoice to a payment that arrived before the invoice was created. The latter is handy when the customer sent money on their own initiative and the invoice was issued after the funds arrived. ### What happens after matching * **The invoice moves to Paid**—just as with automatic matching. * **The webhook is always sent.** The notification goes out on every transition to Paid, regardless of how the match was made. * **The invoice amount in USD is recalculated from the actual payment.** The `amountFiatUSD` field is overwritten with the amount actually received, at the rate at the moment of matching. The original amount in `amountFiat` does not change. * **The fee is recalculated from the actual amount.** It is charged at the project's fee rate on the amount you received. --- # Webhook URL Source: https://docs.bitsby.app/webhook-url ## Overview This feature delivers the data of a paid invoice to the merchant's server. It exists so the merchant's store can process the payment automatically and deliver the product or service to your customer. The Webhook URL is set individually for each project and points to the payment handler scripts inside the merchant's store. The address must use HTTPS and a domain name: IP addresses are not accepted, and the certificate is verified on every delivery. Every time an invoice's status changes to Paid, the service sends a POST request to this URL in the following format: ```json { "wallet":{ "id":"47aa71e2-07a0-482e-9172-7114d7376ba0", "name":"usdt-tron", "blockchain":"tron", "cryptocurrency":"usdt", "address":"TKbstUwMzLrfTAGL4erYb7gc7ghmHQ9zG7" }, "project":{ "id":"9deea1e2-0c08-41a3-bdc2-a34eada3892d", "name":"My project", "commissionPayer":"seller", "commissionRate":1 }, "invoice":{ "id":"a4c9e2ee-9a03-43e5-a1a1-00caf679d16a", "uid":"AFhygKX21ecd", "createDatetime":"2024-02-26 13:29:24", "timeToPayDatetime":"2024-02-27 01:29:24", "commissionFiatUSD":0.05, "amountFiatUSD":5.02, "amountFiat":5, "calcAmountFiat":5.02, "currencyFiat":"USD", "description":null, "serviceData":null, "status":"paid" }, "payment":{ "id":"f986ad8d-2298-473d-982a-efbc817b975d", "amount":5.02, "hash":"74763b65e43bcc9492a6ce9a7f26fbfdbd7635aecd3454420b5e9534cba50ee6", "transactionDatetime":"2024-02-26 13:32:57" } } ``` The notification is sent on every transition to Paid—both when the service found the payment automatically and when the merchant matched the payment to the invoice manually. ## Request parameters | Parameter | Description | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `wallet.id` | Wallet ID in UUID format | | `wallet.name` | Wallet name | | `wallet.blockchain` | The crypto wallet's blockchain | | `wallet.cryptocurrency` | The wallet's cryptocurrency | | `wallet.address` | The crypto wallet's address | | `project.id` | Project ID in UUID format | | `project.name` | Project name | | `project.commissionPayer` | Who pays the service fee | | `project.commissionRate` | Fee rate in % | | `invoice.id` | Invoice ID in UUID format | | `invoice.uid` | Invoice ID (for the customer) | | `invoice.createDatetime` | Invoice creation date and time (UTC) | | `invoice.timeToPayDatetime` | Date and time until which the invoice is valid for the customer (UTC). The payment search continues for 1 more hour after this mark—to account for slow networks. So a notification may arrive for an invoice that was already shown as expired | | `invoice.commissionFiatUSD` | Service fee amount. Calculated from the `invoice.amountFiatUSD` amount | | `invoice.amountFiatUSD` | Amount in USD. At invoice creation it is calculated from `invoice.amountFiat` at the current exchange rate. **At the moment of payment it is overwritten with the amount actually received**, converted to USD at the rate at that moment. The fee in `invoice.commissionFiatUSD` is recalculated from it as well | | `invoice.amountFiat` | The original invoice amount in fiat currency. Does not change | | `invoice.calcAmountFiat` | The calculated amount in the `invoice.currencyFiat` currency at the crypto exchange rate at the moment of payment. It can differ from `invoice.amountFiat` because the customer may have paid the invoice some time after creation rather than right away. Over that time, the crypto rate against `invoice.currencyFiat` could move either way | | `invoice.currencyFiat` | Fiat currency | | `invoice.description` | The description set at invoice creation | | `invoice.serviceData` | The service data set at invoice creation | | `invoice.status` | Invoice status | | `payment.id` | Payment ID in UUID format | | `payment.amount` | Payment amount in cryptocurrency | | `payment.hash` | The on-chain transaction hash | | `payment.transactionDatetime` | Date and time of the on-chain transaction (UTC) | ## Notification signature Every request is signed with the webhook secret. The signature travels in the `X-Timestamp` and `X-Signature` headers and lets you make sure the notification came from the service, not from an outsider who learned your handler's address. Verify the signature before processing the order. The mechanics and ready-made examples are in [Webhook signature verification](./signature-verification.md). ## Handling recommendations **Do not reject a notification because the payment deadline has passed.** A check like “the invoice is expired, so the payment is invalid” looks logical but cuts off a share of real payments: on slow networks a transaction may confirm after the deadline, and the merchant can manually match a payment to an expired invoice. The notification itself confirms the payment. **Verify the amount against the original field** `invoice.amountFiat`. This is the invoice amount as issued, and it does not change. The `amountFiatUSD` field reflects the amount actually received and can differ from the issued one—both because of rate movement and because of manual payment matching. **Parse amounts as numbers.** Trailing zeros are dropped: an amount of 10.00 arrives as 10; format it on your side for display. Amounts below 0.0001 arrive in exponential notation, for example 1.0e-6—standard JSON parsing returns the correct number; only manual string parsing breaks. **Handle notifications idempotently.** The same notification can arrive again—for example, if your script processed the payment successfully but returned a non-2xx code. Before fulfilling the order, check whether this `invoice.id` has already been processed. **Escape values on output.** The `invoice.description` and `invoice.serviceData` fields come back exactly as the merchant submitted them. If you render them in HTML, escape them on your side. ## Delivery schedule The server at the Webhook URL must respond with a 2xx HTTP code. Any other code, a timeout, or a dropped connection counts as a failed delivery. Redirects are not followed: a 301 or 302 response is a failed delivery, not a hop to the new address. Set the handler's final address. The connection is allowed 5 seconds, the whole request 10 seconds. If the handler does not fit within that, the delivery counts as failed. After a failed delivery, the service retries on the following schedule: * 5 minutes after the last failed delivery * After 15 minutes * After 30 minutes * After 1 hour * After 3 hours * After 6 hours * After 12 hours * After 24 hours After that, delivery attempts stop. ## Disabling the Webhook URL Sometimes a store's webhook script processes the payment correctly but returns a non-2xx HTTP code. This leads to frequent retries from our server on the schedule above, putting extra load on both our server and yours. To prevent such cases, we have a mechanism that disables the Webhook URL in a project. To avoid it, take the following steps: 1. Change your webhook handler code so it returns a 2xx code, usually 200, on successful payment processing. Test it with any emulator, for example Postman. 2. Contact technical support to correct the settings. 3. Re-enable the Webhook URL in the project settings and save the project. --- # Webhook signature verification Source: https://docs.bitsby.app/webhook-url/signature-verification The payment notification arrives at an address only your store and our service know. But the address can leak—from logs, from configuration, from development history. To tell a genuine notification from a forgery, every request is signed with the webhook secret. Verify the signature before fulfilling the order or changing its status. ## Signature headers | Header | Value | | ------------- | ------------------------------------------------ | | `X-Timestamp` | Send time, unix time in seconds | | `X-Signature` | The `sha256=` prefix and the HMAC-SHA256 in hex | The signed string is the send time, a dot, and the request body: ``` 1756901234.{"wallet":{...},"project":{...},"invoice":{...},"payment":{...}} ``` The signing key is the Webhook secret field in the project settings. You can reissue it there if the value is compromised. ## Verification steps 1. Take the raw request body, before parsing the JSON. 2. Concatenate the `X-Timestamp` value, a dot, and the body. 3. Compute the HMAC-SHA256 with the webhook secret. 4. Compare the result with `X-Signature` using a constant-time comparison. 5. Reject the request if `X-Timestamp` deviates too far from the current time. ## Implementation examples **PHP** ```php showLineNumbers 300) { http_response_code(400); exit; } $expected = 'sha256='.hash_hmac('sha256', $timestamp.'.'.$body, $secret); // hash_equals compares in constant time if (!hash_equals($expected, $signature)) { http_response_code(403); exit; } $data = json_decode($body, true); // Handle the order http_response_code(200); ``` **TypeScript** ```typescript showLineNumbers import { createHmac, timingSafeEqual } from 'node:crypto'; import express, { type Request, type Response } from 'express'; const app = express(); const secret = 'webhook secret from your project settings'; // The body must stay raw, so express.raw instead of express.json app.post('/webhook', express.raw({ type: 'application/json' }), (req: Request, res: Response) => { const timestamp = req.get('X-Timestamp') ?? ''; const signature = req.get('X-Signature') ?? ''; const body = (req.body as Buffer).toString('utf8'); if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) { return res.sendStatus(400); } const expected = 'sha256=' + createHmac('sha256', secret) .update(`${timestamp}.${body}`, 'utf8') .digest('hex'); const a = Buffer.from(expected); const b = Buffer.from(signature); // Check the length first: timingSafeEqual needs buffers of equal size if (a.length !== b.length || !timingSafeEqual(a, b)) { return res.sendStatus(403); } const data = JSON.parse(body); // Handle the order res.sendStatus(200); }); ``` **JavaScript** The same handler without types—for a plain JavaScript project. ```js showLineNumbers import { createHmac, timingSafeEqual } from 'node:crypto'; import express from 'express'; const app = express(); const secret = 'webhook secret from your project settings'; // The body must stay raw, so express.raw instead of express.json app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const timestamp = req.get('X-Timestamp') ?? ''; const signature = req.get('X-Signature') ?? ''; const body = req.body.toString('utf8'); if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) { return res.sendStatus(400); } const expected = 'sha256=' + createHmac('sha256', secret) .update(`${timestamp}.${body}`, 'utf8') .digest('hex'); const a = Buffer.from(expected); const b = Buffer.from(signature); // Check the length first: timingSafeEqual needs buffers of equal size if (a.length !== b.length || !timingSafeEqual(a, b)) { return res.sendStatus(403); } const data = JSON.parse(body); // Handle the order res.sendStatus(200); }); ``` **Python** ```python showLineNumbers import hmac import hashlib import time from flask import Flask, request app = Flask(__name__) secret = 'webhook secret from your project settings' @app.post('/webhook') def webhook(): timestamp = request.headers.get('X-Timestamp', '') signature = request.headers.get('X-Signature', '') body = request.get_data(as_text=True) if abs(time.time() - int(timestamp or 0)) > 300: return '', 400 expected = 'sha256=' + hmac.new( secret.encode(), f'{timestamp}.{body}'.encode(), hashlib.sha256, ).hexdigest() if not hmac.compare_digest(expected, signature): return '', 403 data = request.get_json() # Handle the order return '', 200 ``` ## Things to keep in mind **The raw body is signed.** If you parse the JSON and rebuild it, the key order and spacing change—the signature will not match. Compute the HMAC before parsing. **Every retry has its own signature.** The send time is new on each retry, so the signature differs too. Do not store a signature to compare against future requests. **The secret can be reissued.** The old secret stops working immediately after reissue, so update the value in your store at the same time. **Signature verification does not replace idempotency.** The signature proves the request is authentic, not that you are seeing it for the first time. A retry arrives with a valid signature—check against `invoice.id`. --- # Balance and fees Source: https://docs.bitsby.app/balance-and-fees Because our payment service delivers payments straight to the users' (merchants') own wallets, it cannot deduct the fee from the payment itself. So we built a mechanism where the fee is deducted from the account balance in the dashboard. The merchant tops up the account balance from time to time. ## Fee deduction 1. The invoice fee is calculated from the amount actually received, converted to USD. 2. At the start of each day, the fees of all invoices paid during the previous day are summed up. 3. The resulting fee amount in USD is deducted from the account balance. 4. If the account balance does not have enough funds, it goes negative. ## Billing cycle 1. During the week (Monday through Sunday), the service works fully without restrictions (even with a negative account balance). 2. Every Monday at 23:50 UTC, the service checks the account balance; if it is negative, invoice creation is blocked. 3. To unblock it, pay off the debt in full. 4. Once the debt is paid off, the feature is unblocked immediately. 5. For uninterrupted operation (no restrictions), either keep the account balance positive at all times or pay off the debt in full every Monday. ![Billing cycle diagram: every Monday the service checks the balance, and the merchant pays off the past week's debt](/img/docs/V822lBgqEBMs2q9UF39S-image.webp) ## Who pays the fee In each project's settings, you choose which side pays the invoice fee. It can be: 1. The merchant—the fee does not increase the invoice amount. The customer pays the amount the merchant specified at invoice creation. 2. The customer—the fee is added to the invoice amount. The customer pays the invoice amount plus the fee, and the merchant receives the specified amount in full. An example for a 100 USD invoice at a 1% rate. | | Merchant pays | Customer pays | | --------------------------------- | ------------- | ------------- | | Invoice issued | 100 USD | 100 USD | | Customer pays | 100 USD | 101 USD | | Arrives in the merchant's wallet | 100 USD | 101 USD | | Deducted from the account balance | 1 USD | 1 USD | | Merchant's revenue | 99 USD | 100 USD | ## Fee rates for projects The service automatically adjusts each project's fee percentage every week. You can see the percentage set for your projects in the [Balance and fees](https://dash.bitsby.app/billing/tariffs) section. 1. The fee percentage is calculated automatically at the end of each week for each project. 2. The calculation takes the total of all paid invoices created over the last 30 days, per project. The receipts are summed in USD equivalent. 3. The resulting fee percentage is locked in for the new week for each project, based on the fee schedule: | 30-day total | Fee rate % | | ---------------- | ---------- | | From $0.00 | 1.00% | | From $1,000.00 | 0.90% | | From $5,000.00 | 0.80% | | From $10,000.00 | 0.70% | | From $50,000.00 | 0.60% | | From $100,000.00 | 0.50% | --- # Custom domain Source: https://docs.bitsby.app/custom-domain You can connect any domain you own and brand the service as yours. In that case, all logos and links are replaced with your project's. A custom domain is used in two cases: * Branding the payment page * Participating in the partner program ## Branding | | Payment page | Partner program | | ------------------- | :----------: | :-------------: | | Primary domain | + | + | | Available languages | + | + | | Project name | + | + | | Logos, icons | + | + | | Background image | + | + | | Site links | + | + | | Fee rates | | + | | Support contacts | | + | | Telegram bot | | + | ## Addressing All service addresses stay the same; only the domain changes—to yours. For example, with the domain `example.com`: | | Address on bitsby.app | Address on your domain | | ------------ | ---------------------------------------- | ----------------------------------------- | | HTML form | `https://dash.bitsby.app/invoices/form` | `https://dash.example.com/invoices/form` | | API | `https://api.bitsby.app/invoices/create` | `https://api.example.com/invoices/create` | | Payment page | `https://dash.bitsby.app/invoices/pay/…` | `https://dash.example.com/invoices/pay/…` | ## Pricing * Connection is paid; ask technical support for details. * There are no fixed monthly payments. * A yearly payment covers SSL certificate renewals. ## How to connect [Submit a request](https://dash.bitsby.app/support/list) and follow the instructions. The service is paid for before connection. --- # Partner program Source: https://docs.bitsby.app/partner-program The partner program format: connecting a [custom domain](../custom-domain.md) with full branding. The partner grows their service on their own and provides support to their users. We handle the entire technical side so the partner can work smoothly and without interruption. For complex technical questions, the partner can turn to our technical support. We also keep improving the service's features and internal technical side for stable, uninterrupted operation under access restrictions. For users from Russia, we run a separate mirror that currently provides stable access to the service. Our entire infrastructure is protected against DDoS and application-level attacks. The partner can set their own fee schedule. As the operator, we take a small fee of our own (up to 1%) on top of the partner's rates. The fee is agreed individually with each partner and depends on sales volume. ## Why work with us 1. A technically strong product with an extensive feature set. No competitor offers this combination of features. 2. A huge crypto payments market with almost unlimited growth. Only 1% of online stores and services accept crypto payments. 3. Grow your business worldwide, with no regional lock-in. 4. Add any language to get closer to your clients and make the service feel local. 5. Set your own fee rates. 6. Excellent, fast technical support. 7. Quick onboarding that does not require deep technical knowledge. ## Monetization * All your clients' payments arrive at the partner's wallets (yours). * All processing of your clients' payments and balances is automatic. * The service deducts the fee daily on your clients' sales (not top-ups) from your partner balance. * The fee deduction and payment mechanics are the same as for regular clients and are described in [Balance and fees](../balance-and-fees.md). * If the partner is late paying off the debt, we do not block your clients' features—we switch the payment details from the partner's to ours until the debt is fully paid. ## How to connect [Submit a request](https://dash.bitsby.app/support/list) and follow the instructions you receive. The service is paid for before connection. --- # White Label use cases Source: https://docs.bitsby.app/partner-program/white-label-cases ## Website builder **The upside:** Higher conversion to paid subscriptions and a powerful selling point for users—“crypto payments on your site in 1 click.” Plus passive income from the turnover of every store built on the platform. **How it will work:** 1. The payment module is added to the official marketplace or the list of standard blocks (next to Visa/Stripe) under the builder's brand. 2. The user (a merchant) enables the module in their dashboard. To the user it looks like the platform's built-in crypto service. 3. When a customer on the site pays with crypto, the service generates an invoice via the API. The platform takes its share of the fee, and the service takes its own. ## Chatbot builder (Telegram / WhatsApp) **The upside:** Give bot authors a tool for selling digital goods and subscriptions without the blocking risk fiat merchants face. Earn a share of all transactions flowing through the bots. **How it will work:** 1. A standard “Add crypto payment” block appears in the flow builder panel. 2. The bot admin (the builder's client) sets up a sale (say, access to a private channel) and enables the block. 3. The bot issues the customer an invoice. The backend verifies the transaction on-chain and sends a webhook to the builder. The bot grants the client access instantly and automatically. ## Freelance developer **The upside:** Sell clients not just “a website build” but “a ready business with payments in place.” That raises your personal rate by 30–50% and earns a lifelong percentage of the client's turnover. **How it will work:** 1. The freelancer registers a White Label platform of their own and ties it to their domain. 2. On delivery, the client gets access to a payment admin panel carrying the freelancer's logos (or no mention of third-party services at all). The client believes it is custom-built. 3. The client's fee rate is set inside the panel, letting the freelancer earn the difference as passive income. ## Systems integrator (CRM, ERP, accounting systems) **The upside:** Win tenders from large retail and B2B companies that need international payment acceptance. Solve the client's tough need for automated crypto accounting. **How it will work:** 1. A ready-made plugin for CRM systems is built under the integrator's brand. 2. The client's manager clicks an “Issue invoice” button right on the CRM deal page. 3. The system generates a crypto invoice through the White Label. When the client pays, the deal status in the CRM automatically changes to Paid, the warehouse reserves the goods, and the integrator earns a fee. ## Development company (web studio / digital agency) **The upside:** Get your own fintech product with zero development cost, which raises your standing. Capitalize the studio: move from one-off orders to a payment business model with steady cash flow (LTV). **How it will work:** 1. The service is fully branded (logos, colors, domain, email notifications), and the product becomes the company standard for e-commerce. 2. Every new store development client gets this gateway installed as an exclusive solution. 3. The client uses it for years, and the studio earns a steady percentage of sales. ## Game server developer (Minecraft, GTA5/SAMP, mobile games) **The upside:** A complete fix for the blocking problem with classic fiat payment systems (gaming's high-risk status). Higher conversion on in-game purchases thanks to players from all over the world. Your own payment hub that increases the game project's valuation. **How it will work:** 1. The game project deploys the White Label platform on its own subdomain and customizes the interface (logos, colors, fonts) to the style of the specific game or studio. 2. A payment form is embedded in the player's account area (on the server's site or right inside the game launcher). The purchase flow looks like part of the game's ecosystem, which removes gamers' distrust of third-party services. 3. The player picks a product (in-game currency, VIP status, skins), the system generates a crypto invoice via the API to the developer's wallets, and once the transaction confirms on-chain, the game server delivers the item to the player instantly and automatically (via Webhook/RCON). ## Game hosting providers and add-on marketplaces Companies that rent out ready-made servers or sell plugins/mods to game developers have a huge audience of solo admins. **How it works:** The hosting provider embeds the White Label into its control panel. Every hosting client (a server admin) can enable crypto payments for their project in 1 click. The host earns a percentage of the purchases across all servers on its hardware. --- # API reference Source: https://docs.bitsby.app/api --- # Authentication and limits Source: https://docs.bitsby.app/api/authentication Get an API key in the [Integrations](https://dash.bitsby.app/integrations/list) section. ## Capabilities * **Invoice management:** creation, cancellation, status checks * **Payment management:** listing payments, matching a payment to an invoice * **Statistics:** aggregated data by project, period, wallet, and more * **Billing:** project fee rates, balances ## Limits * **Request rate:** at most 1 request per second per key * **Number of keys:** 10 per account ## Request authorization Pass your API key in the `Authorization` header to authorize every API request. An example of the `Authorization` header in an API request: ```bash curl -X POST https://api.bitsby.app/invoices/list \ -H "Authorization: Token MSvL2ltaDZdWVjmZURURMVWhqSJLT2NURjhL2Fla1Z1T1IxQTltKs1T3Ay" ``` ## Server response The API server always responds in JSON. Use the `result` parameter to tell whether the request succeeded. An example response for a successful request: ```json { "result": "success", "data": ..... } ``` An example response for an error: ```json { "result": "error", "data": "Requests too frequent" } ``` --- # Key scope Source: https://docs.bitsby.app/api/key-scope ## Project restriction The Project field limits an API key to a specific project: * **Any**—the key has access to all projects in the account * **A specific project**—the key works only with the selected project's data The restriction behaves differently depending on the method. In listing methods it is a filter: the response is successful but contains only the project's data. In methods that address a specific invoice or wallet, a request for another project's object is rejected with `Restricted project`. | Method | Key restricted to a project | | ---------------------- | --------------------------------------------------------------------------- | | `billing/balances` | Account-level data; the restriction does not apply | | `billing/history` | The project's operations plus account-level operations—top-ups and bonuses | | `billing/tariffs` | Fee rate and turnover only for the key's project | | `invoices/list` | Results only for the key's project | | `payments/list` | Results only for the key's project | | `statistics/invoices` | Calculated only for the key's project | | `statistics/payments` | Calculated only on the key's project invoices | | `invoices/create` | `projectId` in the request must match the key's project | | `invoices/cancel` | Another project's invoice is inaccessible | | `invoices/bindPayment` | Another project's invoice is inaccessible | ## Scope restriction The scope defines which API methods the key can use. It is a required, multi-select field. A call to a method outside the key's scope is rejected with `Restricted scope`. ## Combining restrictions The project and scope restrictions work together. For example, if a key is limited to invoice methods and tied to a specific project, then: * Only invoice methods are available * Data is filtered to the specified project only * Requests for payments or statistics are rejected * Requests for other projects' data are rejected --- # Testing in Postman Source: https://docs.bitsby.app/api/postman ## Importing the project 1. Download the ready-made project via this [link](https://dash.bitsby.app/api.bitsby.app.postman_collection.json). 2. Import it into the Collections section. 3. Set your API key in the Token variable on the Variables tab. 4. If you work on a custom domain, set it in the Host variable. 5. When running the whole collection via the Runner, set a delay of at least 1 second between requests—otherwise the rate limit kicks in. --- # Create an invoice Source: https://docs.bitsby.app/api/create-invoice ## Request ```bash curl -X POST https://api.bitsby.app/invoices/create \ -H "Authorization: Token MSvL2ltaDZdWVjmZURURMVWhqSJLT2NURjhL2Fla1Z1T1IxQTltKs1T3Ay" \ -F "projectId=9deea1e2-0c08-41a3-bdc2-a34eada3892d" \ -F "amountFiat=10.55" \ -F "currencyFiat=EUR" \ -F "description=12 months license for BITSBY" \ -F "serviceData={\"userId\":100,\"orderId\":500}" \ -F "timeToPay=12" ``` ## Request parameters | Parameter | Data type | Description | Example | Required? | | -------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | --------- | | `projectId` | UUID | Project ID in UUID format | 1c48b092-9878-48f5-b441-07a8a48e7b54 | yes | | `amountFiat` | Float | Amount in fiat currency | 10.55 | yes | | `currencyFiat` | String | Fiat currency. Allowed values: USD, EUR, RUB | EUR | yes | | `description` | String | Description of the product or service. Shown to the customer on the payment page | 12 months license for BITSBY | no | | `serviceData` | String | Order identifier on the store side. Not shown to the customer. This data associates the invoice and payment with the customer in the merchant's store. JSON is a convenient format for it | {"userId":100,"orderId":500} | no | | `timeToPay` | Float | Time to pay the invoice, in hours. Allowed values: 0.5, 1, 3, 6, 12 | 1 | yes | ## Response example ```json { "result":"success", "data":{ "id":"ade9550d-3dc7-4fd3-b94e-3b4c12aaaa0c", "uid":"MXNj4m8HhcM4", "createDatetime":"2024-02-29 15:20:24", "timeToPayDatetime":"2024-03-01 03:20:24", "commissionFiatUSD":0.1, "amountFiatUSD":10, "url":"https:\/\/dash.bitsby.app\/invoices\/pay\/MXNj4m8HhcM4" } } ``` ## Response parameters | Parameter | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------- | | `id` | Invoice ID in UUID format | | `uid` | Invoice ID (for the customer) | | `createDatetime` | Invoice creation date and time (UTC) | | `timeToPayDatetime` | Date and time until which the invoice is valid (UTC). The customer must pay the invoice before this time | | `commissionFiatUSD` | Service fee amount. Calculated from the `amountFiatUSD` amount | | `amountFiatUSD` | Amount in USD, calculated at invoice creation from `invoice.amountFiat` at the current exchange rate | | `url` | Link to the payment page for the customer. Send this link to the customer to pay the invoice | --- # List invoices Source: https://docs.bitsby.app/api/list-invoices ## Request ```bash curl -X POST https://api.bitsby.app/invoices/list \ -H "Authorization: Token MSvL2ltaDZdWVjmZURURMVWhqSJLT2NURjhL2Fla1Z1T1IxQTltKs1T3Ay" \ -F "startDate=2000-01-01" \ -F "endDate=2030-01-01" \ -F "offset=0" \ -F "limit=10" ``` ## Request parameters | Parameter | Data type | Description | Example | Required? | Default | | ------------ | --------- | --------------------------------------------------------------- | ------------------------------------ | --------- | ------- | | `invoiceId` | UUID | Invoice ID in UUID format | 1c48b092-9878-48f5-b441-07a8a48e7b54 | no | | | `invoiceUid` | String | Invoice ID (for the customer) | F7ncQviM1N92 | no | | | `projectId` | UUID | Project ID in UUID format | 1c48b092-9878-48f5-b441-07a8a48e7b54 | no | | | `startDate` | String | Start date of invoice creation, in Y-m-d format | 2000-01-01 | no | | | `endDate` | String | End date of invoice creation, in Y-m-d format | 2030-01-01 | no | | | `status` | String | Invoice status. Allowed values: unpaid, canceled, paid, expired | paid | no | | | `offset` | Integer | Index of the first record to return | 0 | no | 0 | | `limit` | Integer | Number of records you want to receive | 50 | no | 10 | ## Response example ```json { "result": "success", "data": [ { "id": "59302b02-d1be-4e10-960f-d6ec24bfcaaf", "uid": "F7nYgvxM1N92", "projectId": "9deea1e2-0c08-41a3-bdc2-a34eada3892d", "createDatetime": "2024-02-29 10:49:56", "timeToPayDatetime": "2024-02-29 22:49:56", "commissionFiatUSD": 1, "amountFiatUSD": 100, "amountFiat": 100, "currencyFiat": "USD", "description": null, "serviceData": null, "views": 0, "status": "canceled" }, { "id": "f31c61f6-407f-4999-95cf-56477eff06d4", "uid": "riUZBnNvd2rX", "projectId": "9deea1e2-0c08-41a3-bdc2-a34eada3892d", "createDatetime": "2024-02-27 20:57:23", "timeToPayDatetime": "2024-02-28 08:57:23", "commissionFiatUSD": 0.12, "amountFiatUSD": 12, "amountFiat": 12, "currencyFiat": "USD", "description": null, "serviceData": "orderId:2444", "views": 3, "status": "paid", "payment": { "id": "f986ad8d-2298-473d-982a-efbc817b975d", "amount": 12, "hash": "f09352627039c088688806821b864895ff237558612d834861621fc026de5f68", "transactionDatetime": "2024-02-27 21:05:36" }, "wallet": { "id": "47aa71e2-07a0-482e-9172-7114d7376ba0", "name": "usdt-tron", "blockchain": "tron", "cryptocurrency": "usdt", "address": "TKbstUwMzLrfTAGL4erYb7gc7ghmHQ9zG7" } } ] } ``` ## Response parameters | Parameter | Description | | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | Invoice ID in UUID format | | `uid` | Invoice ID (for the customer) | | `projectId` | Project ID in UUID format | | `createDatetime` | Invoice creation date and time (UTC) | | `timeToPayDatetime` | Date and time until which the invoice is valid (UTC). The customer must pay the invoice before this time | | `commissionFiatUSD` | Service fee amount. Calculated from the `amountFiatUSD` amount | | `amountFiatUSD` | The invoice amount converted to USD. At invoice creation it is calculated from `amountFiat` at the current exchange rate. At the moment of payment it is overwritten with the amount actually received, converted to USD at the rate at that moment. The fee in `commissionFiatUSD` is recalculated from it as well | | `amountFiat` | The original invoice amount in fiat currency. Does not change | | `currencyFiat` | Fiat currency | | `description` | The description set at invoice creation. Shown to the customer | | `serviceData` | The service data set at invoice creation. Not shown to the customer. Used to identify the invoice and payment in the merchant's store | | `views` | Number of payment page views | | `status` | Invoice status | | `payment` | Block with the payment parameters. Present if the invoice was paid | | `payment.id` | Payment ID in UUID format | | `payment.amount` | Payment amount in cryptocurrency | | `payment.hash` | The on-chain transaction hash | | `payment.transactionDatetime` | Date and time of the on-chain transaction (UTC) | | `wallet` | Block with the parameters of the wallet that received the payment. Present if the invoice was paid | | `wallet.id` | Wallet ID in UUID format | | `wallet.name` | Wallet name | | `wallet.blockchain` | The crypto wallet's blockchain | | `wallet.cryptocurrency` | The wallet's cryptocurrency | | `wallet.address` | The crypto wallet's address | --- # Cancel an invoice Source: https://docs.bitsby.app/api/cancel-invoice ## Overview You can cancel an invoice so the customer can no longer pay it. This is useful when the invoice contains a mistake. Cancellation requires two conditions: * Invoice status: Unpaid * Payment page views: 0 If the invoice belongs to another project and the key is restricted to a project, the method responds with `Restricted project`. If you have no invoice with that identifier—`Invoice not found`. If the invoice is found but the cancellation conditions are not met—`Invoice cannot be canceled`. ## Request ```bash curl -X POST https://api.bitsby.app/invoices/cancel \ -H "Authorization: Token MSvL2ltaDZdWVjmZURURMVWhqSJLT2NURjhL2Fla1Z1T1IxQTltKs1T3Ay" \ -F "invoiceId=ade9550d-3dc7-4fd3-b94e-3b4c12aaaa0c" ``` ## Request parameters | Parameter | Data type | Description | Example | Required? | | ----------- | --------- | ------------------------- | ------------------------------------ | --------- | | `invoiceId` | UUID | Invoice ID in UUID format | ade9550d-3dc7-4fd3-b94e-3b4c12aaaa0c | yes | ## Response example ```json { "result":"success", "data":"Invoice canceled" } ``` --- # List payments Source: https://docs.bitsby.app/api/list-payments ## Overview The service automatically finds all payments arriving at your crypto wallets, but only while there are invoices with an active search. The search runs while an invoice is unpaid and continues for one more hour after the payment deadline—to account for slow networks. If no such invoices exist, an incoming transaction does not enter the system. ## Request ```bash curl -X POST https://api.bitsby.app/payments/list \ -H "Authorization: Token MSvL2ltaDZdWVjmZURURMVWhqSJLT2NURjhL2Fla1Z1T1IxQTltKs1T3Ay" \ -F "startDate=2000-01-01" \ -F "endDate=2030-01-01" \ -F "offset=0" \ -F "limit=10" ``` ## Request parameters | Parameter | Data type | Description | Example | Required? | Default | | ----------- | --------- | ------------------------------------------------------- | ------------------------------------ | --------- | ------- | | `walletId` | UUID | Wallet ID in UUID format | 1c48b092-9878-48f5-b441-07a8a48e7b54 | no | | | `invoiceId` | UUID | Invoice ID in UUID format | 1c48b092-9878-48f5-b441-07a8a48e7b54 | no | | | `startDate` | String | Start date of the on-chain transaction, in Y-m-d format | 2000-01-01 | no | | | `endDate` | String | End date of the on-chain transaction, in Y-m-d format | 2030-01-01 | no | | | `offset` | Integer | Index of the first record to return | 0 | no | 0 | | `limit` | Integer | Number of records you want to receive | 50 | no | 10 | ## Response example A payment already matched to an invoice: ```json { "result":"success", "data":[ { "id":"f986ad8d-2298-473d-982a-efbc817b975d", "amount":12, "hash":"f09352627039c088688806821b864895ff237558612d834861621fc026de5f68", "transactionDatetime":"2024-02-27 21:05:36", "invoice":{ "id":"f31c61f6-407f-4999-95cf-56477eff06d4", "uid":"riUZBnNvd2rX", "projectId":"9deea1e2-0c08-41a3-bdc2-a34eada3892d", "createDatetime":"2024-02-27 20:57:23", "timeToPayDatetime":"2024-02-28 08:57:23", "commissionFiatUSD":0.12, "amountFiatUSD":12, "amountFiat":12, "currencyFiat":"USD", "description":null, "serviceData":"orderId:2444", "views":1, "status":"paid" }, "wallet":{ "id":"47aa71e2-07a0-482e-9172-7114d7376ba0", "name":"usdt-tron", "blockchain":"tron", "cryptocurrency":"usdt", "address":"TKbstUwMzLrfTAGL4erYb7gc7ghmHQ9zG7" } }, { "id":"f3195184-9a6f-4c82-928b-d7fe7c015b7b", "amount":5.02, "hash":"74763b65e43bcc9492a6ce9a7f26fbfdbd7635aecd3454420b5e9534cba50ee6", "transactionDatetime":"2024-02-26 13:32:57", "invoice":{ "id":"fac8ef03-2c41-4ce0-a432-7c33b928f16f", "uid":"AFhygKX21ecd", "projectId":"9deea1e2-0c08-41a3-bdc2-a34eada3892d", "createDatetime":"2024-02-26 13:29:24", "timeToPayDatetime":"2024-02-27 01:29:24", "commissionFiatUSD":0.05, "amountFiatUSD":5.02, "amountFiat":5, "currencyFiat":"USD", "description":null, "serviceData":"orderId:3419", "views":3, "status":"paid" }, "wallet":{ "id":"47aa71e2-07a0-482e-9172-7114d7376ba0", "name":"usdt-tron", "blockchain":"tron", "cryptocurrency":"usdt", "address":"TKbstUwMzLrfTAGL4erYb7gc7ghmHQ9zG7" } } ] } ``` **A payment** the service has found but **not yet matched to an invoice** comes without the invoice block. These payments are the candidates for manual matching: ```json { "result":"success", "data":[ { "id":"c1d0f4a2-93b7-4f61-8c25-1a6f0e2b7d94", "amount":9.87, "hash":"5b1e4b1cf0a4c3d2e9f6a7b8c9d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192", "transactionDatetime":"2024-02-28 09:14:02", "wallet":{ "id":"47aa71e2-07a0-482e-9172-7114d7376ba0", "name":"usdt-tron", "blockchain":"tron", "cryptocurrency":"usdt", "address":"TKbstUwMzLrfTAGL4erYb7gc7ghmHQ9zG7" } } ] } ``` ## Response parameters | Parameter | Description | | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | Payment ID in UUID format | | `amount` | Payment amount in cryptocurrency | | `hash` | The on-chain transaction hash | | `transactionDatetime` | Date and time of the on-chain transaction (UTC) | | `invoice` | Block with the invoice parameters. Present if an invoice is matched to the payment | | `invoice.id` | Invoice ID in UUID format | | `invoice.uid` | Invoice ID (for the customer) | | `invoice.projectId` | Project ID in UUID format | | `invoice.createDatetime` | Invoice creation date and time (UTC) | | `invoice.timeToPayDatetime` | Date and time until which the invoice is valid (UTC) | | `invoice.commissionFiatUSD` | Service fee amount. Calculated from the `invoice.amountFiatUSD` amount | | `invoice.amountFiatUSD` | The invoice amount converted to USD. At invoice creation it is calculated from `invoice.amountFiat` at the current exchange rate. At the moment of payment it is overwritten with the amount actually received, converted to USD at the rate at that moment. The fee in `invoice.commissionFiatUSD` is recalculated from it as well | | `invoice.amountFiat` | The original invoice amount in fiat currency. Does not change | | `invoice.currencyFiat` | Fiat currency | | `invoice.description` | The description set at invoice creation | | `invoice.serviceData` | The service data set at invoice creation | | `invoice.views` | Number of payment page views | | `invoice.status` | Invoice status | | `wallet` | Block with the wallet parameters | | `wallet.id` | Wallet ID in UUID format | | `wallet.name` | Wallet name | | `wallet.blockchain` | The crypto wallet's blockchain | | `wallet.cryptocurrency` | The wallet's cryptocurrency | | `wallet.address` | The crypto wallet's address | --- # Match a payment to an invoice Source: https://docs.bitsby.app/api/bind-payment ## Overview In the vast majority of cases, customers pay exactly the amount stated in the invoice to your crypto wallet. But sometimes a customer makes a mistake and pays something other than the reserved amount. In that case, the service cannot automatically match the payment to the invoice and mark the invoice as paid. If this happens, you as the merchant can match the payment to the invoice manually, provided you are certain which payment you are expecting. You can do this in the dashboard or via the API. ## Matching conditions The match goes through only when all of the conditions hold at once: 1. **Invoice status**—`unpaid` or `expired`. Canceled and already paid invoices cannot be matched. Cancellation is irreversible. 2. **The payment is not yet matched to another invoice.** One payment can be tied to only one invoice. 3. **The payment arrived at a wallet involved in this invoice.** 4. **The payment fits within the available payment window**—24 hours before and 24 hours after invoice creation. A payment that arrived before the invoice was created can be matched; one outside the window cannot. If the invoice belongs to another project and the key is restricted to a project, the method responds with `Restricted project`. If any other condition fails, a generic error comes back with no reason given—check the conditions on your side. ## What happens after matching * The invoice moves to **Paid**. * Notifications go out to email, the Telegram bot, and the Webhook URL—just as with automatic matching. * The invoice amount `invoice.amountFiatUSD` **is overwritten with the amount actually received**, converted to USD at the rate at the moment of matching. The original amount in `invoice.amountFiat` does not change. * The service fee is recalculated at the project's fee rate on the amount you received. On overpayment it is higher than calculated at invoice creation; on underpayment, lower. ## Request ```bash curl -X POST https://api.bitsby.app/invoices/bindPayment \ -H "Authorization: Token MSvL2ltaDZdWVjmZURURMVWhqSJLT2NURjhL2Fla1Z1T1IxQTltKs1T3Ay" \ -F "invoiceId=ade9550d-3dc7-4fd3-b94e-3b4c12aaaa0c" \ -F "paymentId=a4c9e2ee-9a03-43e5-a1a1-00caf679d16a" ``` ## Request parameters | Parameter | Data type | Description | Example | Required? | | ----------- | --------- | ------------------------- | ------------------------------------ | --------- | | `invoiceId` | UUID | Invoice ID in UUID format | ade9550d-3dc7-4fd3-b94e-3b4c12aaaa0c | yes | | `paymentId` | UUID | Payment ID in UUID format | a4c9e2ee-9a03-43e5-a1a1-00caf679d16a | yes | ## Response example ```json { "result":"success", "data":"Invoice binded to payment" } ``` --- # Invoice statistics Source: https://docs.bitsby.app/api/invoice-statistics ## Overview Returns overall statistics for your invoices. ## Request ```bash curl -X POST https://api.bitsby.app/statistics/invoices \ -H "Authorization: Token MSvL2ltaDZdWVjmZURURMVWhqSJLT2NURjhL2Fla1Z1T1IxQTltKs1T3Ay" \ -F "startDate=2000-01-01" \ -F "endDate=2030-01-01" ``` ## Request parameters | Parameter | Data type | Description | Example | Required? | | ----------- | --------- | ----------------------------------------------- | ---------- | --------- | | `startDate` | String | Start date of invoice creation, in Y-m-d format | 2000-01-01 | no | | `endDate` | String | End date of invoice creation, in Y-m-d format | 2030-01-01 | no | ## Response example ```json { "result": "success", "data": { "invoicesTotalNum": 51, "invoicesPaidNum": 24, "invoicesTotalUSD": 335.03, "invoicesByProjects": { "9deea1e2-0c08-41a3-bdc2-a34eada3892d": 335.03 }, "invoicesByCurrencies": { "USD": 335.03 }, "invoicesByMonths": { "March-2024": 42, "February-2024": 293.03 }, "invoicesByDate": { "2024-03-04": { "date": 1709510400000, "value": 21 }, "2024-03-03": { "date": 1709424000000, "value": 10 }, "2024-03-01": { "date": 1709251200000, "value": 11 }, "2024-02-27": { "date": 1708992000000, "value": 12 }, "2024-02-26": { "date": 1708905600000, "value": 281.03 } } } } ``` ## Response parameters | Parameter | Description | | ---------------------- | -------------------------------------------------------- | | `invoicesTotalNum` | Total number of invoices | | `invoicesPaidNum` | Number of paid invoices (with the Paid status) | | `invoicesTotalUSD` | Total of all paid invoices in USD | | `invoicesByProjects` | Total of all paid invoices by project, in USD | | `invoicesByCurrencies` | Total of all paid invoices by currency | | `invoicesByMonths` | Total of all paid invoices by month, in USD | | `invoicesByDate` | Total of all paid invoices by day, in USD | | `invoicesByDate.date` | Date as a Unix timestamp, in milliseconds | | `invoicesByDate.value` | Total of all paid invoices for the specific date, in USD | --- # Payment statistics Source: https://docs.bitsby.app/api/payment-statistics ## Overview Returns received totals broken down by cryptocurrency. Only payments matched to invoices are included in the statistics. ## Request ```bash curl -X POST https://api.bitsby.app/statistics/payments \ -H "Authorization: Token MSvL2ltaDZdWVjmZURURMVWhqSJLT2NURjhL2Fla1Z1T1IxQTltKs1T3Ay" \ -F "startDate=2000-01-01" \ -F "endDate=2030-01-01" ``` ## Request parameters | Parameter | Data type | Description | Example | Required? | | ----------- | --------- | -------------------------------------------------------------- | ---------- | --------- | | `startDate` | String | Start date of payment processing by the service, in Y-m-d format | 2000-01-01 | no | | `endDate` | String | End date of payment processing by the service, in Y-m-d format | 2030-01-01 | no | ## Response example ```json { "result": "success", "data": { "usdt": 336.555589 } } ``` --- # Account balances Source: https://docs.bitsby.app/api/balances ## Request ```bash curl -X POST https://api.bitsby.app/billing/balances \ -H "Authorization: Token MSvL2ltaDZdWVjmZURURMVWhqSJLT2NURjhL2Fla1Z1T1IxQTltKs1T3Ay" ``` ## Response example ```json { "result": "success", "data": { "internal": 100, "partner": 0 } } ``` --- # Balance history Source: https://docs.bitsby.app/api/balance-history ## Request ```bash curl -X POST https://api.bitsby.app/billing/history \ -H "Authorization: Token MSvL2ltaDZdWVjmZURURMVWhqSJLT2NURjhL2Fla1Z1T1IxQTltKs1T3Ay" \ -F "offset=0" \ -F "limit=10" ``` ## Request parameters | Parameter | Data type | Description | Example | Required? | Default | | --------- | --------- | ------------------------------------- | ------- | --------- | ------- | | `offset` | Integer | Index of the first record to return | 0 | no | 0 | | `limit` | Integer | Number of records you want to receive | 50 | no | 10 | ## Response example ```json { "result": "success", "data": [ { "datetime": "2024-11-30 18:02:41", "event": "other", "projectId": null, "target": "internal", "amount": 5, "comment": "welcome bonus 5" }, { "datetime": "2025-08-15 02:00:01", "event": "commission", "projectId": "9deea1e2-0c08-41a3-bdc2-a34eada3892d", "target": "internal", "amount": -0.15, "comment": "Commission 14.08.2025 (Sample project)" } ] } ``` ## Response parameters | Parameter | Description | | ----------- | ------------------------------------------------------------------------------------------------------- | | `datetime` | Event date and time | | `event` | Event type. Allowed values: deposit, withdrawal, transfer, commission, aml, other | | `projectId` | ID of the project the operation belongs to. For account-level operations—top-ups and bonuses—it is null | | `target` | The balance that was changed. Allowed values: internal, partner | | `amount` | The amount the balance was changed by | | `comment` | Event comment | --- # Project fees Source: https://docs.bitsby.app/api/project-fees ## Request ```bash curl -X POST https://api.bitsby.app/billing/tariffs \ -H "Authorization: Token MSvL2ltaDZdWVjmZURURMVWhqSJLT2NURjhL2Fla1Z1T1IxQTltKs1T3Ay" ``` ## Response example ```json { "result": "success", "data": [ { "id": "f31c61f6-407f-4999-95cf-56477eff06d4", "name": "Bitsby crypto payments", "30dPayments": 123456.78, "payer": "seller", "rate": 0.5 }, { "id": "fac8ef03-2c41-4ce0-a432-7c33b928f16f", "name": "My project", "30dPayments": 0, "payer": "buyer", "rate": 1 } ] } ``` ## Response parameters | Parameter | Description | | ------------- | -------------------------------------------------------- | | `id` | Project ID in UUID format | | `name` | Project name | | `30dPayments` | Total of paid invoices created over the last 30 days | | `payer` | The side that pays the fee. Allowed values: seller, buyer | | `rate` | Fee rate in % | --- # Plugins for online stores and CMS Source: https://docs.bitsby.app/cms-plugins :::info **Under development.** We are building ready-made modules for quick and secure Bitsby integration with popular e-commerce engines and CMS platforms. ::: --- # Changelog Source: https://docs.bitsby.app/changelog ## September 2026 ### September 10 **The HTML form now submits a signed container.** Instead of separate invoice fields—`data` (base64url of the JSON) and `signature` (HMAC-SHA256 of the `data` string). The endpoint changed from `invoices/createFromForm` to `invoices/form`. Old forms no longer work and must be rebuilt: see [HTML form signature](./creating-invoices/html-forms/form-signature.md). ### September 8 **Numeric fields in the webhook body are now numbers.** Six fields that used to be sent as strings are now sent as numbers: `project.commissionRate`, `invoice.commissionFiatUSD`, `invoice.amountFiatUSD`, `invoice.amountFiat`, `invoice.calcAmountFiat`, `payment.amount`. The set of fields, their names, and their order have not changed. Keep in mind: trailing zeros are dropped, so an amount of 10.00 arrives as 10. Amounts below 0.0001 arrive in exponential notation, for example 1.0e-6—standard JSON parsing returns the correct number; only manual string parsing breaks. Notifications built before the rollout and not yet delivered arrive in the old format. The receiver must accept both variants until the queue drains—this takes no more than two days. ### September 6 **Numeric fields in API responses are now numbers.** Affects `invoices/list`, `payments/list`, and `invoices/create`. | Field | Before | After | | ------------------- | ---------------- | ------ | | `commissionFiatUSD` | `"0.10"` | `0.1` | | `amountFiatUSD` | `"10.00"` | `10` | | `amountFiat` | `"1000.00"` | `1000` | | `views` | `"0"` | `0` | | `payment.amount` | `"10.500000000"` | `10.5` | **A key restricted to a project now applies the restriction in all methods.** Previously, some methods ignored the project. Now it is a filter in listing methods, and a request for another project's invoice or wallet is rejected with `Restricted project`. A method that cannot honor the project restriction is closed to such a key. Details are in the “Key scope” section. **The payment list now returns unmatched payments.** Previously, `payments/list` returned only payments tied to an invoice. Now the results include all payments across your wallets; an unmatched one has no `invoice` block. These are exactly the payments needed for manual matching via `invoices/bindPayment`. **Invoice cancellation now distinguishes refusal reasons.** Previously, both a missing invoice and unmet conditions returned a generic `Invoice not found`. Now `Invoice not found` means only that the invoice does not exist, while unmet conditions return `Invoice cannot be canceled`. **Malformed identifiers are now rejected.** `invoices/list` validates `invoiceId`, `invoiceUid`, and `projectId`; `payments/list`—`walletId` and `invoiceId`. A value that does not fit returns `Parameter is filled in incorrectly` with the field name. Previously, such a parameter was silently dropped and the results came back unfiltered. **Period boundaries now include the whole named day.** `startDate` and `endDate` are interpreted from `00:00:00` to `23:59:59`. Previously, the end day was cut off entirely, and a one-day query returned only records created exactly at midnight. Affects `invoices/list`, `payments/list`, `statistics/invoices`, and `statistics/payments`. **A project field was added to the balance history.** Every operation in the `billing/history` method now carries a `projectId` field. For account-level operations—top-ups and bonuses—it is `null`. ### September 4 **Description and service data come back in their original form.** The `description` and `serviceData` fields used to be stored encoded, and in API responses a quote arrived as `"` and a less-than sign as `<`. Now exactly what the merchant submitted is returned. When rendering in HTML, escape the value on your side. **HTML forms accept only POST.** Creating an invoice by following a link with parameters in the address bar no longer works—parameters are read only from the request body. If you used links, replace them with a form with `method="post"`. ### September 3 **Webhook notifications are now signed.** The `X-Timestamp` and `X-Signature` headers were added; the signature is computed with HMAC-SHA256 on the webhook secret. Verifying the signature lets you tell a genuine notification from a forgery. The mechanics and examples are in the “Webhook signature verification” section. The change is backward compatible: if you do not add the check, your handler keeps working as before.