Webhooks¶
Instead of polling, subscribe to events and HugeProfit will POST them to your URL in real time. Deliveries are queued durably, signed, and retried on failure.
How it works¶
- You create a subscription: an event + a target URL (+ an optional secret).
- When that event happens anywhere in the system, we enqueue a delivery.
- A dispatcher POSTs the signed payload to your URL.
- On any non-2xx / network error we retry with backoff, up to 5 attempts.
Event catalog¶
GET /webhooks/events returns the live list. Current events:
| Event | Fires when |
|---|---|
sms |
an SMS was sent to a customer |
order_create |
a sale was created |
order_update |
a sale was changed: edited, its status changed (delivery tracking included — the parcel arrived, was received, returned), a waybill was added |
order_delete |
a sale was deleted |
product_update |
a product was created or updated |
client_update |
a client was created or updated |
stock_update |
product stock changed |
order_* = sales
The order_* events fire on the sale lifecycle (/sales), not on marketplace orders
(/orders).
Subscriptions¶
| Method | Path | Scope |
|---|---|---|
GET |
/webhooks |
webhooks:read |
POST |
/webhooks |
webhooks:write |
GET |
/webhooks/{id} |
webhooks:read |
PUT |
/webhooks/{id} |
webhooks:write |
DELETE |
/webhooks/{id} |
webhooks:write |
GET |
/webhooks/events |
webhooks:read |
GET |
/webhooks/deliveries |
webhooks:read |
POST |
/webhooks/test |
webhooks:write |
Create:
curl -X POST https://crm.h-profit.com/bapi-v2/webhooks \
-H "Authorization: hp_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "event": "stock_update", "url": "https://yoursite.com/hooks/stock",
"description": "stock sync" }'
The response returns the generated secret once — store it, you need it to verify
signatures. Later listings only show a masked secret_hint.
{
"id": 5, "event": "stock_update", "url": "https://yoursite.com/hooks/stock",
"is_active": true, "description": "stock sync",
"has_secret": true, "secret_hint": "whsec_0L2T1…",
"secret": "whsec_0L2T1ARi6Ug8C7VBea-e0NJ-…", "created_at": 1784556220
}
You may pass your own secret, custom headers (object), and is_active. PUT updates any
of these; passing secret rotates it.
url must be an http(s) address on the public internet: localhost, private networks
(10.*, 192.168.*, …) and link-local addresses are rejected with 422; a domain that resolves
to such an address fails at delivery time. Redirects are not followed — a 3xx counts as a failed
attempt.
Delivery payload¶
Every delivery POSTs this JSON body:
{
"id": "b3f1…-uuid",
"event": "stock_update",
"created_at": 1784556220,
"data": { "products": [ { "pid": 123, "mid": 8562, "stock": 7, "price": "10.00" } ] }
}
With these headers:
| Header | Meaning |
|---|---|
X-GP-Event |
the event name |
X-GP-Delivery |
delivery UUID (also data.id) — use it for idempotency |
X-GP-Signature |
signature (if the subscription has a secret) |
data shape per event: stock_update → { products: [...] }; order_* →
{ id, action, order_number, status, tracking_number, external_id }; client_update → { id, action, phone, name };
product_update → { id, action, name } or { action: "imported", count, source };
sms → { phone, order_id, order_status, message }.
action of order_update: status_changed (with previous_status) or updated; after an edit the
sale has a new id, and previous_id carries the old one. external_id is your order number given on
creation (null if none):
{ "id": 4605, "action": "status_changed", "order_number": "20072602", "status": "arrived",
"previous_status": "delivering", "tracking_number": "20450000000001", "external_id": "site-1001" }
Verifying the signature¶
When the subscription has a secret, we sign the raw body:
v1 is HMAC_SHA256(secret, "<t>.<raw_body>"). To verify:
- Parse
tandv1from the header. - Recompute
HMAC_SHA256(secret, t + "." + raw_request_body). - Compare in constant time; reject if it differs.
- Reject if
tis older than ~5 minutes (replay protection).
import hmac, hashlib, time
def verify(secret, raw_body, header, tolerance=300):
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts["t"])
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.{raw_body}".encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
Retries & backoff¶
Respond with 2xx to acknowledge. Any other status (or a timeout/network error) triggers a retry:
| Attempt | Wait before next |
|---|---|
| 1 → 2 | 1 minute |
| 2 → 3 | 5 minutes |
| 3 → 4 | 15 minutes |
| 4 → 5 | 30 minutes |
| after 5 | marked dead |
Delivery timeout is 15 seconds. Delivery is at-least-once — deduplicate on X-GP-Delivery.
Delivery journal¶
GET /webhooks/deliveries returns delivery history (filter by webhook_id, status):
{
"id": "b3f1…-uuid", "webhook_id": 5, "event": "stock_update",
"status": "success", "attempt": 1, "max_attempts": 5,
"response_code": 200, "error": null, "duration_ms": 84,
"next_retry_at": null, "created_at": "2026-07-20T…"
}
status is one of pending, success, failed (will retry), dead (gave up).
Testing an endpoint¶
POST /webhooks/test delivers a sample payload synchronously and returns the raw result —
great while wiring up your receiver. Pass an existing webhook_id, or an ad-hoc url (+ optional
secret, event, data):
curl -X POST https://crm.h-profit.com/bapi-v2/webhooks/test \
-H "Authorization: hp_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "webhook_id": 5, "data": { "ping": 1 } }'
{ "ok": true, "response_code": 200, "response_body": "…", "duration_ms": 84,
"sent_signature": "t=…,v1=…", "sent_body": { /* the envelope we sent */ } }
Best practices¶
- Verify the signature on every request.
- Return
2xxfast (within 15s); do heavy work asynchronously. - Deduplicate on
X-GP-Delivery— the same event may arrive more than once. - A new subscription can take up to ~30s to become active across all producers.