Verifying Abra webhooks
Last updated: September 29, 2026
Abra can send a POST request to your server when a promotion or discount in a store is created, updated or deleted. This guide shows how to subscribe, what each request contains, how to check that it comes from Abra, and how deliveries behave.
Beta. The Abra API is in beta, so its behavior may change. If something doesn't work as described, contact us at help@abrapromotions.com.
Subscribe to events
Create a subscription with POST /v1/partner/webhooks. Each subscription sends one topic to one URL.
Name | Description |
|---|---|
string, required | The event to receive. One of the six topics listed under Event topics below. |
string, required | Where Abra sends the deliveries. It must be a public HTTPS URL. |
string | Used to sign each delivery. Optional, but without it (or with an empty string) deliveries aren't signed. Abra never returns it, so keep a copy. |
The example uses your store's API token and the $ABRA_API_BASE and $ABRA_API_TOKEN variables from Getting started with the Abra API.
curl -X POST "$ABRA_API_BASE/v1/partner/webhooks" \
-H "Authorization: Bearer $ABRA_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"topic": "promotions/update",
"url": "https://example.com/webhooks/abra",
"secret": "your-webhook-secret"
}'Abra responds with 201 and the subscription in data, including its id and a status of ACTIVE. Every delivery carries the same id in the X-Abra-Webhook-Id header.
To receive several topics, create one subscription per topic. They can all use the same URL.
Subscriptions belong to the store whose token you use. With several stores, subscribe once per store. The
shopDomainfield of each delivery tells you which store it's from.Abra returns
400forhttpURLs and for URLs that point to private or reserved IP addresses. To test from your own machine, use a tunnel that gives you a public HTTPS URL.There's no sandbox. To test, subscribe on a development store, then create or change a promotion there.
You can't edit a subscription. To change its URL or secret, delete it and create a new one.
Generate the secret with a secure random generator, for example
openssl rand -hex 32.
For the list, get and delete endpoints and their errors, see Abra API: Webhooks.
Event topics
Topic | Sent when |
|---|---|
| A promotion is created. |
| A promotion is updated, including when it's deactivated. |
| A promotion is deleted. |
| A discount is created, or a Shopify discount is attached. |
| A discount is updated. |
| A discount is deleted. |
One request can send more than one event. For example, updating a discount that belongs to a promotion can also send promotions/update for that promotion. You also receive events for the changes your own integration makes.
Payload
Each delivery is a POST request with a JSON body that has four fields.
Name | Description |
|---|---|
string | The event topic, for example |
string | The store's myshopify.com domain, for example |
object | For create and update topics, the full promotion or discount. For delete topics, only its |
string | When Abra sent the delivery, as an ISO 8601 timestamp in UTC. |
For create and update topics, resource has the same fields the API returns in data for that promotion or discount. See Abra API: Promotions and Abra API: Discounts.
This is the body of a promotions/delete delivery, formatted for reading. Abra sends it as compact JSON on one line.
{
"topic": "promotions/delete",
"shopDomain": "example.myshopify.com",
"resource": {
"id": "8c1f5b2e-3d4a-4f6b-9e2d-7a1c5b3e9f01"
},
"occurredAt": "2026-09-29T15:04:05.123Z"
}Headers
Header | Description |
|---|---|
| Always |
| The event topic, the same as |
| The ID of the subscription. If one endpoint serves several subscriptions with different secrets, use it to pick the secret. |
| A unique ID (UUID) for this delivery. |
| When Abra sent the delivery, as an ISO 8601 timestamp in UTC. |
|
|
Only the body is signed. The headers, including X-Abra-Delivery-Timestamp, aren't covered by the signature.
Verify the signature
Check the signature of every delivery before you act on it:
Read the raw request body as bytes, before any JSON parsing. Many frameworks parse JSON automatically, so turn that off for this route. Parsing and re-encoding the JSON can change the bytes, and then the signature no longer matches.
Compute the HMAC-SHA256 of those bytes with your secret as the key, and hex-encode it.
Compare the result with the value after
sha256=inX-Abra-Signature. Use a constant-time comparison, not==.If the header is missing or the values differ, reject the request, for example with
401.
Node.js
This example uses Express. express.raw() gives you the body as the bytes Abra sent.
const crypto = require('node:crypto');
const express = require('express');
const app = express();
const secret = process.env.ABRA_WEBHOOK_SECRET;
function isValidSignature(rawBody, header) {
if (!Buffer.isBuffer(rawBody) || typeof header !== 'string') return false;
if (!header.startsWith('sha256=')) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
const received = Buffer.from(header.slice('sha256='.length));
const computed = Buffer.from(expected);
return (
received.length === computed.length &&
crypto.timingSafeEqual(received, computed)
);
}
app.post(
'/webhooks/abra',
express.raw({ type: 'application/json', limit: '10mb' }),
(req, res) => {
if (!isValidSignature(req.body, req.get('X-Abra-Signature'))) {
return res.sendStatus(401);
}
res.sendStatus(200); // respond first, then do the work
const event = JSON.parse(req.body.toString('utf8'));
enqueue(event); // your code
},
);
app.listen(3000);Python
This example uses Flask. request.get_data() returns the body as the bytes Abra sent.
import hashlib
import hmac
import json
import os
from flask import Flask, request
app = Flask(__name__)
SECRET = os.environ["ABRA_WEBHOOK_SECRET"].encode()
def is_valid_signature(raw_body, header):
if not header or not header.startswith("sha256="):
return False
expected = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
received = header[len("sha256="):]
return hmac.compare_digest(received.encode(), expected.encode())
@app.post("/webhooks/abra")
def abra_webhook():
raw_body = request.get_data() # the exact bytes Abra sent
signature = request.headers.get("X-Abra-Signature")
if not is_valid_signature(raw_body, signature):
return "", 401
enqueue(json.loads(raw_body)) # your code; keep it fast
return "", 200Delivery rules
Rule | Behavior |
|---|---|
Attempts | One per event and subscription. Failed deliveries aren't retried. |
Timeout | Abra waits up to 5 seconds for your response. |
Success | Any 2xx status. |
Failure | Any other status, a timeout or a connection error. |
Redirects | Not followed. A 3xx response is a failure, so register the final URL. |
Auto-disable | After 4 failures in a row, the subscription's |
Re-enable | Not possible. Delete the subscription and create it again. |
Abra counts failures in the subscription's consecutiveFailures field. To check your subscriptions, call GET /v1/partner/webhooks. Each one shows its status, consecutiveFailures and lastDeliveryAt, the time of its last successful delivery.
Until you delete a disabled subscription, creating one with the same topic and URL returns 409 with the code CONFLICT.
Good practice
Always set a secret. Without one, deliveries have no
X-Abra-Signatureheader, and you can't tell them apart from forged requests.Respond fast. Return a 2xx as soon as the signature checks out, and do the work afterwards, for example from a queue. A slow handler hits the 5-second limit, and 4 failures in a row disable the subscription.
Reject old deliveries.
occurredAtis part of the signed body. Reject deliveries where it's more than a few minutes old. Don't useX-Abra-Delivery-Timestampfor this check, because it isn't signed.Reject repeats.
X-Abra-Delivery-Idisn't signed either, so a replayed request can carry a new one. To catch a replay that arrives inside your time window, keep theX-Abra-Signaturevalues you accepted during that window and reject any repeat.Don't rely on webhooks alone. Failed deliveries aren't sent again, and Abra doesn't guarantee the order of deliveries. When the current state matters, read it from the API, and check from time to time that your subscriptions are still
ACTIVE.