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

@workmates-app/forms-sdk

v1.0.0

Published

Submit contact and quote forms from your website to WorkMates (my.workmates-app.dk).

Readme

@workmates-app/forms-sdk

Submit contact and quote forms from any Next.js (or Node 18+) site directly into WorkMates. The signing key lives on your server — never in the browser — and submissions land in your WorkMates Leads inbox with push + in-app notifications to the admins you configure.

Why this exists

If you already have a marketing site (e.g. a tomrerar.dk Next.js app) with a contact form and a "get a quote" flow, you probably also have a spreadsheet, an inbox, or a third-party CRM where those submissions end up. This package lets you skip all of that: you post the form to your own Server Action, it forwards to WorkMates, and your team sees a new lead in the same tool they already use for customers, quotes, and invoices.

Install

npm install @workmates-app/forms-sdk
# or
pnpm add @workmates-app/forms-sdk
# or
yarn add @workmates-app/forms-sdk

Requires Node 18+. Works in any runtime with fetch and FormData (Node, Edge, Bun, Deno, Cloudflare Workers).

60-second setup

  1. In WorkMates, go to Organizations → Integrations → Forms and install the integration.

  2. Create a form template (e.g. quote) with the fields you want, then copy the template slug.

  3. Generate an API key. Copy the full key (wmk_live_…) — you won't see it again.

  4. In your site, add the key to .env.local on the server only:

    WORKMATE_API_KEY=wmk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  5. Create a Server Action that wraps the SDK, and point your existing form at it.

Entry points

The package ships three stable entry points:

| Import | Runtime | What you get | | --------------------------------------- | ----------- | ---------------------------------------------------- | | @workmates-app/forms-sdk | server only | submitToWorkmate + types | | @workmates-app/forms-sdk/server | server only | same as above (explicit) | | @workmates-app/forms-sdk/client | client | useWorkmateForm React hook, types only — no secret |

The server entry never imports React. The client entry never imports submitToWorkmate, so the signing key cannot be tree-shaken into a client bundle by accident.

Response shape

Every call resolves with an { data, error } envelope — never throws. This matches the convention used inside WorkMates itself:

type WorkmateSubmitResult = {
  data: { submissionId: string; receivedAt: string } | null
  error: {
    code:
      | 'unauthorized'
      | 'not_found'
      | 'validation_failed'
      | 'too_large'
      | 'rate_limited'
      | 'network_error'
      | 'timeout'
      | 'aborted'
      | 'server_error'
      | 'unknown'
    message: string
    status?: number
  } | null
}

Example: Next.js App Router (the tomrerar.dk setup)

This is the exact pattern used by the first production customer (tomrerar-dk-web). Both the contact and the get a quote forms follow the same shape.

1. Server Action — app/actions/submit-quote.ts

'use server'

import { submitToWorkmate } from '@workmates-app/forms-sdk/server'

export async function submitQuoteAction(formData: FormData) {
  const images = formData
    .getAll('images')
    .filter((f): f is File => f instanceof File && f.size > 0)

  return submitToWorkmate({
    apiKey: process.env.WORKMATE_API_KEY!,
    templateSlug: 'quote',
    data: {
      name: formData.get('name'),
      email: formData.get('email'),
      phone: formData.get('phone'),
      address: formData.get('address'),
      description: formData.get('description'),
      images,
    },
    sourceUrl: formData.get('__page_url')?.toString(),
  })
}

2. Client component — app/components/quote-form.tsx

'use client'

import { useWorkmateForm } from '@workmates-app/forms-sdk/client'
import { submitQuoteAction } from '@/app/actions/submit-quote'

