formdata.dev uses a two-key model: public keys for form submissions and secret keys for administration.
pk_)pk_ followed by a UUID without hyphens (e.g., pk_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4)crypto.randomUUID() with hyphens strippedThe public key appears in the submission URL:
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.
sk_)sk_ followed by a UUID without hyphens (e.g., sk_f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3)~/.config/formdata/credentialscrypto.randomUUID() with hyphens stripped, prefixed with sk_The secret key is used in two ways:
x-tenant-key headerAuthorization: Bearer sk_xxxYour 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 immediately revokes all existing keys for your organization and generates a new one.
To rotate via the CLI:
Expected output:
After rotation:
~/.config/formdata/credentials with the new key.401 errors.To rotate via MCP, use the rotate_key tool. The new key is returned in the response.
Each form has an allowedOrigins array that restricts which origins can submit to it.
[]): Allows submissions from any origin.["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.
The ingestion endpoint handles CORS dynamically:
OPTIONS preflight request to /v1/f/pk_xxx looks up the form config from KV.allowedOrigins is empty, the Access-Control-Allow-Origin header is set to *.403.Captcha can be enabled per-form. When enabled:
captchaSecret configured.x-captcha-token header with the submission request.400 error.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.
The admin API authenticates requests using the x-tenant-key header:
x-tenant-key header.organization_api_keys table, filtering for non-revoked keys.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.