πPublic API & rate limits
The unauthenticated endpoints behind every public Dapta Form: fetch a published form, record a submission, a funnel event or a booking. Plus the OpenAPI document, the per IP rate limit and the 429 res
Every published Dapta Form is rendered from a small, open, unauthenticated API. The same endpoints your respondents' browsers call are available to you, which means you can fetch a published form's structure or post a submission from your own code.
On the hosted product the API lives at https://forms-api.dapta.ai. On a self-hosted deployment it is your own API origin.
β οΈ Note: There are no customer API keys today. Only the endpoints below are open. Everything else, meaning the whole dashboard surface (creating forms, reading submissions, analytics, integrations), requires a signed in dashboard session and is not an integration surface you can call from a script. To get data out of Dapta Forms, use Webhooks, the HubSpot sync or Export to CSV.
The public endpoints
All of them live under /v1/public. {accountCode} is your workspace's account code and {slug} is the form slug, the same two values you can read from the public form URL forms.dapta.ai/{accountCode}/{handle}/{slug}. The API path does not include the handle.
GET /v1/public/forms/{accountCode}/{slug}
The published configuration of a form: its name and its steps, exactly as the public page renders them.
200, or 404 when there is no published form at that address
POST /v1/public/forms/{accountCode}/{slug}/submissions
Record a submission. Body: sessionId, data (an object of answers keyed by field key) and optional partial: true for a partial save.
201
POST /v1/public/forms/{accountCode}/{slug}/events
Record one funnel event. Body: sessionId, type (one of view, start, step_view, step_complete, partial_submit, submit) and optionally stepIndex and stepKey. This is what fills the drop-off chart in Analytics.
202
POST /v1/public/forms/{accountCode}/{slug}/booking
Record that a respondent booked a meeting through an outcome's scheduling embed, so the booking is tied to the same session.
202
GET /v1/public/profiles/{accountCode}/{handle}
A member's public page, or 404 when they have not turned one on.
200
GET /health
Liveness plus a database probe. Handy for a self-hosted deployment.
200
A few behaviours worth knowing before you post anything:
One submission per session. All the writes are keyed by your
sessionId. A partial save is upgraded in place to the complete submission when the same session finishes, so you never get two rows for one respondent.The score is always recomputed on the server from the published configuration. A client cannot assert its own score or outcome.
Answers are validated against the published form. A malformed body comes back as
400with{"error":"BAD_REQUEST","message":"..."}naming the first problem.
Fetching a published form
This is the one endpoint most people want. A plain GET, no headers:
The real response for that form, trimmed to two of the four options on the first question:
Note what comes back: only the published version. Unpublished draft edits are never served here, which is why the response is safe to cache and safe to read from a browser. Each step's key is the field key you will see again in a webhook payload and in a CSV export.
The OpenAPI document
Every deployment describes itself. Two addresses, both open:
https://forms-api.dapta.ai/openapi.json: the OpenAPI 3.1 document, machine readable. Point your client generator or your HTTP client at it.https://forms-api.dapta.ai/docs: the same document rendered as a page, with no external dependencies.

/docs page prints the OpenAPI document and links to the raw JSON.The document covers both the public endpoints above and the dashboard endpoints, which are marked as needing a session. On a self-hosted deployment the same two addresses sit on your own API origin.
Rate limits
The public surface is the only part of Dapta Forms an anonymous client can reach, so it is throttled per client IP address with a token bucket. The shipped defaults are:
Burst
60 requests
Refill
1 request per second
Scope
Per client IP address, across all the public endpoints
In practice: you can fire 60 requests back to back, after which you get roughly one request per second, and the allowance builds back up to 60 while you are idle. That is far above what a real respondent filling in a form ever needs, and low enough to make scraping and submission spam expensive.
When you go over it, the API answers 429 Too Many Requests with a Retry-After header in whole seconds:
Wait for Retry-After seconds and retry. Do not retry in a tight loop: every rejected request refills nothing.
β οΈ Note: On a self-hosted deployment these numbers are yours to change:
RATE_LIMIT_CAPACITY,RATE_LIMIT_REFILL_PER_SECandRATE_LIMIT_ENABLED. If your API sits behind a proxy, also setTRUST_PROXY_HOPSto match, or every request will look like it comes from the proxy and your visitors will share one bucket. See Feature flags & environment.
Calling the API from a browser
Requests from a page on another domain are subject to the usual browser origin rules. The hosted product allows the origins it serves forms from, which is why the embed snippet works on your site out of the box. On a self-hosted deployment you list the extra domains yourself in CORS_ORIGINS.
What's next
Webhooks: get every submission pushed to you instead of polling for it.
Payload & headers reference: what a delivery contains.
Self-hosting: run the API yourself.
Feature flags & environment: the settings that change the limits above.
Last updated