# Appendix C — Troubleshooting (when leads stop arriving)

[← Back to README](README.md) · Related: [04-the-worker.md](04-the-worker.md) · [06-go-live-and-monitor.md](06-go-live-and-monitor.md)

---

This is the lookup table you reach for when something breaks. Each failure mode is laid out
as **symptom → likely cause → fix**. For the live diagnosis procedure (tail logs, fire a
test lead, read the line), use the playbook in
[06-go-live-and-monitor.md](06-go-live-and-monitor.md). For the worker and token internals,
see [04-the-worker.md](04-the-worker.md).

---

## C-1 — Leads silently stopped (Meta still shows leads coming in) — THE headline incident

**Symptom**
Meta's Ads Manager still shows leads arriving (and is still charging you), but you get
**no auto-quote texts, no owner alerts, not even a Formspree backup**. It can go unnoticed
for days because nothing visibly errors. The log line, if you tail it, is
`failed to fetch lead` with **`OAuthException code 190, subcode 460`**.

**Likely cause**
**The page token died.** The worker fetches each lead from Facebook using
`FB_PAGE_ACCESS_TOKEN`. If that token is a user token or a user-derived page token, it is
**invalidated the moment you change your Facebook password** (or Meta forces a security
reset, or it hits its ~60-day expiry). The webhook still arrives, but the fetch step fails,
and the lead is dropped. This is what cost NeatNow ~2 days of leads in June 2026.

**Fix — regenerate a never-expiring System User token**

1. In **Meta Business Suite → Settings → Users → System users** (older accounts may still
   see this as "Business Settings → Users → System Users"; both open the same place), select
   (or create) your System User.
2. **Assign the Page with the Leads task.** Click the **Manage** button → in the **Manage
   assignments** dialog, toggle **Leads** on (in addition to **Ads**) → **Save**. The Ads
   task alone cannot read lead data; it should then read "Ads and Leads."
