Adyvance Developers

Webhooks

Adyvance tells your server when something happens in a shop, such as an order placed, paid or shipped. Each event is signed, and is sent again until your server accepts it.

Add an endpoint

In the admin, open Apps → Developer → Webhooks, choose Add endpoint, enter your https:// address and pick the events. Copy the signing secret (whsec_…); it is shown once. Send test sends a signed webhook.test event so you can check your code.

You can also add one through the API, with a key that has webhooks.manage:

curl -X POST "https://app.adyvance.com/_public/v1/orgs/org_…/webhooks" \
  -H "Authorization: Bearer $ADYVANCE_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/adyvance",
    "events": ["order.created", "order.status_changed", "payment.state_changed"],
    "description": "Accounting sync"
  }'

The answer includes the secret once. Use ["*"] as the events to get every event. Webhooks are part of the Business plan.

What arrives

Each event is a POST with a JSON body:

Body
{
  "id": "evt_…",
  "type": "order.status_changed",
  "api_version": "2026-10-01",
  "org_id": "org_…",
  "store_id": "str_…",
  "occurred_at": "2026-11-05T09:20:03.000Z",
  "resource": { "type": "order", "id": "ord_…", "seq": 4 },
  "livemode": true,
  "data": {
    "object": { "id": "ord_…", "number": "1048", "status": "shipped", "…": "…" }
  }
}
FieldMeaning
idThe event's unique ID. The same event sent twice has the same ID.
typeWhat happened (see events).
resourceWhat it happened to, and seq, a number that goes up with each event for that same thing.
livemodefalse for events caused through a test key.
data.objectThe thing as it is after the change.

And these headers:

Headers
POST /hooks/adyvance HTTP/1.1
Content-Type: application/json
User-Agent: Adyvance-Webhooks/1
Ady-Webhook-Id: evt_…
Ady-Webhook-Timestamp: 1793870403000
Ady-Signature: t=1793870403000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Ady-Resource: order:ord_…;seq=4

The payload follows the same rules as the API: no secrets, no product costs unless the person who made the endpoint may see costs, and customers' phone numbers, emails and street addresses masked unless they may see contact details.

Check the signature

Ady-Signature is t=<time in milliseconds>,v1=<signature>. The signature is a hex HMAC-SHA256, made with your signing secret, of the time, a dot, and the raw body:t + "." + body. To check it:

  1. Read the raw body exactly as it arrived. Parsing and re-encoding JSON changes the bytes and breaks the check.
  2. Make the same HMAC with your secret and compare it to each v1 value, in constant time.
  3. Refuse it if t is more than 5 minutes from your clock. This stops old events being replayed.
verify.js (Node.js)
import { createHmac, timingSafeEqual } from "node:crypto";

const TOLERANCE_MS = 5 * 60 * 1000;

/**
 * Checks an Adyvance webhook. `rawBody` must be the exact bytes received,
 * as a string, before any JSON parsing.
 */
export function verifyAdyvanceWebhook(rawBody, signatureHeader, secret) {
  let t = null;
  const v1 = [];
  for (const part of (signatureHeader ?? "").split(",")) {
    const [k, v] = part.split("=", 2).map((s) => s?.trim());
    if (k === "t") t = Number(v);
    if (k === "v1" && v) v1.push(v);
  }
  if (!t || v1.length === 0) return false;
  if (Math.abs(Date.now() - t) > TOLERANCE_MS) return false; // too old or from the future

  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  // During a secret rotation there are two v1 values: accept either.
  return v1.some((hex) => {
    const got = Buffer.from(hex, "hex");
    return got.length === expected.length && timingSafeEqual(got, expected);
  });
}
server.js (Express)
import express from "express";
import { verifyAdyvanceWebhook } from "./verify.js";

const app = express();
const seen = new Set(); // use your database in production

