Connect Zapier or n8n

Any tool that can send an HTTP request can use the API. Set the step up like this:

  • Method and URL: e.g. POST and https://serveu-psi.vercel.app/api/v1/cards/lookup.
  • Header Authorization: Bearer serveu_live_XXXX…, kept in the tool's credentials when it has them (in n8n, a Header Auth credential), never typed into a step's visible text.
  • Header Content-Type: application/json and a JSON body.
  • Header Idempotency-Key built from the trigger's own id (a row id, a form submission id), so a re-run of the same trigger never adds stamps twice.

Example: a row added to Google Sheets adds stamps

  1. Trigger: new row with a phone number and a receipt number.
  2. Find the card:
POST https://serveu-psi.vercel.app/api/v1/cards/lookup
Authorization: Bearer serveu_live_XXXX…
Content-Type: application/json

{"phone":"{{row.phone}}"}
  1. Add the stamps with the card's id from the answer:
POST https://serveu-psi.vercel.app/api/v1/cards/{{lookup.id}}/earn
Authorization: Bearer serveu_live_XXXX…
Content-Type: application/json
Idempotency-Key: sheet-row-{{row.id}}

{"stamps":1,"reference":"{{row.receipt}}"}

The key needs Give stamps and points. A 404 on the lookup means the phone has no card yet: stop there, or add a step that joins the customer (POST /api/v1/cards).

Example: post yesterday's sales to Slack every morning

  1. Trigger: a schedule, every day at 07:00 Riyadh time, once yesterday is over.
  2. Get yesterday's summary: from and to are both yesterday's date (dates are in the business's time zone and both are included). On 9 November:
GET https://serveu-psi.vercel.app/api/v1/orders/summary?from=2026-11-08&to=2026-11-08
Authorization: Bearer serveu_live_XXXX…
  1. Post a message with orders and sales. Amounts are in halalas: divide by 100 for SAR.

The key needs See orders and sales (orders:read).

To start a Zap or workflow when something happens in ServeU (a new order, a stamp added), use webhooks: see the next section.

Trigger a Zap or workflow from ServeU

  1. Create the trigger: in Zapier a Webhooks by Zapier "Catch Hook", in n8n a Webhook trigger node set to POST (use its production URL and activate the workflow). Copy its https:// URL.
  2. Ask the owner to add it in Settings → Developers → Webhooks with the events you need (or create it with the API: see Webhooks).
  3. Press Send test event, then a real one (place a test order, add a stamp) so the tool can map fields such as data.object.number or data.object.balance. The test event is a ping: filter it out (a step that continues only when type isn't ping).

These tools don't check Standard Webhooks signatures by themselves, so anyone who learns the URL could trigger the workflow:

  • Keep the hook URL as secret as a key, and give each workflow its own.
  • In n8n, you can verify in a Code node. It needs the body exactly as it arrived (turn on the Webhook node's raw body option) and the function below — the same as the Node sample in Webhooks, written with require as Code nodes need. On self-hosted n8n, allow the module with NODE_FUNCTION_ALLOW_BUILTIN=crypto. Option names differ between n8n versions; check n8n's own docs if yours don't match. Stop the workflow when it returns false.
  • Don't turn on Include contact details for a Zapier or n8n endpoint unless the workflow needs the customer's name and phone: each run's data is kept in the tool's history.
const { createHmac, timingSafeEqual } = require("crypto");

function verifyServeU(rawBody, headers, secret) {
  const id = headers["webhook-id"], ts = headers["webhook-timestamp"], sigs = headers["webhook-signature"];
  if (!id || !ts || !sigs || !/^\d+$/.test(ts)) return false;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // 5 minutes
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = Buffer.from(createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest("base64"));
  return sigs.split(" ").some((s) => {
    const [version, value] = s.split(",");
    const got = Buffer.from(value ?? "");
    return version === "v1" && got.length === expected.length && timingSafeEqual(got, expected);
  });
}

For a regular schedule instead (a report every morning), poll a list: see Pagination, filtering & sync.

Next: Data & privacy →