# Zapier recipe: bridge accepted sales orders to work orders

Zaps can't be exported as an importable file, so this is the exact click-by-click recipe. It
reproduces the same flow as `templates/n8n/` and `templates/make/`: **Catch Hook → (look up the
order) → Webhooks by Zapier POST → Filter on the result**.

**Mission this completes:** #3 standard ("Bridge it yourself: create the work order"), feeding
mission #6.
**Verified:** written from Zapier's documented app behaviour; not built in a live Zapier account
(no sandbox key can be shared with a third-party SaaS from this environment) — see
`templates/README.md`.

## 1. Trigger: Catch Hook (Webhooks by Zapier)

1. New Zap → app **Webhooks by Zapier** → event **Catch Hook** → **Continue**.
2. Zapier shows a custom webhook URL, e.g. `https://hooks.zapier.com/hooks/catch/123456/abcdef/`.
   Copy it.
3. Subscribe it in Atlas Labs, with either key:

   ```bash
   curl -X POST "https://automationatlas.io/labs/api/v1/webhooks" \
     -H "Authorization: Bearer $ATLAS_LABS_OFFICE_KEY" \
     -H "Content-Type: application/json" \
     -d '{"url":"https://hooks.zapier.com/hooks/catch/123456/abcdef/","events":["office.sales_order.accepted"],"description":"Zapier: bridge accepted orders"}'
   ```

   Save the `whsec_...` secret from the response — Zapier's Catch Hook trigger does not verify
   Standard Webhooks signatures for you (see "Signature verification" below).
4. In Zapier, trigger a test delivery — either wait for a real order to be accepted, or use
   `POST /webhooks/{id}/test` — then **Test trigger** to load a sample. You should see fields
   under `data` including `data__sales_order_id` and a top-level `id` (the event id, `evt_...`).

## 2. Action: GET sales order details (Webhooks by Zapier — Custom Request)

The webhook payload only carries `{sales_order_id}` (PLAN §4.6 — the `office.sales_order.accepted`
event payload is intentionally small), but `POST /factory/work-orders` needs `quantity` and
`due_date` too. `GET /office/sales-orders/{id}` exists (see "Known limitation" at the bottom), but this recipe was
written before it did, so it lists and filters. When you build your own Zap, call the by-id route instead:

1. Add action **Webhooks by Zapier** → **Custom Request**.
2. **Method:** `GET`
3. **URL:** `https://automationatlas.io/labs/api/v1/office/sales-orders`
4. **Query string params:** `limit` = `100`
5. **Headers:** `Authorization` = `Bearer <your lab_off_... key>`
6. **Data Pass-Through?:** No. **Unflatten:** Yes.
7. Test the step. The response is `{"meta": {...}, "data": {"items": [ {...}, ... ], "next_cursor": ...}}` —
   list endpoints wrap their rows as `data.items`, not a bare array (PLAN §4.1 "Lists").

## 3. Action: Formatter — find the matching order (Formatter by Zapier — Utilities → Lookup Table, or Code by Zapier)

Zapier's no-code steps can't filter a JSON array by a field value, so use **Code by Zapier**
(Run JavaScript) here — it's the standard Zapier pattern for "find a row in an array response":

**Input data:**
- `salesOrderId` = `{{1.data__sales_order_id}}` (from step 1, the Catch Hook)
- `salesOrdersJson` = `{{2.data}}` (the raw `{"items": [...], "next_cursor": ...}` object from step 2
  — enable "send raw request data" or reference the step's raw JSON output field if Zapier
  flattened it)

**Code:**

```javascript
// List endpoints wrap their rows as {items: [...], next_cursor}, not a bare array (PLAN §4.1 "Lists").
const orders = JSON.parse(inputData.salesOrdersJson).items;
const order = orders.find((o) => o.id === inputData.salesOrderId);
if (!order) {
  throw new Error(`sales order ${inputData.salesOrderId} not found in the first page of GET /office/sales-orders`);
}
output = { quantity: order.quantity, due_date: order.due_sim_day };
```

## 4. Action: POST /factory/work-orders (Webhooks by Zapier — Custom Request)

1. Add action **Webhooks by Zapier** → **Custom Request**.
2. **Method:** `POST`
3. **URL:** `https://automationatlas.io/labs/api/v1/factory/work-orders`
4. **Headers:**
   - `Authorization` = `Bearer <your lab_fac_... key>`
   - `Content-Type` = `application/json`
   - `Idempotency-Key` = `{{1.id}}` — **the event id from step 1**, not a random value. Replaying
     the same Zap run (Zapier's own retries) always reuses the same key, so a retry never creates a
     second work order.
5. **Data (raw JSON):**

   ```json
   {
     "sales_order_id": "{{1.data__sales_order_id}}",
     "quantity": {{3.quantity}},
     "due_date": {{3.due_date}}
   }
   ```

6. Under **Custom Request** options, leave "Fail on non-2xx" at Zapier's default — the next step
   reads the status either way because Webhooks by Zapier still returns `status_code` and the
   parsed body on a non-2xx `status_code` when "Unflatten" is on (Zapier calls this a "successful"
   step even for a 4xx by default, which is exactly what the next filter needs).

## 5. Filter: treat `409 already_bridged` as success

Add **Filter by Zapier** (or a second Code step, for a cleaner "both branches" Zap, use **Paths by
Zapier**):

- **Path A — Bridged:** continue only if `{{4.status_code}}` **Exactly matches** `201`
  **OR** (`{{4.status_code}}` Exactly matches `409` **AND** `{{4.json__error__code}}` Exactly
  matches `already_bridged`).
- **Path B — Failed:** everything else. Send yourself a notification (Email/Slack) with
  `{{4.status_code}}` and `{{4.json__error}}` so a real failure doesn't disappear silently.

## Field mapping summary

| From | Field | To |
|---|---|---|
| Catch Hook | `id` (event id, `evt_...`) | `Idempotency-Key` header on step 4 |
| Catch Hook | `data.sales_order_id` | `sales_order_id` in step 4's body |
| Step 3 (Code) | `quantity` | `quantity` in step 4's body |
| Step 3 (Code) | `due_date` (= the order's `due_sim_day`) | `due_date` in step 4's body |
| Step 4 | `status_code`, `json.error.code` | Path/Filter condition in step 5 |

## Signature verification

Zapier's Catch Hook trigger has no built-in Standard Webhooks verification. Either:

- accept the (low, for a fictional sandbox) risk of an unverified Catch Hook URL, since Atlas Labs
  webhook URLs aren't secret but the payload is only ever fictional simulation data; or
- add a **Code by Zapier** step right after the trigger that recomputes the
  `webhook-signature` HMAC from the `webhook-id`/`webhook-timestamp` headers (Zapier exposes
  inbound headers as `{{1.headers__webhook_signature}}` etc. after a test poll) the same way
  `templates/snippets/typescript.ts` does, and stops the Zap (`callback_url`/early return, or throw
  inside a Code step to halt) when it doesn't match.

## Known limitation (no longer an API gap)

- **List-and-filter instead of `GET /office/sales-orders/{id}`.** The by-id route exists. Every
  recipe in this unit (n8n, Make, Zapier) predates it and lists `GET /office/sales-orders?limit=100`
  and filters client-side, which doesn't scale past 100 open orders and costs an extra request per
  bridge. Use the by-id route in anything new — see `templates/README.md`.
