Webhooks
Webhooks allow you to receive real-time notifications about events. When an event occurs (e.g., an invoice status changes), iBuy sends an HTTP POST request to your configured URL with event details.
How It Works
- You configure a webhook URL in your shop settings or via the
hook_urlfield when creating an invoice. - When a relevant event occurs, iBuy sends a
POSTrequest to your URL. - Your server must respond with HTTP
200or204to acknowledge receipt. - If delivery fails, iBuy will retry up to 5 times.
WARNING
Events may be delivered out of order. If you need to process them sequentially, use the event_created_at field to sort them.
Authentication
Every webhook request includes an Authorization header with your shop's API token hash:
Authorization: Bearer <token_hash>You should verify this token hash matches your expected value to ensure the request is genuinely from iBuy.
The token hash is a SHA-256 hex digest of your API key. To verify the webhook, compute the hash of your API key and compare it with the Authorization header value.
const crypto = require("crypto");
function hashToken(token) {
return crypto.createHash("sha256").update(token).digest("hex");
}import hashlib
def hash_token(token: str) -> str:
return hashlib.sha256(token.encode()).hexdigest()import (
"crypto/sha256"
"encoding/hex"
)
func HashToken(token string) string {
sum := sha256.Sum256([]byte(token))
return hex.EncodeToString(sum[:])
}Request Format
All webhook requests are sent as POST with Content-Type: application/json. The body has the following structure:
{
"shop_id": "uuid-of-your-shop",
"type": "event_type",
"body": { },
"event_created_at": 1700000000000
}| Field | Type | Description |
|---|---|---|
shop_id | string (UUID) | Your identifier |
type | string | The event type (see Event Types below) |
body | object | Event-specific payload |
event_created_at | number | Unix timestamp in milliseconds when the event was created |
Event Types
invoice_status_change
DANGER
For ASSIGNED status we are returning invoice_requisites_change event instead because requisites are changed as well.
Sent when the status of an invoice changes (e.g., from PENDING to PAID_BY_USER).
Body:
{
"id": "invoice-uuid",
"external_id": "inv-ext-001",
"requisite_id": "requisite-uuid",
"status": "PAID_BY_USER",
"amount": "1499.00",
"created_at": "2025-12-01T12:00:00Z",
"expires_at": "2025-12-01T12:30:00Z",
"updated_at": "2025-12-01T12:05:00Z",
"metadata": {
"user_id": "user_42",
"cart_id": "cart_777"
}
}| Field | Type | Description |
|---|---|---|
id | string (UUID) | Invoice identifier |
external_id | string or null | Your external reference (if provided during creation) |
requisite_id | string (UUID) or null | Payment requisite identifier (may be null if not yet assigned) |
status | string | New invoice status (see Invoice Statuses) |
amount | string | Invoice amount as a decimal string |
created_at | string | Invoice creation timestamp (ISO 8601) |
expires_at | string | Invoice expiration timestamp (ISO 8601) |
updated_at | string | Last update timestamp (ISO 8601) |
metadata | object or null | Custom metadata you provided when creating the invoice |
invoice_requisites_change
Sent when a payment method (requisite) is assigned or changed for an invoice.
Body:
{
"id": "invoice-uuid",
"external_id": "inv-ext-001",
"requisite_id": "requisite-uuid",
"status": "ASSIGNED",
"amount": "1499.00",
"created_at": "2025-12-01T12:00:00Z",
"expires_at": "2025-12-01T12:30:00Z",
"updated_at": "2025-12-01T12:03:00Z",
"metadata": {
"user_id": "user_42",
"cart_id": "cart_777"
}
}| Field | Type | Description |
|---|---|---|
id | string (UUID) | Invoice identifier |
external_id | string or null | Your external reference (if provided during creation) |
requisite_id | string (UUID) | Payment requisite identifier (always present for this event) |
status | string | Current invoice status (see Invoice Statuses) |
amount | string | Invoice amount as a decimal string |
created_at | string | Invoice creation timestamp (ISO 8601) |
expires_at | string | Invoice expiration timestamp (ISO 8601) |
updated_at | string | Last update timestamp (ISO 8601) |
metadata | object or null | Custom metadata you provided when creating the invoice |
Invoice Statuses
See Invoice Statuses for the full list of statuses and their flow.
Responding to Webhooks
Your endpoint must return one of the following HTTP status codes to acknowledge the webhook:
200 OK204 No Content
Any other status code is treated as a delivery failure and will trigger a retry.
WARNING
Your webhook endpoint must respond within 2 seconds. If the request times out, it will be treated as a failure and retried.
Retry Policy
If webhook delivery fails (timeout, non-200/204 response, or network error), iBuy will retry the delivery up to 5 times with increasing delays between attempts.
TIP
Make sure your webhook handler is idempotent — the same event may be delivered more than once in case of retries.
Example: Handling a Webhook
const express = require("express");
const app = express();
app.use(express.json());
app.post("/webhooks/ibuy", (req, res) => {
const { shop_id, type, body, event_created_at } = req.body;
// Verify the authorization token
const token = req.headers["authorization"]?.replace("Bearer ", "");
if (token !== process.env.IBUY_WEBHOOK_TOKEN) {
return res.status(401).send();
}
switch (type) {
case "invoice_status_change":
console.log(`Invoice ${body.id} status changed to ${body.status}`);
// Handle status change...
break;
case "invoice_requisites_change":
console.log(`Invoice ${body.id} requisites updated`);
// Handle requisites change...
break;
}
res.status(204).send();
});
app.listen(3000);from flask import Flask, request
app = Flask(__name__)
@app.route("/webhooks/ibuy", methods=["POST"])
def handle_webhook():
token = request.headers.get("Authorization", "").replace("Bearer ", "")
if token != EXPECTED_TOKEN:
return "", 401
data = request.json
event_type = data["type"]
body = data["body"]
if event_type == "invoice_status_change":
print(f"Invoice {body['id']} status changed to {body['status']}")
# Handle status change...
elif event_type == "invoice_requisites_change":
print(f"Invoice {body['id']} requisites updated")
# Handle requisites change...
return "", 204