# 04 — The Worker (the engine)

[← Back to README](README.md) · [← 03 Pricing engine](03-pricing-engine.md) · [05 The Facebook ad →](05-facebook-ad.md) · [06 Go live & monitor →](06-go-live-and-monitor.md)

---

This is the one technical stage in the guide. It is also the load-bearing one: the
worker is the piece that actually receives each Facebook lead and fires the text
messages. Everything before this (the form, the pricing) is just data and rules;
the worker is what *does* something with them.

You do **not** need to be a programmer. This page is written so you can either run
the commands yourself by copy-paste, or hand the whole thing — plus the complete code
in [Appendix A](appendix-A-reference-worker.md) — to an AI assistant and let it deploy
for you. Either way, **read the warning box below first.** It describes the single
mistake that has the highest cost in this entire system.

---

## What the worker is

The worker is a **tiny serverless function that runs on Cloudflare**. "Serverless"
means there is no server for you to rent, patch, or keep alive — Cloudflare runs your
code only in the moment a request arrives, and the free tier covers far more traffic
than a small cleaning business will ever generate. Practically, the worker itself costs
**$0** to run. That is the compute only, though. Your SMS provider is a separate, real
cost, covered in "A note on the SMS provider" below; do not read "$0" as "the whole
system is free."

It is a single small program. When Facebook says "you have a new lead," Cloudflare
wakes the worker, runs it for a fraction of a second, and puts it back to sleep. There
is nothing to monitor day-to-day beyond what we cover in [06](06-go-live-and-monitor.md).

---

## The request flow, in plain English

Here is everything the worker does, in order, when a lead comes in:

1. **Meta sends a webhook.** The moment someone submits your lead form, Facebook makes
   an HTTP `POST` to your worker's URL saying "there is a new lead, here is its ID."
2. **Verify it is really Meta.** Anyone on the internet can send your worker a fake
   "new lead" message. So the worker checks a cryptographic signature on the request
   (an HMAC built with your **`FB_APP_SECRET`**). No valid signature → rejected.
3. **Fetch the lead's answers.** The webhook only contains an *ID*, not the actual
   name/phone/answers. The worker calls Facebook's Graph API with that ID, using your
   **`FB_PAGE_ACCESS_TOKEN`**, to pull the real form answers. **This is the step that
   the warning box below is about.**
4. **Compute the price.** It runs the answers (square footage, bathrooms, service type)
   through the pricing logic from [03](03-pricing-engine.md).
5. **Text the lead their quote.** It sends the lead an SMS: their estimated price plus
   a booking link.
6. **Text *you* a notification.** In parallel it sends *you* (the owner) a second SMS
   with the lead's full details, so you never have to go digging.
7. **Save a backup copy.** It also posts the lead to a backup endpoint (email via
   Formspree in this example) so there is always a second record.

All of that happens in about a second, untouched by human hands.

---