app.post("/hooks/adyvance", express.text({ type: "application/json" }), (req, res) => {
  const ok = verifyAdyvanceWebhook(
    req.body,
    req.get("Ady-Signature"),
    process.env.ADYVANCE_WEBHOOK_SECRET,
  );
  if (!ok) return res.sendStatus(400);

  const event = JSON.parse(req.body);
  res.sendStatus(200); // answer first, then work

  if (seen.has(event.id)) return; // delivered before
  seen.add(event.id);
  queueWork(event); // your own background job
});
WebCrypto version
// Cloudflare Workers, Deno, Bun or any runtime with WebCrypto
async function verify(rawBody, header, secret) {
  const parts = Object.groupBy(header.split(","), (p) => p.split("=")[0]);
  const t = Number(parts.t?.[0]?.slice(2));
  const sigs = (parts.v1 ?? []).map((p) => p.slice(3));
  if (!t || !sigs.length || Math.abs(Date.now() - t) > 300_000) return false;

  const key = await crypto.subtle.importKey(
    "raw", new TextEncoder().encode(secret),
    { name: "HMAC", hash: "SHA-256" }, false, ["sign"],
  );
  const mac = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${t}.${rawBody}`));
  const hex = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, "0")).join("");
  return sigs.some((s) => sameHex(s, hex));
}

// Constant-time compare, so the time taken says nothing about the secret.
function sameHex(a, b) {
  if (a.length !== b.length) return false;
  let diff = 0;
  for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
  return diff === 0;
}

When you choose New signing secret in the admin, Adyvance signs with both the old and the new secret for 24 hours, so the header carries two v1 values. Accept either, and switch your server to the new secret within that time.

Answer quickly

  • Answer any 2xx status as soon as the signature checks out, then do the work in the background. Anything else, or no answer, counts as a failure.
  • The same event can arrive more than once. Keep the ids you have handled and skip repeats.
  • Your endpoint must use https:// on a public address. Private and local addresses are refused.

Retries

When a delivery fails, Adyvance waits and tries again: after about 30 seconds, 2 minutes, 10 minutes, 30 minutes, 1 hour and 2 hours, then every 3 hours. While your server is failing, other events for that endpoint wait too, so one outage does not flood you when you are back.

  • An event not delivered within 24 hours stops being retried. You can still send it from the endpoint's Deliveries list in the admin with Send again.
  • After 20 failures in a row, the endpoint is paused (Paused after failures) and the shop's audit log records it. Turn it back on in the admin once your server is fixed; waiting events then go out.

Order of events

Events about the same thing (the same order, product or customer) are delivered in order: the next one waits until the one before it has been delivered. Events about different things can arrive in any order. If you need to be sure, compare resource.seq, also sent in the Ady-Resource header, and ignore an event whose seq is lower than one you already handled.

Events

Subscribe to the ones you need, or to * for all of them.

Orders

order.createdAn order was placed, from any channel.
order.confirmedAn order was confirmed.
order.status_changedAn order moved to another status.
order.editedAn order's lines, address, note or tags changed.
order.cancelledAn order was cancelled.

Payments and money

payment.state_changedAn order's payment status changed. The new payment_status is in the object.
payment_session.expiredA started online payment ran out of time.
refund.createdA refund was made.
invoice.issuedAn invoice was issued.
cod.remittance_postedA courier's cash-on-delivery payout was recorded.
settlement.matchedA payment provider's settlement was matched to orders.
wallet.balance_changedThe shop's Adyvance Wallet balance changed.
subscription.changedThe shop's plan changed.

Delivery and returns

shipment.bookedA parcel was booked with a courier.
shipment.state_changedA parcel's courier status changed.
return.createdA return was opened.
return.refundedA return was refunded.
return.closedA return was closed.

Products and stock

product.createdA product was created.
product.updatedA product changed.
product.deletedA product was deleted.
inventory.level_changedA variant's stock level changed at a location.
inventory.movementStock moved in or out (a sale, adjustment, return, transfer).
inventory.transfer_sentA stock transfer left a location.
inventory.transfer_receivedA stock transfer arrived.
stock_alert.triggeredA variant fell to its low-stock alert.

Customers

customer.createdA customer was added.
customer.updatedA customer's details changed.
customer.anonymizedA customer's personal data was erased on request. Erase your copy too.
consent.changedA customer changed their marketing consent.

Point of sale

pos.sale_completedA sale was completed at the counter.
pos.shift_closedA POS shift was closed.

Jobs

import.completedA product or customer import finished.
export.completedAn export file is ready.
campaign.completedAn SMS or email campaign finished sending.