Auth & Security

formdata.dev uses a two-key model: public keys for form submissions and secret keys for administration.

Key Types

Public Key (pk_)

  • Format: pk_ followed by a UUID without hyphens (e.g., pk_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4)
  • Purpose: Identifies a form in submission URLs
  • Visibility: Client-side, embedded in your HTML form action or JavaScript fetch call
  • Generation: crypto.randomUUID() with hyphens stripped
  • Scope: Each form gets its own public key

The public key appears in the submission URL:

POST https://api.formdata.dev/v1/f/pk_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4
INFO

Public keys are safe to expose in client-side code. They only allow submitting data to a form -- they cannot read, modify, or delete anything.

Secret Key (sk_)

  • Format: sk_ followed by a UUID without hyphens (e.g., sk_f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3)
  • Purpose: Authenticates admin API requests and MCP connections
  • Storage: SHA-256 hashed in the D1 database; the plaintext is never stored
  • Visibility: Shown once during account creation, then saved to ~/.config/formdata/credentials
  • Generation: crypto.randomUUID() with hyphens stripped, prefixed with sk_

The secret key is used in two ways:

  1. Admin API: Sent as x-tenant-key header
  2. MCP server: Sent as Authorization: Bearer sk_xxx
WARNING

Your secret key is displayed only once when the account is created or when keys are rotated. If you lose it, you must rotate to generate a new one (which invalidates the old key).

Key Rotation

Key rotation immediately revokes all existing keys for your organization and generates a new one.

To rotate via the CLI:

npx formdata-dev keys rotate

Expected output:

Rotate Secret Key ───────────────── Warning: This will immediately invalidate your current key. Warning: MCP connections and API integrations will break until reconfigured. Continue? (y/n): y Done: Secret key rotated New key: sk_newkey1234567890... Saved to: /home/you/.config/formdata/credentials Warning: The old key is now permanently invalid.

After rotation:

  1. The CLI automatically updates ~/.config/formdata/credentials with the new key.
  2. You must update your MCP client configuration with the new key.
  3. Any API integrations using the old key will receive 401 errors.

To rotate via MCP, use the rotate_key tool. The new key is returned in the response.

Origin Validation

Each form has an allowedOrigins array that restricts which origins can submit to it.

  • Empty array ([]): Allows submissions from any origin.
  • Non-empty array (e.g., ["https://example.com", "https://staging.example.com"]): Only requests with a matching Origin header are accepted. All others receive a 403 response.

The origin check happens before any payload processing, so invalid origins are rejected immediately.

CORS

The ingestion endpoint handles CORS dynamically:

  1. An OPTIONS preflight request to /v1/f/pk_xxx looks up the form config from KV.
  2. If the form exists and the request origin is allowed, the response includes:
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type, x-captcha-token
Access-Control-Max-Age: 86400
  1. If allowedOrigins is empty, the Access-Control-Allow-Origin header is set to *.
  2. If the origin is not allowed, the preflight returns 403.

Captcha Verification

Captcha can be enabled per-form. When enabled:

  1. The form must have a captchaSecret configured.
  2. The client must include an x-captcha-token header with the submission request.
  3. The Worker verifies the token against the captcha provider before processing.
  4. If the token is missing or invalid, the submission is rejected with a 400 error.
// Client-side example with captcha
const response = await fetch('https://api.formdata.dev/v1/f/pk_xxx', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-captcha-token': captchaToken
  },
  body: JSON.stringify({ name: 'Jane', email: 'jane@example.com' })
});
TIP

Captcha verification is optional. Leave verifyCaptcha set to false if your form does not need bot protection, or if you handle captcha validation on your own backend.

Authentication Flow (Admin API)

The admin API authenticates requests using the x-tenant-key header:

  1. The client sends the secret key in the x-tenant-key header.
  2. The server computes the SHA-256 hash of the provided key.
  3. The hash is looked up in the organization_api_keys table, filtering for non-revoked keys.
  4. If a matching, non-revoked key is found, the request is associated with the corresponding organization.
  5. If no match is found, the request receives a 401 response.

This approach means the plaintext key is never stored in the database. Even if the database is compromised, the actual secret keys cannot be recovered.