> ## ⚠️ THE ONE THING THAT WILL BREAK THIS — read before anything else
>
> **`FB_PAGE_ACCESS_TOKEN` must be a never-expiring _System User_ token. Nothing else
> will do.**
>
> Step 3 above — fetching the lead's answers — needs a Facebook access token. There are
> several kinds of token, and they look identical. If you use a **normal user token or a
> user-derived Page token**, it will work perfectly in testing and then **die the moment
> you (or anyone with access) change your Facebook password**, or when Meta does a routine
> security reset. The error Facebook returns is `OAuthException code 190, subcode 460`.
>
> When that token dies, the worker still *receives* every webhook — but step 3 fails, so
> it sends **no quote to the lead, no notification to you, and (without the safety net
> below) not even a backup**. You keep paying Facebook for leads while every one of them
> is **silently dropped**. There is no error in your face; you just slowly notice "it's
> gone quiet."
>
> **This actually happened to NeatNow and cost about two days of leads** before anyone
> noticed. (Full postmortem: [Appendix C](appendix-C-troubleshooting.md) / [06](06-go-live-and-monitor.md).)
>
> **The rule that prevents it:**
>
> - Use a **System User** token, generated in **Meta Business Suite → Settings → Users →
>   System users** (older accounts may still see this as "Business Settings → System Users";
>   both open the same place). System User tokens survive password changes and can be set to
>   expire **Never**.
> - **Critically:** the System User must have your **Page assigned with the _Leads_ task
>   enabled.** The **_Ads_ task alone is NOT enough** to read lead data — this is the exact
>   trap NeatNow fell into (the page was assigned with "Ads" only). In the System User detail,
>   click the **Manage** button → in the **Manage assignments** dialog, toggle **Leads** on
>   (in addition to **Ads**) → **Save**.
> - Then click **Generate token** and walk the wizard: **Select app → Set expiration (choose
>   Never; the choices are "60 days (Recommended)" and "Never") → Assign permissions (select
>   `leads_retrieval`, `pages_read_engagement`, `pages_manage_ads`, `pages_show_list`,
>   `ads_management`) → Done**.
> - Meta will require an account-verification step ("Verify account", password/2FA) before
>   it reveals the token. That is normal; complete it.
>
> See [`appendix-D-visual-walkthrough.md`](appendix-D-visual-walkthrough.md) for dated
> screenshots of these screens.
>
> The step-by-step generation walkthrough is in the setup checklist further down and in
> [06](06-go-live-and-monitor.md).

---

## Fail-loud design: the safety net

You cannot fully prevent a token from one day going bad (someone resets a password, Meta
changes a policy). What you *can* do is make sure a dead token screams instead of
whispering. The reference worker is built to **fail loud**:

When the lead-fetch in step 3 fails, instead of silently giving up, the worker:

1. **Texts you an alert immediately** — something like *"⚠️ A Facebook lead arrived but
   could not be fetched. Your page token is probably invalid (often after a Facebook
   password change). Regenerate `FB_PAGE_ACCESS_TOKEN`."*
2. **Writes the raw lead ID to the backup endpoint**, so even the dropped lead leaves a
   trace and is recoverable from Meta's Leads Center.

This is the single change that turns a silent two-day outage into a two-minute alert. It
is already in the [Appendix A](appendix-A-reference-worker.md) code — do not remove it.

> 🔧 **Customize:** the alert goes to `FB_OWNER_NOTIFY_PHONE`. Set it to your own mobile
> number so *you* get paged, not the example one.

---

## Environment configuration

The worker reads its settings from two kinds of values. The distinction matters:

- **Vars** are plain-text, non-secret settings. They live openly in `wrangler.toml`.
- **Secrets** are credentials. They are set with a separate command and are never written
  into any file — Cloudflare stores them encrypted.

### Vars (plain text, in `wrangler.toml`)

