← Developers

Quickstart

Send your first form via the API in under 5 minutes. We'll generate a key, create a form from a template, email it to a signer, and verify the webhook when the signer completes it.

1

Get an API key

Open /dashboard/developers, click Generate key, label it (e.g. “local-dev”), and pick Test mode for evaluation or Live mode for production. Copy the key — it's shown once.

Plan policy: free-trial accounts can only mint fk_test_* keys. fk_live_* keys require a paid plan.

Every request looks like this:

header
Authorization: Bearer fk_live_<your-32-hex-key>
2

List your templates

Templates are the reusable PDF forms you've already built in the dashboard. Pick one to instantiate.

curl
curl https://formfy.ai/api/v1/templates \
  -H "Authorization: Bearer fk_live_xxxxxxxxxxxxxxxx"

# → { "data": [{ "id": "tmpl_abc123", "name": "Client Intake", ... }] }
3

Create a form

Pass template_id and an optional prefill map. Keys in prefill that don't match a template field come back in the X-Formfy-Prefill-Unmatched response header so you can spot typos.

curl
curl https://formfy.ai/api/v1/forms \
  -H "Authorization: Bearer fk_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "template_id": "tmpl_abc123",
    "prefill": { "client_name": "Alex Doe", "service_date": "2026-06-01" }
  }'

# → { "id": "form_xyz789", "status": "draft", ... }
node
const res = await fetch('https://formfy.ai/api/v1/forms', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.FORMFY_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    template_id: 'tmpl_abc123',
    prefill: { client_name: 'Alex Doe', service_date: '2026-06-01' },
  }),
});
const form = await res.json();
console.log('Created', form.id);
python
import os, requests

resp = requests.post(
    'https://formfy.ai/api/v1/forms',
    headers={'Authorization': f"Bearer {os.environ['FORMFY_API_KEY']}"},
    json={
        'template_id': 'tmpl_abc123',
        'prefill': {'client_name': 'Alex Doe', 'service_date': '2026-06-01'},
    },
)
resp.raise_for_status()
form = resp.json()
print('Created', form['id'])
4

Send it to a signer

Use channel: "email" or channel: "sms". The same recipient can't be sent twice — re-sending returns form_already_sent.

curl
curl https://formfy.ai/api/v1/forms/form_xyz789/send \
  -H "Authorization: Bearer fk_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "recipients": [{
      "email": "alex@example.com",
      "channel": "email",
      "name": "Alex Doe"
    }]
  }'

# → { "id": "form_xyz789", "status": "sent", ... }
5

Subscribe to webhooks

Set up an endpoint on your side, then register it with Formfy. We'll POST event payloads to it. Five event types are available: form.created, form.sent, form.viewed, form.signed, form.expired.

curl
curl https://formfy.ai/api/v1/webhooks \
  -H "Authorization: Bearer fk_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.example.com/webhooks/formfy",
    "events": ["form.signed", "form.viewed"]
  }'

# → { "id": "wh_qwerty", "signing_secret": "whsec_...", "url": "...", ... }

The signing_secret is returned once. Store it — every delivery is HMAC-SHA256 signed with it.

6

Verify webhook signatures

Every webhook delivery includes Formfy-Signature: t=<unix_ts>,v1=<hmac_hex>. Recompute HMAC-SHA256 of "<t>.<rawBody>" using your signing secret and constant-time-compare. Reject if mismatch OR if t is more than 5 minutes from your clock (replay protection).

node
import crypto from 'crypto';

function verify(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map(p => p.split('=', 2))
  );
  const t = Number.parseInt(parts.t, 10);
  if (Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = crypto.createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(parts.v1)
  );
}
python
import hmac, hashlib, time

def verify(raw_body: str, signature_header: str, secret: str) -> bool:
    parts = dict(p.split('=', 1) for p in signature_header.split(','))
    t = int(parts['t'])
    if abs(time.time() - t) > 300:
        return False
    expected = hmac.new(
        secret.encode(),
        f"{t}.{raw_body}".encode(),
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, parts['v1'])
7

Retrieve the signed PDF

When you receive a form.signed webhook, fetch the final PDF. Two return shapes: ?format=url (default — 302 redirect to a presigned URL, expires in 1h) or ?format=base64 (JSON-wrapped binary).

curl
curl -L https://formfy.ai/api/v1/forms/form_xyz789/signed-pdf \
  -H "Authorization: Bearer fk_live_xxxxxxxxxxxxxxxx" \
  -o signed.pdf

# → 302 → https://formfy.ai/api/v1/files/pdf/<token> → application/pdf

You're shipping.

That covers the full lifecycle — create, send, signed, retrieve. Browse the rest of the API in the full reference.

Common gotchas

  • Test vs live partition. fk_test_* keys can only see test-mode forms; you cannot query live data with a test key (or vice versa).
  • Idempotency. POST endpoints are NOT idempotent in v1. Use webhooks for exactly-once delivery semantics on your side.
  • Rate limits. Per-endpoint sliding window + monthly quota. Watch theX-RateLimit-* and X-RateLimit-Monthly-* headers on every response.
  • Request IDs. Every response includes Formfy-Request-Id: req_<32hex> — quote it in support tickets.