Démarrage rapide
Une intégration de paiement minimale : la création d'une facture via l'API et le traitement de la notification de paiement. La dernière section couvre la livraison de la commande. Les exemples sont autonomes ; aucun SDK n'est utilisé.
Vous travaillez avec un agent IA ? Donnez-lui la version markdown de cette page. Elle est assez détaillée pour construire une intégration pour votre site ou votre boutique.
Prérequis
| Valeur | Où la trouver |
|---|---|
| Clé d'API | La section Intégrations (Integrations) |
| ID du projet | La section Projets (Projects) |
| Secret du webhook | Réglages du projet |
Dans ces mêmes réglages du projet, définissez le Webhook URL — l'adresse du gestionnaire de notifications sur votre serveur. HTTPS uniquement ; les redirections ne sont pas suivies.
Les exemples utilisent des valeurs de test — remplacez-les par les vôtres.
Création d'une facture
POST https://api.bitsby.app/invoices/create, la clé d'API est passée dans l'en-tête Authorization.
| Paramètre | Obligatoire | Valeur |
|---|---|---|
projectId | oui | ID du projet, UUID |
amountFiat | oui | Montant de la facture |
currencyFiat | oui | Devise : USD, EUR, RUB |
timeToPay | oui | Délai de paiement en heures : 0.5, 1, 3, 6, 12 |
description | non | Description, affichée au client sur la page de paiement |
serviceData | non | Données de service, non affichées au client. Nous recommandons d'y passer le numéro de commande : il revient dans la notification de paiement |
- 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
Réponse :
{
"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"
}
}
Conservez data.id avec la commande et envoyez le client vers data.url — la page de paiement. Transmettez le lien comme vous voulez : une redirection, un e-mail, un message de bot. La facture est valable jusqu'à timeToPayDatetime (UTC).
Traitement de la notification
Quand une facture passe au statut Payée (Paid), le service envoie une requête POST vers le Webhook URL du projet. Le corps est en JSON ; la signature est dans les en-têtes :
| En-tête | Valeur |
|---|---|
X-Timestamp | Heure d'envoi, unix time en secondes |
X-Signature | sha256= + HMAC-SHA256 en hexadécimal de la chaîne <timestamp>.<request body> |
La clé de signature est le secret du webhook. La signature est calculée sur le corps brut de la requête, vérifiez-la donc avant d'analyser le 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
Répondez avec un code 2xx sous 10 secondes. Tout autre code, une redirection ou un dépassement de délai compte comme une remise échouée : le service réessaie à intervalles croissants, de 5 minutes à 24 heures, puis s'arrête. Déportez les traitements longs dans une file d'attente : répondez 200 d'abord, puis traitez la commande.
Le format complet de la notification et la référence des champs se trouvent dans la section Webhook URL.
Livraison de la commande
Une notification vérifiée avec le statut paid confirme le paiement. Étapes de la livraison :
- Retrouvez la commande par
invoice.serviceData— la valeur passée à la création de la facture (order-4172). - Vérifiez par
invoice.idsi cette facture a déjà été livrée. Une notification avec le mêmeinvoice.idpeut arriver plusieurs fois — livrez la commande une seule fois et stockez un marqueur de traitement. - Contrôlez le montant et la devise avec
invoice.amountFiatetinvoice.currencyFiat— ce sont les valeurs d'origine de la facture et elles ne changent jamais.amountFiatUSDest remplacé par le montant réellement reçu. Comparez les montants comme des nombres, pas comme des chaînes : les zéros finaux sont supprimés,49.90arrive donc comme49.9. Si les valeurs ne correspondent pas à la commande, envoyez la facture en vérification manuelle au lieu de la livrer — ces cas sont couverts dans Association des paiements et écarts de montant. - Livrez la commande et marquez-la comme traitée.
Ne vérifiez pas le délai de paiement au moment de la livraison : le paiement peut s'être confirmé on-chain après timeToPayDatetime, et le marchand peut associer un paiement à une facture manuellement. La notification elle-même confirme le paiement.
Et ensuite
- Référence de l'API — listes de factures, annulation, statistiques, soldes
- Vérification de la signature du webhook — la mécanique de la signature en détail
- Association des paiements et écarts de montant — paiement insuffisant, paiement excédentaire, paiements non associés
- Formulaires HTML — accepter les paiements sans l'API