# Connect Zapier to Atlas Labs

> To connect Zapier to Atlas Labs, use Webhooks by Zapier: a **Catch Raw Hook** trigger receives events at a `hooks.zapier.com` URL that you subscribe with `POST /webhooks`, and a **Custom Request** action calls the REST API with an `Authorization: Bearer` header. Zapier does not verify the Standard Webhooks signature for you, so this page includes the Code by Zapier step that does.

Canonical: `https://automationatlas.io/labs/docs/tools/zapier/` · Checked: `2026-10-15`

Atlas Labs is a fictional company's API; every figure in every response is simulation data.

*Reference table — Atlas Labs API fields and Zapier's documented app names, not part of the fictional simulation. Zapier help centre checked `15 Oct 2026`.*

| At a glance | Value |
| --- | --- |
| Receive events | Webhooks by Zapier → **Catch Raw Hook** (keeps headers and the raw body; **Catch Hook** parses the body and drops the headers) |
| Call the API | Webhooks by Zapier → **Custom Request**; or the premium beta app API by Zapier → **API Request** |
| Where the key goes | Headers section: `Authorization` = `Bearer lab_off_YOUR_KEY` (Head Office) or `Bearer lab_fac_YOUR_KEY` (Factory) |
| Body | Data field, raw JSON, with header `Content-Type: application/json` |
| `Idempotency-Key` | The `webhook-id` header from the Catch Raw Hook (it equals the event `id`, `evt_…`), or the `id` that the Code by Zapier step outputs after parsing the raw body |
| Webhook host | `hooks.zapier.com` is a recognised platform host with a higher delivery allowance |
| Plan | Webhooks by Zapier is on Professional, Team and Enterprise, not Free; API by Zapier is premium and beta (Zapier help) |
| OpenAPI import | None found in Zapier's documentation; use the HTTP steps |

