npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@formbuilder-cloud/sdk

v0.1.1

Published

Typed TypeScript SDK for the formBuilder Cloud API: a headless form backend and form submissions API, generated from its OpenAPI 3.1 contract.

Readme

@formbuilder-cloud/sdk

Typed TypeScript SDK for the formBuilder Cloud API: a form builder API and headless forms backend. You create forms from formBuilder definitions, publish them, collect submissions through the form submissions API, and read them back. The types are generated from the API's OpenAPI 3.1 contract (GET https://api.formbuilder.online/v1/openapi.json), so every /v1 operation is typed. The client is a thin layer on top that adds auth, idempotency, retries and typed RFC 7807 errors.

Status: 0.1.0, not published yet. The package name is @formbuilder-cloud/sdk. It needs Node 20+ and works in modern browsers (see Browser vs Node). The fb CLI (@formbuilder-cloud/cli, cli/ in this repository) is built on this SDK; an MCP server is planned as another package.

Keywords: form builder api, form backend, form submissions api, headless forms, mcp.

30-second quickstart

npm i @formbuilder-cloud/sdk
export FORMBUILDER_API_KEY=fb_mgmt_…   # a write management key (dashboard → API keys)
export PROJECT_ID=…                    # a project id (dashboard, or `fb.projects.list()`)
import { FormBuilderCloud } from '@formbuilder-cloud/sdk'
const fb = new FormBuilderCloud() // reads FORMBUILDER_API_KEY
const form = await fb.forms.create(process.env.PROJECT_ID, { name: 'Contact', definition: [{ type: 'text', subtype: 'email', name: 'email', label: 'Email', required: true }] })
await fb.forms.publish(form.id)
for await (const s of fb.submissions.list(form.id)) console.log(s.id, s.accepted_at)

The form now accepts submissions at POST /v1/forms/{form.id}/submit with its collection key. Embed it with the render adapter, or submit from your server:

const { submission, replayed } = await fb.forms.submit(form.id, { values: { email: '[email protected]' } }, { collectionKey: 'fb_col_…' })

Authentication

new FormBuilderCloud({ apiKey?: string, baseUrl?: string })
  • apiKey is a management key (fb_mgmt_…, scope read or write) for every management operation, or a form's collection key (fb_col_…), which can only submit to that form. In Node it defaults to process.env.FORMBUILDER_API_KEY. The browser entry never reads process.
  • baseUrl defaults to https://api.formbuilder.online. In Node, FORMBUILDER_BASE_URL overrides the default (for example for staging). An explicit option always wins.
  • baseUrl must be https:. Plain http: is accepted only for loopback hosts (localhost, *.localhost, 127.0.0.0/8, [::1]), such as a local dev stack, so the bearer key never crosses a network in clear text. A base URL with credentials (user:pass@), a query or a fragment is refused too. Both checks run at construction, before any request, and throw a TypeError that doesn't repeat the value.
  • The key is sent as Authorization: Bearer <key>. It is never sent to the public operations (the published document, the OpenAPI document, the error catalog), never logged, and never included in an error message. String(fb), console.log(fb), util.inspect(fb) and JSON.stringify(fb) show only its kind, e.g. FormBuilderCloud { baseUrl: 'https://api.formbuilder.online', apiKey: fb_mgmt_[redacted] }.
  • In Node every request carries User-Agent: formbuilder-cloud-sdk/<version>. That is the only thing that identifies the SDK.
  • No telemetry. Importing the package or constructing a client sends nothing. A request is made only when you call a method, and only to baseUrl.

Other options: fetch (your own fetch, which receives a Request), retry (below), timeoutMs (per attempt, until the response headers arrive; default 30 000, 0 = none) and autoIdempotencyKey (default true).

Idempotency

POST /v1/forms/{f}/submit takes an Idempotency-Key (1 to 128 visible ASCII characters, required for production workspaces, kept for 24 h). It is the only operation in the contract that takes one today.

  • fb.forms.submit() generates a key (a random UUID) unless you pass { idempotencyKey }, and reuses it on every retry. The result tells you the key that was sent and whether the API replayed the original answer:

    const { submission, replayed, idempotencyKey } = await fb.forms.submit(formId, body, { collectionKey })
    // Retry the same submission later (another process, after a crash): same key, same body ⇒ the original 201.
    await fb.forms.submit(formId, body, { collectionKey, idempotencyKey })
  • The same key with a different body is 409 idempotency-conflict. If the workspace owner purged the submission within the key's 24 h, a retry with its key is 410 submission-purged and stores nothing: don't resend it under a new key automatically. Generate one key per logical submission (idempotencyKey() is exported).

  • The low-level client also gets a generated key on submit, unless you set one or pass autoIdempotencyKey: false. An invalid key is refused before anything is sent.

Retries

A request is retried only when sending it twice cannot do a second thing:

  • GET, HEAD and OPTIONS;
  • an operation the API's OpenAPI document marks x-fbc-idempotent: true, such as publish (an unchanged draft returns the active version), activate, renames and settings updates, revoking a key or cancelling a workspace deletion;
  • an operation marked x-fbc-idempotent: "idempotency-key" (submit), only when the request carries an Idempotency-Key. submit() always sends one.

The list is generated from the document when the SDK is built (RETRY_SAFE_OPERATIONS in src/generated/operations.ts). Any other request is sent once, whatever the error: creating a form, saving a draft (a repeat with the old If-Match would be 412) and most deletes (a repeat would be 404). Read the state back before repeating one yourself.