export function QuoteForm() {
  const { submit, pending, result, reset } = useWorkmateForm(submitQuoteAction)

  if (result?.data) {
    return (
      <div className="success">
        <h3>Tak — vi vender tilbage hurtigst muligt.</h3>
        <button onClick={reset}>Send endnu en</button>
      </div>
    )
  }

  return (
    <form action={submit} encType="multipart/form-data" className="grid gap-4">
      <input name="name" placeholder="Navn" required />
      <input name="email" type="email" placeholder="Email" required />
      <input name="phone" type="tel" placeholder="Telefon" />
      <input name="address" placeholder="Adresse" />
      <textarea name="description" placeholder="Beskriv opgaven" required />
      <input name="images" type="file" accept="image/*" multiple />

      <button type="submit" disabled={pending}>
        {pending ? 'Sender…' : 'Send forespørgsel'}
      </button>

      {result?.error ? <p className="error">{result.error.message}</p> : null}
    </form>
  )
}

3. Contact form — app/actions/submit-contact.ts

Same pattern, different template slug:

'use server'

import { submitToWorkmate } from '@workmates-app/forms-sdk/server'

export async function submitContactAction(formData: FormData) {
  return submitToWorkmate({
    apiKey: process.env.WORKMATE_API_KEY!,
    templateSlug: 'contact',
    data: {
      name: formData.get('name'),
      email: formData.get('email'),
      message: formData.get('message'),
    },
  })
}

Example: Plain Node script / cron

import { submitToWorkmate } from '@workmates-app/forms-sdk'

const res = await submitToWorkmate({
  apiKey: process.env.WORKMATE_API_KEY!,
  templateSlug: 'quote',
  data: {
    name: 'Peter',
    email: '[email protected]',
    phone: '+4512345678',
    description: 'Renovering af køkken',
  },
})

if (res.error) {
  console.error('WorkMates rejected the submission:', res.error)
} else {
  console.log('Submitted as', res.data.submissionId)
}

Supported field values

The data map is keyed by your WorkMates template field slugs. Values can be:

  • Scalarsstring, number, boolean, or null / undefined (skipped).
  • Arrays — one multipart part per entry. Useful for multi-file uploads.
  • Files — a File, Blob, or { buffer, filename, contentType? }. Buffers accept ArrayBuffer, Uint8Array, or Node Buffer.

A missing field that the template marks as required will fail validation (422). A missing optional field is silently skipped.

Options

| Option | Default | Notes | | --------------- | ----------------------------- | ----------------------------------------------------------------------- | | apiKey | — | Required. wmk_live_… from WorkMates. | | templateSlug | — | Required. Lowercase + hyphens. | | data | — | Required. Keyed by template field slug. | | baseUrl | https://my.workmates-app.dk | Override for staging / private deployments. | | sourceUrl | — | Page URL the form was submitted from. Shown in the WorkMates Leads UI. | | timeoutMs | 30000 | Abort after this many ms. | | signal | — | Standard AbortSignal to cancel mid-flight. | | headers | — | Extra request headers (e.g. a trace id). | | signRequests | false | Reserved. WorkMates logs HMAC signatures today but does not enforce. | | fetch | globalThis.fetch | Override for tests. |

Security checklist

  • Never import @workmates-app/forms-sdk/server from a client component. The SDK's server entry is Node/Edge only and assumes the signing key is a server-only env var. The client entry intentionally does not expose submitToWorkmate.
  • Keep WORKMATE_API_KEY in the Server env. In Vercel / Netlify / AWS, make sure it is not prefixed with NEXT_PUBLIC_.
  • Use a separate key per site and per environment — you can revoke them independently in WorkMates.
  • For extra defence-in-depth you can forward the key through a reverse proxy and add your own origin / hCaptcha layer in front of the Server Action. WorkMates itself applies a 30-submissions-per-minute rate limit per key.

Error handling

The envelope returned from submitToWorkmate never throws for expected failures (4xx/5xx, network errors, timeouts). Unexpected programmer errors (missing apiKey, invalid templateSlug) also arrive as an error with code: 'unknown'. This means your Server Action can be as simple as:

const res = await submitToWorkmate({ … })
return res // client hook renders result.error / result.data

Versioning

Follows semver. The wire protocol (X-WorkMate-Key, multipart body, { submissionId, receivedAt } response) is versioned via the /api/v1/… URL path, so minor SDK releases will never require a server change in WorkMates.

License

MIT © WorkMates