> **Start a sandbox and your keys are filled in for you.** This page uses `lab_off_YOUR_KEY` and `lab_fac_YOUR_KEY`. After you [start a sandbox](https://automationatlas.io/labs/#start-sandbox-heading), the “Connect it” panel's Zapier tab shows these field values with your real keys and a copy button.

- Atlas Labs → Your Zap: signed webhook, POST, event JSON
- Your Zap → Atlas Labs: Custom Request, Bearer key
- Your Zap → Your apps: action

Both legs are optional: a Zap can only listen, only call, or do both.

## How do I receive Atlas Labs events in a Zap?

1. **Create a Zap** → Trigger: **Webhooks by Zapier** → event **Catch Raw Hook** → Continue. It hands later steps exactly two things, the unparsed body and the headers, so fields such as `id` and `data.sales_order_id` appear only after the Code step in the next section parses the body. Pick **Catch Hook** instead only if you will not verify the signature: it parses the body for you but discards the headers and the raw bytes.
2. **Copy the webhook URL** Zapier shows, of the form `https://hooks.zapier.com/hooks/catch/…/…/`.
3. **Subscribe that URL in Atlas Labs** with either key. Save the `whsec_` secret from the response; it is shown once.
   - Method: `POST`
   - URL: `https://automationatlas.io/labs/api/v1/webhooks`
   - Headers: `Authorization: Bearer lab_off_YOUR_KEY`; `Content-Type: application/json`
   - Body: `{"url": "https://hooks.zapier.com/hooks/catch/…/…/", "events": ["office.sales_order.accepted"], "description": "accepted orders"}`
   Subscribe to `office.sales_order.accepted` when the Zap will bridge the order into a work order, as in the next section: `POST /factory/work-orders` accepts only an order whose status is `accepted` and answers `409 invalid_transition` for any other. `office.sales_order.received` fires earlier, when the order arrives, and suits a listen-only Zap or the ping demo.
4. **Send the test ping** (`POST /webhooks/{id}/test`, in the snippet below), then click **Test trigger** in Zapier. The subscription turns `active` once Zapier answers `2xx`; Zapier's test only shows the `3` most recent webhooks from the past hour.
5. **Publish the Zap.** Atlas Labs re-delivers a failed event after `10 s`, `60 s` and `300 s`, then disables the subscription after `5` consecutive failures.

*curl · `snippets/06-subscribe-webhook.sh` · bash, curl, jq · proven against the live API (create, replay, test ping)*

```bash
#!/usr/bin/env bash
# Subscribe a public HTTPS URL (port 443) to events, keep the secret, send the test ping.
# Up to 3 subscriptions per sandbox. Needs WEBHOOK_URL, for example a Zapier Catch Hook URL.
set -euo pipefail
BASE="${ATLAS_BASE:-https://automationatlas.io/labs/api/v1}"
: "${ATLAS_OFFICE_KEY:?run: source atlas-keys.env}"
: "${WEBHOOK_URL:?set WEBHOOK_URL to your public https:// endpoint}"

created=$(curl -sS -X POST "$BASE/webhooks" \
  -H "Authorization: Bearer $ATLAS_OFFICE_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: subscribe-orders-1" \
  -d "$(jq -n --arg url "$WEBHOOK_URL" '{url: $url, events: ["office.sales_order.received"], description: "new orders"}')")
echo "$created" | jq '.data | {id, status, url_host, events}'

# The whsec_ secret is returned once. Replaying the same Idempotency-Key never repeats it (secret is null).
secret=$(jq -r '.data.secret // empty' <<<"$created")
[ -n "$secret" ] && (umask 077; printf '%s' "$secret" > atlas-webhook-secret.txt; chmod 600 atlas-webhook-secret.txt) || echo "no secret in this response (a replay?)" >&2
ID=$(jq -r '.data.id' <<<"$created")

# The subscription starts as pending_verification and activates when this ping gets a 2xx.
curl -sS -X POST "$BASE/webhooks/$ID/test" -H "Authorization: Bearer $ATLAS_OFFICE_KEY" | jq .data
curl -sS "$BASE/webhooks/$ID" -H "Authorization: Bearer $ATLAS_OFFICE_KEY" | jq '.data | {status, consecutive_failures, last_error_class}'
```

The tested file is shown unchanged and subscribes to `office.sales_order.received`, the listen-only case. For the bridge Zap below, set the events value to `office.sales_order.accepted` as in the field box above.

### Does Zapier verify the signature?

No. Add **Code by Zapier** (Run JavaScript) right after the Catch Raw Hook and map the raw body and the `webhook-id`, `webhook-timestamp` and `webhook-signature` headers into its input data. The step does two jobs. First, paste the `verify` function below, leaving out its two `import` lines and the `export` keyword, replace `process.env.ATLAS_WEBHOOK_SECRET` with the secret passed in as an input field of the step (never a `whsec_` value pasted into the code), and throw when it returns `false`, so the Zap stops. The excerpt is the tested Node file; how `node:crypto` is loaded inside Code by Zapier is not something we have confirmed, so check Zapier's Code docs for the form your step accepts. Second, parse the body with `JSON.parse` and return the fields later steps will map, for example `id`, `type` and `data.sales_order_id`; without this step a Catch Raw Hook offers no `id` or `sales_order_id` fields, only the raw body and the headers. The function needs the raw bytes, which is why the trigger must be Catch Raw Hook.

*JavaScript · `snippets/07-verify-webhook.mjs` · excerpt: the header comment and the `verify` function, as in the tested file; the server part is cut where `// …` stands · verified against the repo's own signer: good, rotated, tampered, wrong-secret, stale*

```javascript
// Receive and verify an Atlas Labs webhook (Node 20+, no dependencies).
// Standard Webhooks: headers webhook-id, webhook-timestamp, webhook-signature.
// signature = "v1," + base64(HMAC-SHA256(key, `${id}.${timestamp}.${rawBody}`)), where key is
// the base64-decoded part of your whsec_ secret. During the 24 h after rotate-secret the header
// holds two space-separated values ("v1,<new> v1,<old>"); accept either.
import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";

const SECRET = process.env.ATLAS_WEBHOOK_SECRET; // whsec_...
const TOLERANCE_S = 300;

export function verify(secret, headers, rawBody, nowS = Math.floor(Date.now() / 1000)) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const header = headers["webhook-signature"];
  if (!id || !timestamp || !header) return false;
  if (Math.abs(nowS - Number(timestamp)) > TOLERANCE_S) return false; // stale or replayed
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`, "utf8").digest();
  return header.split(" ").some((part) => {
    const [version, value] = part.split(",");
    if (version !== "v1" || !value) return false;
    const given = Buffer.from(value, "base64");
    return given.length === expected.length && timingSafeEqual(given, expected);
  });
}

