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

topic

string, required

The event to receive. One of the six topics listed under Event topics below.

url

string, required

Where Abra sends the deliveries. It must be a public HTTPS URL.

secret

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 shopDomain field of each delivery tells you which store it's from.

  • Abra returns 400 for http URLs 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

promotions/create

A promotion is created.

promotions/update

A promotion is updated, including when it's deactivated.

promotions/delete

A promotion is deleted.

discounts/create

A discount is created, or a Shopify discount is attached.

discounts/update

A discount is updated.

discounts/delete

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

topic

string

The event topic, for example promotions/update.

shopDomain

string

The store's myshopify.com domain, for example example.myshopify.com.

resource

object

For create and update topics, the full promotion or discount. For delete topics, only its id.

occurredAt

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

Content-Type

Always application/json.

X-Abra-Topic

The event topic, the same as topic in the body.

X-Abra-Webhook-Id

The ID of the subscription. If one endpoint serves several subscriptions with different secrets, use it to pick the secret.

X-Abra-Delivery-Id

A unique ID (UUID) for this delivery.

X-Abra-Delivery-Timestamp

When Abra sent the delivery, as an ISO 8601 timestamp in UTC.

X-Abra-Signature

sha256= followed by the lowercase hex HMAC-SHA256 of the raw body, keyed with your secret. Sent only when the subscription has a secret.

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:

  1. 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.

  2. Compute the HMAC-SHA256 of those bytes with your secret as the key, and hex-encode it.

  3. Compare the result with the value after sha256= in X-Abra-Signature. Use a constant-time comparison, not ==.

  4. 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 "", 200

Delivery 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 status changes to DISABLED and Abra stops sending to it. A success resets the count.

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-Signature header, 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. occurredAt is part of the signed body. Reject deliveries where it's more than a few minutes old. Don't use X-Abra-Delivery-Timestamp for this check, because it isn't signed.

  • Reject repeats. X-Abra-Delivery-Id isn't signed either, so a replayed request can carry a new one. To catch a replay that arrives inside your time window, keep the X-Abra-Signature values 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.