A request is retried after a network failure, a timeout, or a problem the error catalog marks retryable: rate-limited (429), internal (500), unavailable (503), upstream-timeout (504 from the proxy: the first request may have been processed, which is why only the operations above are retried), or a 502/503/504 from a proxy. It is never retried after quota-exceeded or any other 4xx. Retry-After is honoured. The defaults are { maxRetries: 2, baseDelayMs: 500, maxDelayMs: 8000, maxRetryAfterMs: 60000 }: at most 3 attempts, exponential backoff with jitter when there is no Retry-After, and no retry when Retry-After is longer than 60 s (the error carries retryAfter). Use retry: false to turn retries off. An AbortSignal you pass ({ signal }) stops the request at once.

Errors

Every API error is an RFC 7807 problem. The convenience methods throw a FormBuilderCloudError that carries its members:

| Member | Meaning | |---|---| | status | HTTP status | | type | the problem type URI, e.g. https://api.formbuilder.online/errors/not-found (it resolves to its docs page) | | slug | not-found, …: one of the generated ProblemSlugs, or a newer type's slug, or null for a non-problem body | | title, detail, instance | RFC 7807 | | hint | a short remedy (optional; don't parse it) | | errors | validation-failed: one { field, code, message } per field (code is a FieldErrorCode); publish 422s: definition errors | | extensions | any other member, e.g. active_version on the version 410s | | requestId | X-Request-Id, for support | | retryAfter | Retry-After in seconds, or null | | retryable | whether the identical request may succeed later (from the error catalog) |

import { isProblem, PROBLEM_CATALOG } from '@formbuilder-cloud/sdk'

try {
  await fb.forms.submit(formId, body, { collectionKey })
} catch (err) {
  if (isProblem(err, 'validation-failed')) for (const e of err.errors) console.log(e)
  else if (isProblem(err, 'version-retired', 'version-unknown')) reload(err.extensions.active_version)
  else throw err
}

isProblem(err, ...slugs) narrows err.slug. isFormBuilderCloudError and isConnectionError work across the ESM and CJS builds. A request that never got an answer throws FormBuilderCloudConnectionError (kind: 'network' | 'timeout', attempts). PROBLEM_SLUGS, PROBLEM_CATALOG and FIELD_ERROR_CODES are generated from GET /v1/errors.json. Every type is documented at its type URI.

Convenience methods

| Method | Operation | |---|---| | fb.forms.create(projectId, { name, definition? }) | POST /v1/projects/{p}/forms | | fb.forms.get(formId), fb.forms.list(projectId) | GET /v1/forms/{f}, GET /v1/projects/{p}/forms | | fb.forms.getDraft(formId), fb.forms.updateDraft(formId, definition, { ifMatch? }) | GET/PUT /v1/forms/{f}/draft (publish_check says what publish would answer) | | fb.forms.publish(formId) | POST /v1/forms/{f}:publish | | fb.forms.getPublished(formId) | GET /v1/forms/{f}/published (public, sent without the key) | | fb.forms.submit(formId, { values, meta? }, { collectionKey?, idempotencyKey? }) | POST /v1/forms/{f}/submit | | fb.submissions.list(formId, filters?), fb.submissions.get(id) | GET /v1/forms/{f}/submissions, GET /v1/submissions/{id} | | fb.projects.list(workspaceId?), fb.projects.create({ name }, { workspaceId? }) | a management key implies its own workspace (GET /v1/me) | | fb.me() | GET /v1/me |

Lists return a Paginator. for await (const item of list) walks every page through next_cursor, list.pages() yields whole pages, list.first() makes one request, and list.toArray(limit?) collects the items. Nothing is requested until you iterate.

Everything else: the typed low-level client

fb.api is an openapi-fetch client over the generated paths. Paths, parameters, bodies and responses are all typed, and it shares the auth, Idempotency-Key and retry behaviour:

const result = await fb.api.GET('/v1/workspaces/{ws}/usage', { params: { path: { ws } } })
const usage = fb.unwrap(result) // the data, or throws FormBuilderCloudError

The generated paths, components and operations types are exported too. paginate(fetchPage) turns any list operation into a Paginator.

Browser vs Node

| | Node 20+ (import/require) | Browser (browser condition, or @formbuilder-cloud/sdk/browser) | |---|---|---| | FORMBUILDER_API_KEY / FORMBUILDER_BASE_URL | read | never (no process) | | User-Agent | formbuilder-cloud-sdk/<version> | not set (browsers forbid it) | | Management keys | yes | refused: anything in a page is public | | What works | every operation | the published document and submit with the form's collection key (management routes don't answer other origins, CORS) |

The browser entry imports no Node built-ins. Both entries ship as ESM and CommonJS with their own type declarations. To render and submit a form in a page, the render adapter (@formbuilder-cloud/adapter, not published yet) is usually the better fit.

Versioning

The SDK follows semver. The API itself is versioned in its path (/v1): additive changes ship in /v1 and clients ignore members, enum values and problem types they don't know, while breaking changes ship only in /v2. Deprecations are announced with Deprecation/Sunset headers and in the API changelog. Regenerating the SDK from a newer contract is a minor release.

Licence

Proprietary (UNLICENSED): Copyright (c) 2026 Kevin Chappell. All rights reserved. You may use it only with the formBuilder Cloud service and under that service's terms; see LICENSE. No open-source licence is granted.