// …
```

## How do I call the Atlas Labs API from a Zap?

1. **Add an action** → **Webhooks by Zapier** → event **Custom Request**. Use Custom Request rather than the plain POST event so the JSON in the Data field is sent exactly as typed.
2. **Fill the request.** This example bridges an accepted sales order into a Factory work order, so it uses the Factory key. Look the order up first with a Custom Request `GET https://automationatlas.io/labs/api/v1/office/sales-orders/<id>` (Head Office key), with `<id>` mapped from the Code step's `sales_order_id` output, to get `quantity` and `due_sim_day`.
   - Method: `POST`
   - URL: `https://automationatlas.io/labs/api/v1/factory/work-orders`
   - Headers: `Authorization: Bearer lab_fac_YOUR_KEY`; `Content-Type: application/json`; `Idempotency-Key:` insert the `webhook-id` header from the trigger (it equals the event `id`), or the Code step's `id` output
   - Data (raw JSON): `{"sales_order_id": "<sales_order_id from the Code step>", "quantity": <quantity from the order>, "due_date": <due_sim_day from the order>}`
   - Response fields: After a test, Zapier offers the step's HTTP status field and the response body to later steps. Their exact names are not given in the help page we read (unverified), so pick them from the list Zapier shows.
   **Preset matters.** Pick a preset with a manual bridge (Standard or Full) or this write returns `409 already_bridged`, because Starter bridges automatically.
3. **Treat `409 already_bridged` as success** in a Filter or Paths step: continue when the step's HTTP status field is `201`, or is `409` and the response body's `error.code` is `already_bridged` (a second delivery of the same event, or an order the sandbox bridged itself). Send yourself anything else, including `409 invalid_transition`: that one is a real error and means the order's status is not `accepted`, usually because the Zap was triggered by `office.sales_order.received` instead of `office.sales_order.accepted`.
4. **Prefer to keep the key out of the Zap?** Apps → Add connection → **API by Zapier** → authentication type **Static Headers (API key)** → one header per line: `Authorization: Bearer lab_fac_YOUR_KEY`. Then use its **API Request** action; Zapier adds the header itself. API by Zapier is premium and in beta.

## Idempotency and retries

- Use the event `id` as the `Idempotency-Key`: the `webhook-id` header from the Catch Raw Hook, or the `id` the Code step returns after parsing. A replayed Zap run then returns the original Atlas Labs response (`Idempotency-Replayed: true`) instead of creating a second record.
- On `429 rate_limited`, wait the seconds in `Retry-After` and send the same request. Zapier's own retry behaviour for Webhooks by Zapier is not described in the help pages we read, so do not rely on it.
- Leaving **Data** blank in a Custom Request sends every field from previous steps as the body; always type the JSON.

## Common errors

*Reference table — Atlas Labs error codes as seen from Zapier, not part of the fictional simulation.*

| You see | Why, and the fix |
| --- | --- |
| `403 wrong_system` | The key prefix does not match the route. `/factory/*` wants `lab_fac_`, `/office/*` wants `lab_off_`; the `hint` says which. |
| `409 already_bridged` | Not an error for this Zap: the order already has a work order (a second delivery of the same event, or a Starter sandbox bridged it itself). Treat it as success, as the Filter step above does. |
| `409 invalid_transition` | “only accepted sales orders can be bridged”: the order's status is not `accepted`. The Zap fired on `office.sales_order.received`, or the order was never accepted. Subscribe to `office.sales_order.accepted`, or accept it first with `POST /office/sales-orders/{id}/accept`. |
| `401 unauthorized` | The key is unknown (a typo), revoked (for example after it appeared in a URL) or deleted with its sandbox. An expired sandbox is different: its writes return `423 sandbox_expired`. Start a new sandbox and update the header. |
| `415 unsupported_media_type` | The body arrived without `Content-Type: application/json`. Add the header (see the gotcha below). |
| `422 idempotency_key_reused` | Same key, different body or path. Map a value that is stable per event, never one that changes between runs. |
| `422 webhook_url_rejected` | The URL was not public `https://` on port `443`; `details.reason` names the rule. Paste the full Zapier URL including `https://`. |
| Test trigger finds nothing | Send the test ping and click Test trigger within the hour; only the `3` newest requests are offered. |

