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

@draggable/formeo-sdk

v0.2.0

Published

Typed JavaScript/TypeScript client for the Formeo REST API: forms, submissions and webhooks with an organization API key, plus webhook signature verification

Readme

@draggable/formeo-sdk

A typed JavaScript/TypeScript client for the Formeo REST API: forms, submissions, webhooks and document generation with an organization API key, webhook signature verification, typed errors and rate-limit-aware retries. It uses only fetch and has no runtime dependencies, so it runs on Node.js 22+, browsers, Deno, Bun and edge runtimes.

npm install @draggable/formeo-sdk

Preview. The SDK is a 0.x release: a new minor version (0.2, 0.3, ...) may change its API. npm's default range (^0.1.0) doesn't pick those up, so upgrading is your choice.

import { Formeo } from '@draggable/formeo-sdk'

// formeo.io (https://api.formeo.io is the default); self-hosted, add baseUrl: 'https://formeo.example.com'
const formeo = new Formeo({ apiKey: process.env.FORMEO_API_KEY })

const form = await formeo.forms.create({ name: 'Signup', schema: {} })
await formeo.forms.publish(form.id)
const { items } = await formeo.submissions.list({ formId: form.id })

// In a webhook receiver: verify, then parse (throws WebhookVerificationError)
import { constructWebhookEvent } from '@draggable/formeo-sdk/webhooks'
const event = await constructWebhookEvent({
  payload: await request.text(),
  headers: request.headers,
  secret: process.env.FORMEO_WEBHOOK_SECRET,
})

baseUrl defaults to https://api.formeo.io (HOSTED_API_ORIGIN). The API is at <baseUrl>/api; a URL already ending in /api is used as is, and https://formeo.io (the website, no API) means the hosted API (apiBaseUrl() in src/base-url.ts, shared with the MCP server). apiUrl overrides the derived API base URL. The SDK reads no environment variables.

Generate a document from a template and download it once signed:

const templateId = '0199f6a4-1a2b-7c3d-8e4f-5a6b7c8d9e0f' // a template of the organization
const document = await formeo.documents.generate({
  templateId,
  values: { client: 'Ada Lovelace' },
  recipients: [{ name: 'Ada Lovelace', email: '[email protected]', signerRoleKey: 'client' }],
})
// Each recipient's private link (also emailed when the server has email on), shown once
const { links } = await formeo.documents.send(document.id!)
console.log(links?.map((link) => link.url))
// Later, once every signer has signed (`pdf.status` is `ready`)
const pdf: Uint8Array = await formeo.documents.downloadPdf(document.id!)

Customer docs (methods, scopes, errors, retries, configuration): docs/public/sdk.md, served at /docs/sdk.