| Var | What it is |
|---|---|
| `OPENPHONE_FROM_NUMBER` | The phone number your texts are sent *from* (your business line), in `+1...` E.164 format. |
| `FORMSPREE_ENDPOINT` | The backup endpoint that receives a copy of every lead (a Formspree form URL here). |
| `ALLOWED_ORIGINS` | Comma-separated list of websites allowed to call the worker (CORS). FB-lead-only setups can leave this minimal. |
| `FB_GRAPH_API_VERSION` | Which Graph API version to call, e.g. `v25.0`. Use a current version (v25.0 was current as of June 2026) and check the [changelog](https://developers.facebook.com/docs/graph-api/changelog) — versions deprecate on a ~2-year cycle. |
| `FB_OWNER_NOTIFY_PHONE` | *Your* mobile number — gets the "new lead" detail SMS and the fail-loud alert. |

> The `OPENPHONE_*` var/secret names are historical (the provider was OpenPhone, now Quo).
> They are arbitrary internal names — keep them as-is; renaming them would just mean editing
> the worker code for no benefit.

### Secrets (set with `wrangler secret put`, never in a file)

| Secret | What it is |
|---|---|
| `OPENPHONE_API_KEY` | Your SMS provider API key, used to send texts. |
| `FB_VERIFY_TOKEN` | A random string *you invent*. Used once, during the webhook handshake, to prove you own the worker. |
| `FB_APP_SECRET` | Your Facebook App Secret. Used to verify each webhook is genuinely from Meta. |
| `FB_PAGE_ACCESS_TOKEN` | The **never-expiring System User token** from the warning box. Used to fetch lead answers. |

> **No secret values appear anywhere in this guide.** Every example uses a placeholder like
> `<YOUR_OPENPHONE_API_KEY>`. Never paste a real key, token, or App Secret into `wrangler.toml`
> or any file you commit.

---

## Deploy steps

You will run these from inside the worker folder. The complete code to put in that folder
is in [Appendix A](appendix-A-reference-worker.md) — create the files exactly as listed
there, or hand Appendix A to your AI assistant and have it create them.

**1. Install Wrangler** (Cloudflare's command-line tool). You need Node.js installed first
(from nodejs.org). Then, in the worker folder:

```bash
npm install
```

> **Note:** Wrangler's current major is **v4** (it dropped Node 16 support). Cloudflare now
> recommends **`wrangler.jsonc`** for new projects (some newer features are JSON-config only);
> the `wrangler.toml` used in this guide still works. See the
> [configuration docs](https://developers.cloudflare.com/workers/wrangler/configuration/).

**2. Log in to Cloudflare:**

```bash
npx wrangler login
```

This opens a browser window; approve the access. One-time.

**3. Deploy the worker:**

```bash
npx wrangler deploy
```

When this finishes, Wrangler prints your worker's public URL. It looks like:

```
https://<worker-name>.<your-subdomain>.workers.dev
```

> 🔧 **Customize:** `<worker-name>` is the `name` field in `wrangler.toml` (NeatNow's is
> `neatnow-api` — use your own). `<your-subdomain>` is assigned to your Cloudflare account.
> **Write this full URL down** — you need it to subscribe the webhook in the next section.

**4. Set each secret.** Secrets are *not* in any file, so set them one at a time. The
`printf` form avoids your shell mangling special characters in a key:

```bash
printf '%s' "<YOUR_OPENPHONE_API_KEY>"   | npx wrangler secret put OPENPHONE_API_KEY
printf '%s' "<YOUR_FB_VERIFY_TOKEN>"     | npx wrangler secret put FB_VERIFY_TOKEN
printf '%s' "<YOUR_FB_APP_SECRET>"       | npx wrangler secret put FB_APP_SECRET
printf '%s' "<YOUR_FB_PAGE_ACCESS_TOKEN>"| npx wrangler secret put FB_PAGE_ACCESS_TOKEN
```

`FB_VERIFY_TOKEN` is any random string you choose — generate something long and random and
keep a copy; you will paste the same value into Meta in the next step.

> **Secrets apply immediately** — `wrangler secret put` creates and deploys a new version of
> the Worker right away (no separate `wrangler deploy` needed). This is why fixing a dead
> token is a 30-second job once you have a fresh one: just re-run the `FB_PAGE_ACCESS_TOKEN`
> line. ([Cloudflare secrets docs](https://developers.cloudflare.com/workers/configuration/secrets/))

---

## Subscribing the webhook

The worker is live, but Facebook does not know to call it yet. You connect them in your
Meta App.

> ℹ️ **Meta moves these screens around often.** The labels and click-paths below were current
> in mid-2026, but Meta reorganizes the App dashboard regularly, so match by intent if a name
> has drifted rather than expecting an exact match. Where a step can be done by an API call
> instead of a dashboard click (step 5), the API call is the more stable path.

1. In your Meta App dashboard, add the **Webhooks** product (or open it if present).
2. Subscribe to the **Page** object and tick the **`leadgen`** field. (This is the event
   that fires when someone submits a lead form.)
3. Meta asks for two things:
   - **Callback URL:** your worker URL plus the webhook path, i.e.
     `https://<worker-name>.<your-subdomain>.workers.dev/facebook/webhook`
   - **Verify token:** the exact same string you set as `FB_VERIFY_TOKEN`.
4. Click verify. Meta sends a one-time `GET` to your worker with a challenge value. The
   worker echoes the challenge back **only if** the verify token matches — so a successful
   handshake also confirms your `FB_VERIFY_TOKEN` is set correctly. If it fails, the token
   you typed into Meta does not match the secret you set.
5. Finally, subscribe your specific **Page** to the app for `leadgen`. This is a *separate*
   thing from step 2: step 2 subscribes the app to the `leadgen` field, but the individual
   Page also has to be subscribed to the app, and if it is not, Facebook has nowhere to send
   the ping. This is the single most common silent failure. The dependable way to do it is one
   Graph API call, using a Page access token that has `pages_manage_metadata`:

   ```
   POST https://graph.facebook.com/v25.0/<PAGE_ID>/subscribed_apps
   subscribed_fields=leadgen
   ```

   The [Graph API Explorer](https://developers.facebook.com/tools/explorer/) is the easy way
   to run this once. (Meta has at times also exposed a per-Page subscribe toggle in the
   dashboard, but that surface keeps moving, so the API call above is the reliable path.)

The end-to-end smoke test that proves the whole chain works — using Meta's Lead Ads Testing
Tool — is in [06](06-go-live-and-monitor.md).

---

## A note on the SMS provider

This guide's example sends texts through **Quo (formerly OpenPhone)**. The worker talks to it
with one small helper function that does an HTTP `POST`:

- **Endpoint:** `POST https://api.quo.com/v1/messages` (the legacy `api.openphone.com` host
  may still resolve, but `api.quo.com` is canonical).
- **Auth header:** `Authorization: <your key>` — note it is the **raw key, with no
  `Bearer ` prefix** (a common mistake; Quo does not use `Bearer`).
- **Success:** Quo returns **HTTP `202`** (accepted), not `200`. The worker treats
  `202` as success.
- Docs: [send a text message](https://www.quo.com/docs/api-reference/messages/send-a-text-message),
  [authentication](https://www.quo.com/docs/mdx/api-reference/authentication).

> 💵 **What Quo actually costs (two separate charges, and a gotcha).** Sending texts through
> the API is not free and is not covered by the plan fee. Per Quo's own API pricing docs, there
> are two costs:
> - A **recurring subscription** (per user/number). An active Quo plan is required just to have
>   API access.
> - **Prepaid message credits** for programmatic sending, on top of the subscription ("no
>   monthly API fees: pay only for messages sent"). Every API text draws down a credit balance
>   at **$0.01 per segment** for US/Canada (a segment is ~160 standard characters; one special
>   character can drop the limit to 70 and cost an extra segment). You top the balance up
>   manually or turn on **auto-recharge**.
> - **The gotcha:** if the credit balance runs out, the API **rejects the send and the text
>   never goes out**. For a quote engine that is a silent way to stop replying to leads, so keep
>   credits funded (auto-recharge is the safe default) and watch the balance.
>
> So your real monthly cost is the subscription plus your messaging volume. Any provider you
> swap in (Twilio, Telnyx, etc.) has its own per-message cost too. The worker is free to run;
> the texts are not.
>
> Source: Quo Docs, *"API Pricing overview"* — "We use a credit-based system for all API
> messaging charges"; "messages will fail if insufficient credits are available."

> 🔧 **Customize:** any SMS API works — Twilio, Telnyx, ClickSend, whatever you already use.
> You only swap **one function** (`sendOpenPhoneSms` in [Appendix A](appendix-A-reference-worker.md)):
> change the URL, the auth header, the JSON body shape, and the success status code to match
> your provider's docs. Nothing else in the worker needs to change.

---

## What's next

- Your engine is live. Now build the ad that feeds it → **[05 — The Facebook ad](05-facebook-ad.md)**
- Then test it end-to-end and learn to monitor it (including the full token-incident
  diagnosis playbook) → **[06 — Go live & monitor](06-go-live-and-monitor.md)**
- The complete, copy-pasteable code for everything above → **[Appendix A — Reference worker](appendix-A-reference-worker.md)**
