# Troubleshooting



Debugging an SDK form triangulates three views: the **browser console*&#x2A; (runtime warnings), &#x2A;*`validate_form_html`** (the static gate), and **response read-back** (what actually landed). When in doubt, read a response back.

## First moves [#first-moves]

1. `form.state()` in the console: `currentStepId`, `answers`, `responseId`, `finished`. `form.capabilities` shows what is wired.

2. Attach a firehose while reproducing:

   ```js
   ["viewed", "started", "stepChanged", "stepCompleted", "completed", "disqualified", "saved", "error"]
     .forEach((e) => form.on(e, (p) => console.log("[surface]", e, p)));
   ```

3. Re-run `validate_form_html` against the deployed HTML; drift since the last validation is the most common root cause after a record edit.

## Symptom table [#symptom-table]

| Symptom                                           | Likely cause                                                                                                      | Check                                                                      |
| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `init` throws "could not fetch runtime config"    | Wrong or missing `apiBaseUrl`                                                                                     | The error names the URL it tried; use the one from `get_sdk_binding_map`   |
| No responses at all                               | `preview: true` left in the deployed page                                                                         | Grep the page for `preview`; `saved` never fires in preview                |
| No responses at all                               | Form never published, so IDs don't match                                                                          | Console shows unbound-question warnings; run `publish_form`                |
| Some answers missing                              | Binding drift: a question ID unbound or misspelled                                                                | Console names the unbound IDs; `validate_form_html`                        |
| Some answers missing                              | Partial responses off and the visitor abandoned mid-form                                                          | `trackPartialResponses` in settings; expected                              |
| 400 "Fields are missing or incorrectly formatted" | Hand-built `setAnswer` with the wrong shape                                                                       | Compare with the binding map's `answerShape`                               |
| Choice answers wrong or empty                     | `value` attributes drifted from the record's option keys                                                          | `validate_form_html` warns on exactly this                                 |
| Same email accepted one day, rejected the next    | A fail-open verdict later resolving definitively; expected                                                        | Check `failed` on the verdict                                              |
| Thank-you never shows                             | No thank-you ending step, or no container for it                                                                  | A step with `endStepKind: "thank_you"` and its container                   |
| Disqualified visitors see the thank-you           | No disqualified ending step on the record                                                                         | `disqualify()` only reveals a `"disqualified"` ending                      |
| Steps don't switch                                | `data-step-id` values don't match record step IDs, or invalid goto                                                | Console warns on invalid `goToStep`                                        |
| Duplicate responses per visitor                   | `init` called again without `destroy()` (SPA remounts)                                                            | One engine per page life                                                   |
| GTM, GA4, or Meta events not firing               | Preview mode, or settings not set                                                                                 | Deployed non-preview page; `window.dataLayer` after a step submit          |
| Ad conversions not firing                         | Rules gate on trigger and URL conditions; terminal rules fire on completion only; queued until first `responseId` | Rules in the dashboard; watch for `saved` first                            |
| Enrichment fields empty                           | No enabled enrichment provider                                                                                    | `form.capabilities.enrichment`; `debug: true` warns at boot                |
| Scheduler says "No availability"                  | The event type has no bookable slots                                                                              | `list_schedulers` with `includeAvailability: true`                         |
| Lead journey empty                                | Journey ID never carried (or `journey: false`)                                                                    | Save payload's `meta` for `userJourneyId`                                  |
| Lead attribution missing                          | Identify blocked (preview, ad blocker), or stale SPA session                                                      | `form.identify()`; network tab for the identify call                       |
| Slower than the hosted form                       | A custom header or JSON content type added preflights                                                             | DevTools should show no `OPTIONS` requests                                 |
| Finished response stored as partial               | Page torn down mid-flight                                                                                         | `await form.submit()`, then `await form.flush()` before programmatic close |

## Reading a response back [#reading-a-response-back]

`list_responses` (newest first), then `get_response`. Confirm: every expected question, structured choice answers, `surfaceTagStatus: "sdk"`, and, for a booking step, `eventScheduled: true`. A response that looks right here ends the client-side investigation.

## When the record and the HTML disagree [#when-the-record-and-the-html-disagree]

`get_sdk_binding_map` is the arbiter: the `source: "live"` map is what the deployed SDK actually fetched; the draft map (default) is what the next publish makes true. A page validated against the draft but running against an older live record explains most "validator says fine, runtime disagrees" reports. Publish, or validate against `source: "live"`.

<Note>
  Still stuck? The npm package ships a longer runbook at `skills/surface-forms-sdk/debugging.md`, and `debug: true` narrates everything the SDK does.
</Note>