3. **Generate a token** under your lead-webhook app. Click **Generate token** and walk the
   wizard: **Select app → Set expiration (choose Never; the choices are "60 days
   (Recommended)" and "Never") → Assign permissions** (select all five) **→ Done**:
   - `leads_retrieval`
   - `pages_read_engagement`
   - `pages_manage_ads`
   - `pages_show_list`
   - `ads_management`
4. **Complete the "Verify account" prompt** — Meta requires this account-verification step
   (password / 2FA) before it reveals the token.
5. **Set the secret on the worker** (applies immediately — `wrangler secret put` creates and
   deploys a new version of the Worker right away, no separate `wrangler deploy` needed):

   ```
   npx wrangler secret put FB_PAGE_ACCESS_TOKEN
   ```

   Paste the new token when prompted.
6. **Verify it worked** — ask the Graph API for the form's leads directly:

   ```
   GET https://graph.facebook.com/<version>/<FORM_ID>/leads?access_token=<TOKEN>
   ```

   A list of leads back = fixed. An auth error (`code 190`) = the token still is not
   right (re-check permissions and the Leads task).

**Why this no longer goes unnoticed (the fail-loud alert)**
The original design failed *silently* — the only trace was a buried Cloudflare log line.
The hardened worker now **sends you an owner-alert SMS the instant a lead fetch fails**
(something like *"⚠️ FB lead received but could not be fetched — the page token may be
invalid. Check/regenerate FB_PAGE_ACCESS_TOKEN."*) and forwards the raw lead ID to your
backup even on failure. So instead of losing days of leads, you get paged within minutes
and run the fix above. This alerting is part of the reference worker in
[04-the-worker.md](04-the-worker.md) — make sure your build includes it.

**Prevention:** Always use a **System User token**, never a user/page token. System User
tokens survive password changes and can be set to never expire.

---

## C-2 — Auto-quote SMS never arrives, but the owner notification does (or vice-versa)

**Symptom**
Part of the texting works and part does not. Either the **lead** never gets their quote but
**you** get the owner alert, or you get nothing while the lead does. The log shows
something like `owner notify failed: OpenPhone …`.

**Likely cause**
An **SMS provider key or from-number issue** — a wrong/expired API key, or a "from" number
that is not provisioned/verified to send.

**Fix**

- Check the **`OPENPHONE_API_KEY`** secret is current and valid (Quo, formerly OpenPhone:
  workspace **Settings → API** tab; requires an **Owner/Admin** role to mint). ([Quo API auth
  docs](https://www.quo.com/docs/mdx/api-reference/authentication)) The `OPENPHONE_*` secret
  name is historical — keep it as-is.
- Check **`OPENPHONE_FROM_NUMBER`** is a real, send-enabled number on your account, in
  E.164 format (e.g. `+15551234567`).
- Re-set whichever is wrong:

  ```
  npx wrangler secret put OPENPHONE_API_KEY
  ```

- **Success looks like HTTP 202** from the provider's send endpoint in the logs. Anything
  else (401/403 = bad key, 4xx on the number = bad from-number) tells you which half to fix.

> 🔧 **Customize:** If you use a different SMS provider, the secret names and the success
> status come from *that* provider's send API — check their docs for the expected response
> code and auth header.

---

## C-3 — Nothing logs at all on a test lead

**Symptom**
You fire a test lead from the Lead Ads Testing Tool, you are tailing the worker, and
**absolutely nothing appears in the log.** The worker is not even being reached.

**Likely cause**
**The webhook subscription lapsed.** The Meta app is no longer subscribed to the Page's
`leadgen` field, so Facebook has nowhere to send the "new lead" ping.

**Fix**

1. Re-subscribe the **Page** to the app for `leadgen`. The reliable way is one Graph API call
   with a Page access token that has `pages_manage_metadata`:
   `POST https://graph.facebook.com/v25.0/<PAGE_ID>/subscribed_apps` with
   `subscribed_fields=leadgen` (the [Graph API Explorer](https://developers.facebook.com/tools/explorer/)
   is the easy way to run it). Meta sometimes also surfaces a per-Page subscribe toggle in the
   dashboard, but that screen keeps moving, so the API call is the dependable path. This is a
   *separate* subscription from the app-to-`leadgen`-field subscription in the Webhooks product
   ([04](04-the-worker.md), step 5 of "Subscribing the webhook").
2. **Verify the handshake.** When you (re)subscribe, Facebook sends a GET verification
   request to your worker; the worker must echo back **`hub.challenge`** when the incoming
   `hub.verify_token` matches your **`FB_VERIFY_TOKEN`** secret. A successful subscription
   means the handshake passed. If it fails, confirm `FB_VERIFY_TOKEN` on the worker matches
   the verify token you entered in the app's webhook config, and that the worker's webhook
   URL is correct.

See [04-the-worker.md](04-the-worker.md) for where the verify-token handshake lives in the
worker code.

---

## C-4 — Ad won't deliver: "Certification Required" (code 2859024)

**Symptom**
Your ad or ad set refuses to deliver and Meta returns an error referencing certification.
It surfaces as a **"Certification Required"** error (NeatNow observed **error code `2859024`**;
this code could not be confirmed on Meta's
[error reference](https://developers.facebook.com/docs/graph-api/guides/error-handling/), so it
is reported here as observed in practice).

**Likely cause**
The **Non-Discrimination Policy has not been accepted** for the account (or you accepted it
recently and it is still propagating after an edit). Meta requires this acceptance before
certain ad types will run.

**Fix**

1. An **account admin** accepts the Non-Discrimination Policy in **Meta Business Suite →
   Settings** (older accounts: "Business Settings"; both open the same place — Meta surfaces
   the prompt where the policy acceptance lives for the account).
2. **Wait** — it can take **a few hours to clear** after acceptance (or after an edit that
   re-triggers it).
3. Re-check, then **re-activate the ad.**

---

## C-5 — Ad performance tanked right after an edit

**Symptom**
The ad was performing fine, you made a change, and cost-per-lead jumped / delivery dropped
right afterward.

**Likely cause**
**You reset the learning phase.** Meta re-enters the "learning" phase after meaningful
edits, during which delivery is unstable and CPL is often worse.

**Fix / avoid**

Significant edits (budget, targeting, creative) restart Meta's learning phase, during which
delivery is unstable. Change things sparingly and in batches, and let an ad set accumulate
conversions before judging it. (Meta doesn't publish an exact public threshold in
machine-readable docs; treat specific figures you see quoted online — like "50 conversions
per week" or "a 20% budget change resets learning" — as rules of thumb, not hard facts.)

---

## Quick reference

| Symptom | Likely cause | Section |
|---|---|---|
| Leads silently stopped, Meta still shows leads | Page token died (OAuth 190 / subcode 460) | [C-1](#c-1--leads-silently-stopped-meta-still-shows-leads-coming-in--the-headline-incident) |
| One side of the texting fails (quote or owner alert) | SMS key / from-number issue | [C-2](#c-2--auto-quote-sms-never-arrives-but-the-owner-notification-does-or-vice-versa) |
| Nothing logs on a test lead | Webhook subscription lapsed | [C-3](#c-3--nothing-logs-at-all-on-a-test-lead) |
| Ad won't deliver: "Certification Required" (2859024) | Non-Discrimination Policy not accepted | [C-4](#c-4--ad-wont-deliver-certification-required-code-2859024) |
| Performance tanked after an edit | Learning phase reset | [C-5](#c-5--ad-performance-tanked-right-after-an-edit) |

---

For the worker, secrets, and token setup, see **[04-the-worker.md](04-the-worker.md)**. For
the live go-live and monitoring procedure, see
**[06-go-live-and-monitor.md](06-go-live-and-monitor.md)**.
