Quickstart
Install the Forms SDK, bind a form record to your own HTML, and store your first response
You need a Surface environment and an SDK form record. Create one in the dashboard (Forms, Create New Form, Build SDK form) or through the MCP server.
1. Install
npm install @surface-labs/forms-sdkpnpm and yarn work the same. ESM-first, zero runtime dependencies; init returns a promise, so use target: "esnext" for top-level await or wrap the call in an async function. Alternatively, self-host the prebuilt IIFE bundle and use the global SurfaceFormsSDK.
2. Get your IDs
init needs environmentId, formId, and apiBaseUrl. The get_sdk_binding_map MCP tool returns all three plus a ready-to-paste init snippet and every question and step ID; the form's page in the dashboard shows the same IDs.
A wrong apiBaseUrl is the number one boot failure. The default is https://forms.withsurface.com; any other deployment 404s until you pass its host. If init throws, the error names the URL it tried.
3. Write the HTML
One container per step, one data-question-id per question:
<div id="form" hidden>
<section data-step-id="step_about">
<label>
Work email
<input type="email" data-question-id="q_email" />
</label>
<button type="button" class="surface-next-button">Continue</button>
</section>
<section data-step-id="step_role" hidden>
<div data-question-id="q_role" data-question-type="MultipleOptionsQuestion">
<label><input type="radio" name="role" value="Sales Ops" /> Sales Ops</label>
<label><input type="radio" name="role" value="Leadership" /> Leadership</label>
</div>
<button type="button" class="surface-submit-button">Submit</button>
</section>
<section data-step-id="step_thanks" hidden><h2>Thanks!</h2></section>
</div>- Every step container after the first is authored
hidden; otherwise steps flash stacked during boot. - The choice question binds as a group:
data-question-typeon the container, eachvalueequal to the record's option key, nodata-field-nameon options. - The submit button sits on the last question step; the SDK reveals the thank-you step itself.
Full attribute vocabulary: binding contract.
4. Boot the SDK
import { SurfaceForms } from "@surface-labs/forms-sdk";
const form = await SurfaceForms.init({
environmentId: "env_xxx",
formId: "form_xxx",
apiBaseUrl: "https://forms.withsurface.com",
container: document.querySelector("#form"),
});
document.querySelector("#form").hidden = false;With the IIFE bundle, call SurfaceFormsSDK.SurfaceForms.init(...) the same way.
During development, add debug: true to log every captured answer, lifecycle event, and navigation intent. Remove before shipping.
5. Verify
Submit a test response, then check the Responses table (or get_response over MCP). SDK responses carry surfaceTagStatus: "sdk" in their metadata.
If an answer is missing, the browser console warns about unbound record questions. See troubleshooting for the full symptom table.