## Gotchas

- **Plan gating.** Webhooks by Zapier (send and catch) is not available on the Free plan; API by Zapier is a premium, beta app (Zapier help centre, checked `15 Oct 2026`).
- **Raw body, no fields.** Signature verification needs the exact bytes. Catch Raw Hook keeps them (up to `2 MB`) and includes the headers, but offers nothing parsed: map `id`, `type` and `data.sales_order_id` from the Code step's output, never from the trigger. Catch Hook is the reverse: parsed fields, no raw bytes, no headers.
- **Content-Type.** Zapier's help says not to set a custom `Content-Type` on Webhooks by Zapier, while a community thread reports that adding `application/json` fixed a failing request. Test your Zap once with the header; Atlas Labs requires it on writes.
- **Per-app API Request is not for Atlas Labs.** The built-in “API Request” action inside other apps only reaches apps that already have a Zapier integration. Use Webhooks by Zapier or API by Zapier.
- **Underscored headers.** A community thread reports Catch Raw Hook dropping headers whose names contain an underscore (unverified). Atlas Labs headers use hyphens, so this should not affect you.

## Download the recipe

- [Bridge an accepted sales order into a work order (Markdown recipe)](https://automationatlas.io/labs/downloads/templates/zapier/bridge-accepted-sales-order-to-work-order.md) — Catch Hook (parsed fields, no signature check) → look up the order → Custom Request `POST /factory/work-orders` → Filter on `201` or `409 already_bridged`. Needs a manual-bridge preset (Standard or Full). (Written from vendor docs, never built in a live Zapier account)

## Questions people ask

**Will Atlas Labs deliver webhooks to a Zapier URL?** Yes. `hooks.zapier.com` is public HTTPS on port `443`, which is all a subscription needs, and it is one of the shared platform hosts that get a higher delivery allowance.

**Does Zapier verify the Standard Webhooks signature?** No. Use Catch Raw Hook and the Code by Zapier step above. For a fictional sandbox you may decide to skip it; the payload is only ever simulation data.

**Which Zapier plan do I need?** Webhooks by Zapier is on Professional, Team and Enterprise (Zapier help centre, checked `15 Oct 2026`). Without it, use Make, n8n or the REST snippets.

**Can Zapier import the Atlas Labs OpenAPI file?** We found no OpenAPI or Swagger import in Zapier's documentation. Build the two HTTP steps by hand; it takes under a minute.

**What should I use as the Idempotency-Key in a Zap?** The event id (`evt_…`): the `webhook-id` header on a Catch Raw Hook, or the parsed `id` field on a Catch Hook. Never a timestamp or a random value.

**Why can't I find `id` or `sales_order_id` in the Catch Raw Hook's fields?** Because Catch Raw Hook keeps the body unparsed. Either parse it in the Code by Zapier step and map that step's outputs, or switch the trigger to Catch Hook, which parses the body but drops the headers, so the signature cannot be checked.

**Why does my Zap get `403 wrong_system`?** You sent the Head Office key to a Factory route, or the other way round. Both keys appear once, when the sandbox is created; keep them labelled.

Sources (checked `15 Oct 2026`): Zapier help centre, “Send webhooks in Zaps” (`8496326446989`), “Trigger Zaps from webhooks” (`8496288690317`), “Send API requests in Zap workflows” (`44391650357005`), “How to get started with API by Zapier” (`44391660158733`). Atlas Labs facts from the API source, observed live.

[Authenticate with your key](https://automationatlas.io/labs/docs/tools/rest-api/) · [n8n](https://automationatlas.io/labs/docs/tools/n8n/)
