# ReelsBuilder + Zapier (Webhooks by Zapier)

Zapier cannot import a workflow file, so build these Zaps by hand. Everything
below uses the live ReelsBuilder API:

- Base URL: `https://api.reelsbuilder.ai/api/v1`
- Auth header: `Authorization: Bearer rb_...` (create a key at
  https://reelsbuilder.ai/dashboard/v5/settings?tab=api)
- Full contract: https://reelsbuilder.ai/docs/api/llms.txt and
  https://api.reelsbuilder.ai/api/v1/capabilities

Store the key once in **Storage by Zapier** or paste it into the header fields
of your own Zaps only. Never share a Zap template that contains a real key.

Price: stock video $0.60 per video, refunded automatically if the render fails.
The dry run is free.

---

## Recommended: two Zaps joined by a webhook

Zap A queues the video and stops. ReelsBuilder calls Zap B when the render
finishes. No polling, no long-running Zap.

### Zap B (build it first): receive the finished video

1. **Trigger: Webhooks by Zapier > Catch Raw Hook.** Copy the hook URL
   (`https://hooks.zapier.com/hooks/catch/...`). Use *Catch Raw Hook*, not
   *Catch Hook*, if you want to verify the signature (see below).
2. **Action: Code by Zapier > Run JavaScript** (optional but recommended,
   verifies the signature). Input data:
   - `raw_body` = the raw body from step 1
   - `signature` = the `X-ReelsBuilder-Signature` header from step 1
     (shown under *Headers* as `Http X Reelsbuilder Signature`)
   - `secret` = your webhook secret from `GET /webhook-secret` (see below)

   ```js
   const crypto = require('crypto');
   const parts = Object.fromEntries(inputData.signature.split(',').map((p) => p.split('=')));
   const t = Number(parts.t);
   const expected = crypto.createHmac('sha256', inputData.secret)
     .update(`${t}.${inputData.raw_body}`).digest('hex');
   const fresh = Math.abs(Date.now() / 1000 - t) < 300;
   if (!fresh || parts.v1 !== expected) throw new Error('Invalid ReelsBuilder signature');
   const event = JSON.parse(inputData.raw_body);
   const hashtags = (event.description || '').match(/#[\w]+/g) || [];
   return { ...event, hashtags: hashtags.join(' ') };
   ```
3. **Filter by Zapier:** only continue if `status` exactly matches `completed`.
   (Failed jobs arrive with `status` `failed` or `dead_lettered` and an
   `error` object with `code`, `message`, `retryable`; the charge is refunded.)
4. **Your actions:** upload `video_url` to Drive/S3, post it with your
   scheduler, or write `video_url`, `title`, `description` and `hashtags` to a
   sheet. Copy the MP4 to your own storage; CDN files have a retention window.

Callback body fields: `event` (`job.completed` | `job.failed`), `job_id`,
`status`, `video_id`, `video_url`, `video_urls`, `title`, `description`
(includes hashtags), `duration_sec`, `error`, `agent_run_id`, `poll_url`,
`timestamp`. ReelsBuilder tries 3 times (0 s, 30 s, 120 s) and the same job can
arrive more than once, so de-duplicate on `job_id` (for example with
*Storage by Zapier* or a sheet lookup).

**Get the webhook secret once** (a one-off Zap step or your terminal):

```bash
curl https://api.reelsbuilder.ai/api/v1/webhook-secret \
  -H "Authorization: Bearer $REELSBUILDER_API_KEY"
# -> { "data": { "secret": "...", "header": "X-ReelsBuilder-Signature", "tolerance_seconds": 300 } }
```

### Zap A: queue the video

1. **Trigger:** anything (Schedule by Zapier, new Google Sheets row, new RSS
   item, new CRM record).
2. **Action: Webhooks by Zapier > Custom Request** (the quote, free)
   - Method: `POST`
   - URL: `https://api.reelsbuilder.ai/api/v1/generate-and-post`
   - Data pass-through: `false`
   - Data (raw JSON):

     ```json
     {
       "dry_run": true,
       "prompt": "Why mid-size IT teams are moving to zero-trust networking",
       "content_purpose": "education",
       "clip_type": "tech",
       "duration_sec": 30,
       "aspect_ratio": "9:16",
       "include_branding": false,
       "cta": { "text": "Book a demo at acme.com" },
       "brand": {
         "name": "Acme Cloud",
         "website_url": "https://acme.com",
         "logo_url": "https://acme.com/logo.png",
         "primary_color": "#0A66C2"
       }
     }
     ```
   - Headers: `Authorization` = `Bearer rb_...`, `Content-Type` = `application/json`
   - Map your trigger fields into `prompt` and `brand`. Avoid unescaped double
     quotes in mapped text, or build the body with *Code by Zapier* and
     `JSON.stringify`.
3. **Filter by Zapier:** continue only if `Data Valid` is true and
   `Data Price Usd Cents` (number) is less than or equal to `60`.
   Nothing has been charged yet.
4. **Action: Webhooks by Zapier > Custom Request** (the paid call)
   - Method: `POST`, same URL
   - Data: the same JSON **without** `"dry_run": true`, plus
     `"webhook_url": "<the Zap B hook URL>"` and, if you like,
     `"agent_run_id": "<your row or record id>"` to correlate the callback.
   - Headers: `Authorization` = `Bearer rb_...`,
     `Content-Type` = `application/json`,
     `Idempotency-Key` = a stable id for this creative, 8-256 characters, for
     example `zap-<trigger record id>`. Zapier may replay a step; the same key
     and same body never bill twice. Use a new key for a new video.
   - Response: HTTP 202 with `data.job_id` and `data.poll_url`. Save
     `data.job_id` next to your record.

---

## Alternative: one Zap that polls

Use this only for a handful of videos; every poll is a Zapier task.

1. Trigger and steps 2-4 as in Zap A (without `webhook_url`).
2. **Delay by Zapier > Delay For:** 4 minutes (a 30 s stock video usually
   takes 3-6 minutes).
3. **Webhooks by Zapier > Custom Request:** Method `GET`,
   URL `https://api.reelsbuilder.ai/api/v1/jobs/<data.job_id from step 4>`,
   header `Authorization` = `Bearer rb_...`.
4. **Paths by Zapier:**
   - `Data Job Status` = `completed`: use `Data Job Artifacts Video Url`,
     `Data Job Artifacts Title`, `Data Job Artifacts Description` (hashtags
     included) and `Data Job Artifacts Thumbnail Url`.
   - `Data Job Status` is `failed`, `dead_lettered` or `cancelled`: alert a
     human with `Data Job Error Code` and `Data Job Error Message`. The charge is
     refunded; resubmit with a new `Idempotency-Key` only if
     `Data Job Error Retryable` is true.
   - Otherwise (still `queued`, `running` or `uploading`): add another
     *Delay For* of 1-2 minutes and a second GET, and repeat a few times.
     Give up after 30 minutes total and check the job later.

Zapier has no loop-until step, so the webhook pattern above is more reliable
and cheaper in tasks.

---

## Errors (every endpoint)

Errors use one envelope: `{ "success": false, "error": { "code", "message", "retryable" } }`.

| Response | What to do |
|---|---|
| `400 INPUT_INVALID` | Fix the field named in `error.message`; nothing was charged. |
| `400 IDEMPOTENCY_KEY_REQUIRED` | Add the `Idempotency-Key` header to the paid call. |
| `402 INSUFFICIENT_API_CREDITS` | Top up; do not retry automatically. |
| `409 RESOURCE_ALREADY_EXISTS` | Same key reused with a different body; use a new key. |
| `429 RENDER_CONCURRENCY_LIMIT_EXCEEDED` | Up to 5 stock renders run at once per account; retry later. |
| `429 RATE_LIMIT_EXCEEDED` | Wait `Retry-After` seconds (60 reads per minute per key). |

Many videos at once? `POST /api/v1/batch` takes up to 25 jobs in one call.
See https://reelsbuilder.ai/docs/api/jobs#batch.
