Webhooks
Subscribe a URL and 1UP will POST to it whenever something happens on your account, rather than
you polling for it.
Events
Section titled “Events”| Event | Fires when |
|---|---|
order.created |
An order is placed — through the API, the web app, or Zapier. |
Subscribing
Section titled “Subscribing”curl -X POST https://api.1up.gift/webhooks \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "event": "order.created", "targetUrl": "https://hooks.example.com/1up" }'{ "id": "6f2a1c84-9b3d-4e77-a1f0-23c9e5d8b410", "event": "order.created", "targetUrl": "https://hooks.example.com/1up", "secret": "whsec_7Yk2pQ..."}The target URL must be HTTPS and publicly resolvable. Loopback, private-range and internal addresses
are rejected with a 400 — a tunnel such as ngrok is the way to receive webhooks on a dev machine.
To stop deliveries, DELETE /webhooks/{id} with the id from the response.
What a delivery looks like
Section titled “What a delivery looks like”A POST with a JSON body and two headers:
| Header | |
|---|---|
X-1UP-Event |
The event name, e.g. order.created |
X-1UP-Signature |
sha256=<hex> — an HMAC of the raw body, keyed with your secret |
{ "id": 90412, "event": "order.created", "reference": "8KQ2-WRMT-9P4D", "customerId": 412, "value": 25.00, "currency": "GBP", "country": "GB", "sourceType": "api", "externalReference": "q4-bonus-2026-aisha", "giftSelectionType": "gift", "giftProductId": 184, "giftsCollectionId": null, "wrappingKey": "default", "fromName": "The Northwind team", "deliveryMode": "send", "lineCount": 1, "createdBy": 9031, "creationDate": "2026-10-01T09:14:22Z"}Verifying the signature
Section titled “Verifying the signature”Compute HMAC-SHA256 over the raw request body with your subscription secret, hex-encode it
lowercase, and prefix it with sha256=. Compare against X-1UP-Signature using a
constant-time comparison.
import crypto from 'node:crypto'
function verify(rawBody, header, secret) { const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex')
const a = Buffer.from(expected) const b = Buffer.from(header ?? '')
return a.length === b.length && crypto.timingSafeEqual(a, b)}static bool Verify(string rawBody, string header, string secret){ using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(rawBody)); var expected = "sha256=" + Convert.ToHexString(hash).ToLowerInvariant();
return CryptographicOperations.FixedTimeEquals( Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(header ?? string.Empty));}import hashlib, hmac
def verify(raw_body: bytes, header: str, secret: str) -> bool: expected = "sha256=" + hmac.new( secret.encode(), raw_body, hashlib.sha256 ).hexdigest()
return hmac.compare_digest(expected, header or "")Responding
Section titled “Responding”-
Return 2xx quickly. Any 2xx marks the delivery as done. Acknowledge first and do your work asynchronously — a slow handler will time out and be retried.
-
Return 5xx or 429 to be retried. 1UP backs off exponentially — roughly 4 seconds, then 16, then a minute, capping at 6 hours — with jitter, for up to 12 attempts before giving up.
-
Return 410 Gone to unsubscribe. 1UP deletes the subscription. Use this when the endpoint is permanently retired; any other 4xx just drops that one delivery.
Expect duplicates
Section titled “Expect duplicates”Delivery is at-least-once. A handler that times out after doing its work will be retried, and you
will see the same event again. Make your handler idempotent — the id field is stable per order, so
it works as a deduplication key.
Polling instead
Section titled “Polling instead”If you cannot host a public endpoint, GET /orders returns your most recent orders in the same
shape as the order.created payload, newest first. Webhooks are the better option where you have
the choice.