API

  • forms: list (projectId and tagId optional, 'none' for unassigned or untagged forms; tagId takes a list too, matched with tagMatch: 'any' (default) or 'all', and tagName names the tags instead, ignoring case), get, create, update, publish, schedule, unpublish, delete, getPublic (no key)
  • submissions: list (formId optional; formTagId/formTagName/formTagMatch filter by the forms' tags, as forms.list's tagId and tagName), listAll (a Paginator: for await over the items, or .pages()), get, getFile (an uploaded file's metadata and a 5-minute download URL), export (CSV), delete, deleteMany (up to 100 ids, POST /v1/submissions/bulk-delete), create (public submit, no key)
  • uploads: create (a presigned upload URL for one file of a file upload field) and upload (create, then PUT the file; a failed storage PUT throws code: 'upload_failed'), no key
  • schemas.form(): the form data JSON Schema (no key)
  • webhooks: eventTypes, list, get, create (returns the one-time secret; format formeo or slack, fixed at creation), update (PATCH), delete, rotateSecret, test, listDeliveries, listAllDeliveries (a Paginator), redeliver
  • templates (#665): list and listAll (built-in templates and your organization's own; locale, category, tag, q, source, archived; { items, pagination }), categories, get, createForm (a draft form from a template, forms:write), and on Pro and Enterprise create (save a form or form data as your organization's template), update, archive, unarchive (templates:write)
  • Document generation (#620, Pro and Enterprise; keys need documents:read/documents:write, and documents:export for documents.export): documentTemplates (list, listAll, get, create, update with the version read, archive, unarchive, duplicate), documentRecordTypes and documentRecords (list, listAll, get, create, update, archive, unarchive), documents (list, listAll, generate, get, updateSettings, addRecipient/updateRecipient/removeRecipient, send and createLink (link tokens returned once), revokeLinks, listComments, comment, replyToComment, resolveComment, events, versions, createVersion, compareVersions, downloadPdf (the signed PDF as a Uint8Array; 409 until stored; documents:export), regeneratePdf, export). Their lists answer { items, pagination } like the others (the API sends pagination next to data; PAGINATED_ENVELOPE in src/client.ts). The recipient link endpoints (/v1/document-links/:token) aren't methods: they are the recipient's own page, whose signing evidence is the recipient's browser and IP
  • verifyWebhookSignature(options) / constructWebhookEvent(options): check a delivery's Formeo-Webhook-Signature (Web Crypto, no key needed); also exported from @draggable/formeo-sdk/webhooks. Options: payload (raw body), headers, secret or secrets, toleranceSeconds (300), now. Throws WebhookVerificationError (reason)
  • request(operationId, { params, query, body, signal, requestId }): any operation in src/endpoints.ts

Calls (#139, src/response.ts): every method takes CallOptions last (signal, requestId) and returns a FormeoPromise, a Promise of the data whose withResponse() resolves to { data, status, requestId, rateLimit, warnings, headers } (then/catch return plain promises; the data promise is made on first use, so awaiting only withResponse() leaves no unhandled rejection). warnings (#401) is the envelope's warnings (ApiWarning[]: code, path, message), only the well-formed ones and only those three fields, undefined when there are none; codes the spec doesn't list are kept (ApiWarningCode is the spec's enum or any string). Additive: awaiting a call still resolves to the data alone. Only updateForm sends warnings today. requestId is sent as X-Request-Id on every attempt (REQUEST_ID_PATTERN, else a TypeError before sending); the API otherwise makes one up. signal is combined with the per-attempt timeout (AbortSignal.any, with a fallback) and also ends a retry wait; an abort rejects with AbortError (name 'AbortError', cause the signal's reason), never retried. Paginator requests page after page (ITERATOR_PAGE_SIZE 100 unless limit), stops after totalPages or an empty page, and drops items the previous page had (offset pages shift when new items arrive). When a page answers a nextCursor (submissions in the default order, #157) it asks for the next one by that cursor instead of its number, and stops when there is none; submissions.listAll({ cursor }) starts after one (not with page).

Errors: FormeoError (every one has requestId and, when the response had them, rateLimit), AbortError, AuthenticationError (401), PermissionError (403, requiredScope), NotFoundError, ValidationError (400/422), RateLimitError (429, retryAfter, rateLimit), ServerError (5xx), ConfigurationError (baseUrl/apiUrl isn't the Formeo API: an HTML page, a redirect, or a 2xx/404 without the JSON envelope; never retried), ConnectionError and TimeoutError. Messages never contain the key. Requests use redirect: 'manual', so a redirect is a ConfigurationError rather than a request that loses its key or goes elsewhere.

Input checks (#280): a path id that's empty, . or .. throws a TypeError before any request (each id is one encoded path segment, so .. can't turn forms/{formId}/submissions into every form's submissions); an apiKey with a plain http:// URL to another host throws unless allowInsecureHttp: true; timeoutMs and retry must be whole numbers in range; uploads.upload refuses a plain http storage URL from an https API, and any method but PUT; verifyWebhookSignature refuses a NaN or negative toleranceSeconds or now, reads at most 8 signatures and computes one HMAC per secret.

Retries (default { maxRetries: 2, maxDelayMs: 10_000 }, retry: false to disable): a 429 is retried after Retry-After for any method unless the wait exceeds maxDelayMs; 5xx, network errors and timeouts are retried with backoff for GET/PUT/DELETE only, never POST or PATCH. RateLimitError.rateLimit.scope is key, organization, user (a dashboard session) or ip. FormeoError.requestId is the response's X-Request-Id (the server sends one on every response, middleware/request-id.ts), or without a response the requestId the call sent.

Exports (src/index.ts): the Formeo class, FormeoPromise, Paginator, VERSION, API_KEY_PATTERN, REQUEST_ID_PATTERN, ENDPOINTS, redact, HOSTED_API_ORIGIN and apiBaseUrl (src/base-url.ts), the error classes (including ConfigurationError), the webhook helpers with WEBHOOK_ID_HEADER, WEBHOOK_TIMESTAMP_HEADER, WEBHOOK_SIGNATURE_HEADER and DEFAULT_WEBHOOK_TOLERANCE_SECONDS, and types: every type in src/types.ts (API objects, method params, webhook event types), FormeoOptions, CallOptions, RequestOptions, RetryOptions, WithResponse, ResponseInfo, Page, ApiKeyScope, Endpoint, OperationId, FormeoErrorCode, RateLimitInfo, VerifyWebhookOptions, WebhookHeaders, WebhookVerificationFailure and the generated paths, components, operations. ./webhooks (src/webhooks.ts) exports only the webhook helpers, WebhookVerificationError and their option types: import the event types from the package root.

Development

npm run build -w packages/sdk      # tsc → dist
npm test -w packages/sdk           # unit tests with a mocked fetch
npm run generate -w packages/sdk   # regenerate src/generated/openapi.ts from the spec

src/generated/openapi.ts is generated by openapi-typescript from the spec the server builds (npm run openapi -w packages/server -- sync-sdk-types); don't edit it by hand. The server's src/openapi/sdk.test.ts fails when it's stale, when an operation in src/endpoints.ts drifts from the spec (method, path, x-required-scope), and runs the SDK against the real app.

Packaging

npm pack -w packages/sdk (or npm publish) runs prepack, which cleans and rebuilds dist/. The tarball holds dist/ (ES modules and declarations; no tests or source maps, since src/ isn't shipped), README.md, LICENSE and package.json. Only ., ./webhooks and ./package.json are exported; deep imports (@draggable/formeo-sdk/dist/...) are refused. tsconfig.json is self-contained, so the package builds outside the monorepo.

npm run pack:smoke from the repo root (scripts/pack-smoke.mjs) packs it with the MCP server, installs the tarballs into a temp project outside the repo, calls a local fake API, verifies a signed webhook, checks deep imports are refused and the MIT LICENSE ships, and type-checks a consumer under NodeNext (with @types/node) and Bundler (browser lib, no Node types) with the library check on. npm run ci runs it.

npm run docs:samples type-checks every SDK code sample in the docs (docs/public/*.md, this README) and the app against the packed tarball, and the server's sdk-samples.test.ts runs the runnable ones against the real app with it (#241; docs/README.md, "SDK code samples").

Publishing (#236): 0.1.0 is on npm (first published 2026-10-27). Only the Publish packages workflow publishes it, from an sdk-v<version> tag; see docs/RELEASING-PACKAGES.md. The docs, the website (PLATFORM_FEATURE_NOTES in packages/common/src/plans.ts, the home, pricing, Products, Platform and About pages) and the dashboard's webhook verification tabs (WebhookManager.tsx) call it a preview; drop that once it reaches 1.0.

License

MIT, copyright Draggable llc.