Bắt đầu nhanh
Một tích hợp thanh toán tối giản: tạo hóa đơn qua API và xử lý thông báo thanh toán. Phần cuối nói về giao hàng. Các ví dụ độc lập và đầy đủ; không dùng SDK.
Bạn đang làm việc với một AI agent? Hãy đưa cho nó phiên bản markdown của trang này. Trang có đủ chi tiết để xây dựng tích hợp cho trang web hoặc cửa hàng của bạn.
Điều kiện tiên quyết
| Giá trị | Lấy ở đâu |
|---|---|
| Khóa API | Mục Tích hợp (Integrations) |
| ID dự án | Mục Dự án (Projects) |
| Khóa bí mật webhook | Cài đặt dự án |
Cũng trong cài đặt dự án đó, hãy đặt Webhook URL — địa chỉ của trình xử lý thông báo trên máy chủ của bạn. Chỉ HTTPS; dịch vụ không đi theo chuyển hướng.
Các ví dụ dùng giá trị thử nghiệm — hãy thay bằng giá trị của bạn.
Tạo hóa đơn
POST https://api.bitsby.app/invoices/create, khóa API truyền trong header Authorization.
| Tham số | Bắt buộc | Giá trị |
|---|---|---|
projectId | có | ID dự án, UUID |
amountFiat | có | Số tiền hóa đơn |
currencyFiat | có | Đồng tiền: USD, EUR, RUB |
timeToPay | có | Thời hạn thanh toán tính bằng giờ: 0.5, 1, 3, 6, 12 |
description | không | Mô tả, hiển thị cho người mua trên trang thanh toán |
serviceData | không | Dữ liệu dịch vụ, không hiển thị cho người mua. Nên truyền số đơn hàng: giá trị này quay lại trong thông báo thanh toán |
- cURL
- PHP
- TypeScript
- JavaScript
- Python
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
$apiKey = 'MSvL2ltaDZdWVjmZURURMVWhqSJLT2NURjhL2Fla1Z1T1IxQTltKs1T3Ay';
$projectId = '9deea1e2-0c08-41a3-bdc2-a34eada3892d';
$ch = curl_init('https://api.bitsby.app/invoices/create');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => 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
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
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
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
Phản hồi:
{
"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"
}
}
Lưu data.id cùng đơn hàng và đưa người mua đến data.url — trang thanh toán. Gửi liên kết theo cách bạn muốn: chuyển hướng, email hay tin nhắn bot. Hóa đơn có hiệu lực đến timeToPayDatetime (UTC).
Xử lý thông báo
Khi hóa đơn chuyển sang trạng thái Đã thanh toán (Paid), dịch vụ gửi một yêu cầu POST đến Webhook URL của dự án. Phần thân là JSON; chữ ký nằm trong các header:
| Header | Giá trị |
|---|---|
X-Timestamp | Thời điểm gửi, unix time tính bằng giây |
X-Signature | sha256= + HMAC-SHA256 dạng hex của chuỗi <timestamp>.<request body> |
Khóa ký là khóa bí mật webhook. Dịch vụ tính chữ ký trên phần thân yêu cầu thô, vì vậy hãy xác minh chữ ký trước khi phân tích JSON.
- PHP
- TypeScript
- JavaScript
- Python
<?php
$secret = 'k7QwR2mZ9tXbN4vL8sJpH3dF6yA1cE0u';
$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
// Reject replayed requests: allow up to 5 minutes of clock drift
if (abs(time() - (int)$timestamp) > 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);
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);
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);
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
Phản hồi bằng mã 2xx trong vòng 10 giây. Mã khác, chuyển hướng hoặc hết thời gian chờ đều tính là gửi thất bại: dịch vụ thử lại với khoảng cách tăng dần từ 5 phút đến 24 giờ, rồi dừng. Đưa các xử lý dài vào hàng đợi: phản hồi 200 trước, rồi mới xử lý đơn hàng.
Định dạng thông báo đầy đủ và mô tả các trường nằm trong mục Webhook URL.
Giao hàng
Thông báo đã xác minh với trạng thái paid xác nhận khoản thanh toán. Các bước giao hàng:
- Tìm đơn hàng theo
invoice.serviceData— giá trị đã truyền khi tạo hóa đơn (order-4172). - Kiểm tra theo
invoice.idxem hóa đơn này đã được giao hàng chưa. Thông báo với cùnginvoice.idcó thể đến nhiều lần — chỉ giao hàng một lần và lưu cờ đã xử lý. - Xác minh số tiền và đồng tiền theo
invoice.amountFiatvàinvoice.currencyFiat— đây là giá trị gốc của hóa đơn và không bao giờ thay đổi.amountFiatUSDbị ghi đè bằng số tiền thực nhận. So sánh số tiền dưới dạng số, không phải chuỗi: các số 0 cuối bị lược bỏ, nên49.90đến dưới dạng49.9. Nếu các giá trị không khớp với đơn hàng, hãy chuyển hóa đơn sang xem xét thủ công thay vì giao hàng — những trường hợp này được trình bày trong Khớp thanh toán và chênh lệch số tiền. - Giao hàng và đánh dấu đơn hàng đã xử lý.
Đừng kiểm tra thời hạn thanh toán khi giao hàng: khoản thanh toán có thể được xác nhận on-chain sau timeToPayDatetime, và người bán có thể khớp khoản thanh toán với hóa đơn theo cách thủ công. Bản thân thông báo đã xác nhận khoản thanh toán.
Tiếp theo
- Tham chiếu API — danh sách hóa đơn, hủy hóa đơn, thống kê, số dư
- Xác minh chữ ký webhook — chi tiết cơ chế chữ ký
- Khớp thanh toán và chênh lệch số tiền — thanh toán thiếu, thanh toán thừa, các khoản thanh toán chưa khớp
- Biểu mẫu HTML — nhận thanh toán không cần API