Skip to content

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

  1. You create a subscription: an event + a target URL (+ an optional secret).
  2. When that event happens anywhere in the system, we enqueue a delivery.
  3. A dispatcher POSTs the signed payload to your URL.
  4. 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:

X-GP-Signature: t=1784556220,v1=3f8a...hex

v1 is HMAC_SHA256(secret, "<t>.<raw_body>"). To verify:

  1. Parse t and v1 from the header.
  2. Recompute HMAC_SHA256(secret, t + "." + raw_request_body).
  3. Compare in constant time; reject if it differs.
  4. Reject if t is 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 2xx fast (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.