Webhooks
Updated:
Instead of polling, you can receive the events of an order on your HTTPS endpoint. Webhooks carry the same events as Order events. The event log stays available with or without webhooks, so you can always fall back to polling.
Set up
Pass
callback_urlin Place an order, or set it later with Update order.The response returns
webhook_secret(whsec_…) once. Store it: it signs every webhook.Changing
callback_urlissues a new secret. The previous secret keeps signing webhooks for 24 hours: during this timeX-PS-Signaturecarries two signatures.
Request
Property | Value |
|---|---|
Method |
|
Headers |
|
Body | One event, as in Order events; |
Success | Any |
Delivery | At least once, in no guaranteed order. Deduplicate by |
POST /hooks/proxy HTTP/1.1
Host: agent.example.com
Content-Type: application/json; charset=utf-8
User-Agent: ProxySeller-Webhooks/1
X-PS-Timestamp: 1762077838
X-PS-Signature: v1=5f0c2a…9e
{"event_id":"evt_01JZ8QA2C4E6G8J0K2M4P6R8T0","type":"order.active","created_at":"2026-11-02T10:03:58Z","api_version":"v1","order_id":"ord_01JZ8Q7K5N2R4T6V8X0Z2B4D6F","wallet":"0x857b06519e91e3a54538791bdbb0e22373e36b66","attempt":1,"data":{"lines":[{"line_id":"1","status":"active","delivered":2,"qty":2}],"expires_at":"2026-12-02T23:59:59Z"}}Verify the signature
X-PS-Signature: v1=hex(HMAC-SHA256(key, timestamp + "." + raw_body))
key— the bytes (UTF-8) of the wholewebhook_secretstring, including thewhsec_prefix.timestamp— the value ofX-PS-Timestamp, Unix seconds.raw_body— the body exactly as received, before any JSON parsing.During 24 hours after a secret change the header is
v1=<current>,v1=<previous>: accept the request if any of them matches.Reject requests whose timestamp differs from your clock by more than 300 seconds.
Python
import hashlib
import hmac
import time
def verify(secret: str, timestamp: str, signature_header: str, raw_body: bytes, tolerance: int = 300) -> bool:
if abs(time.time() - int(timestamp)) > tolerance:
return False
expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
candidates = [part.strip()[3:] for part in signature_header.split(",") if part.strip().startswith("v1=")]
return any(hmac.compare_digest(expected, candidate) for candidate in candidates)Node.js
import crypto from 'node:crypto';
function verify(secret, timestamp, signatureHeader, rawBody, toleranceSeconds = 300) {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) return false;
const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest('hex');
return signatureHeader.split(',').map((s) => s.trim()).filter((s) => s.startsWith('v1='))
.some((s) => s.length === expected.length + 3
&& crypto.timingSafeEqual(Buffer.from(s.slice(3)), Buffer.from(expected)));
}Retries
Rule | Value |
|---|---|
First attempt | Not earlier than 30 seconds after the event |
Retries | After 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h: 8 attempts in about 44.6 hours |
Too old | An event older than about 45.6 hours is not sent anymore |
Circuit breaker | After 3 failures in a row the other events of the order wait for the next attempt |
Disabling | If the last attempt of an event fails and the URL has not accepted any webhook since that event was created, delivery to the URL is disabled. The order itself is not affected |
To enable a disabled URL, send it again with Update order: the same URL at most once per hour, a new URL at any time. Missed events are always in Order events.
callback_url rules
Checked when the quote is issued and in PATCH. A violation returns 400 invalid_request:
Rule | detail |
|---|---|
Printable ASCII, up to 2048 characters |
|
|
|
No login or password in the URL |
|
No fragment |
|
Port 443 only |
|
A public DNS name, or a canonical IPv4 address, or an IPv6 address in brackets |
|
All addresses of the host are public |
|
Not a host of the service itself |
|
DNS is checked again before every delivery; the request goes only to the checked addresses.