API reference
The HTTP API behind our Zapier integration and MCP server: OAuth, reading forms and responses, and signed webhooks for every new response.
Deoochform has an HTTP API for reading forms and responses, and for subscribing to responses as they arrive. It is what our Zapier integration and our MCP server are built on, and it is open to anything else you want to connect.
Base URL is https://deoochform.com. Everything is JSON over HTTPS, and every request carries an OAuth bearer token. There are no API keys to paste anywhere.
Authentication
Standard OAuth 2.0 authorization code flow. Metadata is published at /.well-known/oauth-authorization-server, so a client that reads discovery documents needs no further configuration.
| Endpoint | What it does |
|---|---|
POST /register | Dynamic client registration (RFC 7591). Returns a client_id for a public client. Optional. |
GET /authorize | Sends the user to sign in and approve, then redirects back to your redirect_uri with a code. |
POST /token | Exchanges that code for an access token. Form encoded body, grant_type=authorization_code. |
Two kinds of client are supported. A public client, such as a desktop AI assistant that cannot keep a secret, proves itself with PKCE: send code_challenge and code_challenge_method=S256 at /authorize, then code_verifier at /token. A confidential client running on its own server sends a registered client_id and presents its client_secret at /token, as either HTTP Basic or body parameters. Confidential clients are registered by arrangement; write to us if you need one.
curl -X POST https://deoochform.com/token \
-H 'content-type: application/x-www-form-urlencoded' \
-d grant_type=authorization_code \
-d code=THE_CODE \
-d redirect_uri=YOUR_REDIRECT_URI \
-d code_verifier=YOUR_VERIFIER
{ "access_token": "dff_...", "token_type": "bearer" }Send the token as Authorization: Bearer <token> on every request. Tokens do not expire. Each one is listed at Settings, API tokens, named after the client that created it, and revoking one never affects the others.
Endpoints
Every endpoint below is scoped to the account that owns the token. There is no way to ask for another account's data, because no endpoint accepts a user id.
| Endpoint | What it does | Returns |
|---|---|---|
GET /api/zapier/me | The account behind the token. Use it to check a token is still valid. | { id, email, plan } |
GET /api/zapier/forms | Every form the token's owner has, newest first. Trashed forms are left out. | [{ id, label, title, slug, status }] |
GET /api/zapier/submissions | Recent responses, up to 25. Takes an optional formId query parameter. A form with no responses yet answers with one synthesised example so a new form can still be wired up. | [Submission] |
POST /api/zapier/subscribe | Subscribe to responses. Body is { hookUrl, formId }. hookUrl must be https. Omit formId, or send null, to receive every form on the account. | { id } |
DELETE /api/zapier/subscribe/{id} | Remove a subscription. Deleting one that is already gone still answers 200, so a retried call is safe. | { ok: true } |
The response object
One shape, used by both /api/zapier/submissions and the webhook body, built by the same code. Answers are keyed by field label, so a consumer shows answers.Email rather than a field id. Two fields sharing a label are numbered, Name and Name (2), in field order, so nothing is ever dropped.
The fields array carries the same answers with their ids and types, which is the stable handle if you care about types or if a label might be renamed. idis the response's own identifier and is what you should deduplicate on.
{
"id": "9f8c1b2a-0e44-4c7b-9b3e-2a5d6f7c8e10",
"submittedAt": "2026-08-05T10:00:00.000Z",
"form": {
"id": "3d1e5a70-1111-4222-8333-444455556666",
"title": "Contact us",
"url": "https://deoochform.com/f/contact-us"
},
"answers": {
"Email": "respondent@example.com",
"Name": "Ada Lovelace"
},
"fields": [
{ "id": "a1", "label": "Email", "type": "email", "value": "respondent@example.com" },
{ "id": "a2", "label": "Name", "type": "text", "value": "Ada Lovelace" }
],
"meta": { "ip": "203.0.113.10", "userAgent": "Mozilla/5.0", "country": "US", "city": "San Francisco" }
}Values are formatted the same way they appear in your CSV and XLSX exports, so a ranking arrives numbered and an address arrives whole.
Webhooks
Subscribe once and every new response is posted to your URL as it arrives, with the object above as the body. This is push, not polling: nothing is queued and nothing waits for a timer.
curl -X POST https://deoochform.com/api/zapier/subscribe \
-H 'authorization: Bearer YOUR_TOKEN' \
-H 'content-type: application/json' \
-d '{"hookUrl":"https://example.com/hooks/deoochform","formId":null}'
{ "id": "efc22a0c-647f-4e8c-be49-86af3b1664ef" }Keep that id. It is what you pass to DELETE /api/zapier/subscribe/{id} to stop delivery.
Every delivery is signed. X-Deooch-Signature is an HMAC SHA256 of the exact request body, using the secret returned when you subscribed, hex encoded. Verify it against the raw body before parsing, and compare using a timing safe comparison rather than ===.
Answer 410 Gone when you no longer want responses and the subscription deletes itself. That is the cleanest way to stop delivery to an endpoint you have retired, and it means a receiver that disappears is never posted to forever.
Errors
| Status | Meaning |
|---|---|
400 | The request body did not validate. The message says which part. |
401 | Missing, malformed, or revoked token. Send the user through /authorize again. |
404 | The form does not exist, or it is not yours. These are deliberately the same answer. |
500 | Our fault. Worth retrying. |
Questions
Frequently asked questions
- Do tokens expire?
- No. A token stays valid until you revoke it in Settings, API tokens. There is no refresh grant to call, and nothing to rotate on a timer. Revoking takes effect on the next request.
- Can one token read another account's forms?
- No. Every query is scoped to the account that owns the token, and no endpoint accepts a user id from the request. Subscribing to a form id you do not own answers 404 rather than quietly sending you someone else's responses.
- What happens to a webhook whose endpoint is down?
- Each failure is counted and the last error recorded. After a run of consecutive failures the subscription is disabled rather than retried forever. A 410 Gone response deletes it immediately, which is how a receiver tells us to stop for good. There is no retry queue today, so a delivery that fails is dropped rather than repeated.
- Is there a rate limit?
- Not today. Please keep polling reasonable. The subscription webhooks exist precisely so you do not have to poll, they push each response as it arrives.
- Do responses awaiting payment come through?
- No. A form that takes payment records the response as pending while the respondent is on the checkout page, and it becomes real once the payment confirms. Only confirmed responses are sent or listed, so abandoned checkouts never reach you.