@workmates-app/forms-sdk
v1.0.0
Published
Submit contact and quote forms from your website to WorkMates (my.workmates-app.dk).
Maintainers
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-sdkRequires Node 18+. Works in any runtime with fetch and FormData (Node, Edge, Bun, Deno, Cloudflare Workers).
60-second setup
In WorkMates, go to Organizations → Integrations → Forms and install the integration.
Create a form template (e.g.
quote) with the fields you want, then copy the template slug.Generate an API key. Copy the full key (
wmk_live_…) — you won't see it again.In your site, add the key to
.env.localon the server only:WORKMATE_API_KEY=wmk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxCreate 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:
- Scalars —
string,number,boolean, ornull/undefined(skipped). - Arrays — one multipart part per entry. Useful for multi-file uploads.
- Files — a
File,Blob, or{ buffer, filename, contentType? }. Buffers acceptArrayBuffer,Uint8Array, or NodeBuffer.
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/serverfrom a client component. The SDK'sserverentry is Node/Edge only and assumes the signing key is a server-only env var. Thecliententry intentionally does not exposesubmitToWorkmate. - Keep
WORKMATE_API_KEYin the Server env. In Vercel / Netlify / AWS, make sure it is not prefixed withNEXT_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.dataVersioning
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
