> For the complete documentation index, see [llms.txt](https://docs.dapta.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.dapta.ai/dapta-forms/developers/public-api.md).

# 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](/dapta-forms/connect/webhooks.md), the [HubSpot](/dapta-forms/connect/hubspot.md) sync or [Export to CSV](/dapta-forms/results/export-csv.md).

***

## 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.

| Method and path                                          | What it does                                                                                                                                                                                                                            | Success                                                         |
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `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](/dapta-forms/share/public-page.md), 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 `400` with `{"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:

```bash
curl https://forms-api.dapta.ai/v1/public/forms/k7m2xq/lead-qualification-quiz
```

The real response for that form, trimmed to two of the four options on the first question:

```json
{
  "slug": "lead-qualification-quiz",
  "name": "Lead qualification quiz",
  "config": {
    "steps": [
      {
        "key": "multiple_choice_1",
        "type": "multiple_choice",
        "question": "How big is your team?",
        "helper": "Pick the option that fits best.",
        "required": true,
        "selectionMode": "single",
        "flowGroup": "qualification",
        "options": [
          { "label": "Just me", "value": "option_1", "points": 0 },
          { "label": "2 to 10 people", "value": "option_2", "points": 0 }
        ]
      },
      {
        "key": "email_2",
        "type": "email",
        "question": "What's your work email?",
        "required": true,
        "flowGroup": "lead_capture"
      },
      {
        "key": "name_3",
        "type": "name",
        "question": "And your name?",
        "helper": "Both fields, please. Thanks!",
        "required": true,
        "flowGroup": "lead_capture"
      }
    ],
    "version": 1
  }
}
```

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.

<figure><img src="https://3835013762-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FCy5rSNtQmtqYCGzJlNEB%2Fuploads%2Fgit-blob-814e24a43990c01a9e38dd247379ac788bb28317%2Fforms-public-api-01-openapi-docs.png?alt=media" alt="The /docs page showing the OpenAPI 3.1 document with a link to /openapi.json"><figcaption><p>The <code>/docs</code> page prints the OpenAPI document and links to the raw JSON.</p></figcaption></figure>

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:

| Default | Value                                                  |
| ------- | ------------------------------------------------------ |
| 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:

```
HTTP/1.1 429 Too Many Requests
Retry-After: 1
Content-Type: application/json

{"error":"RATE_LIMITED","message":"Too many requests. Please slow down."}
```

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_SEC` and `RATE_LIMIT_ENABLED`. If your API sits behind a proxy, also set `TRUST_PROXY_HOPS` to match, or every request will look like it comes from the proxy and your visitors will share one bucket. See [Feature flags & environment](/dapta-forms/developers/environment-flags.md).

***

## 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](/dapta-forms/share/embed.md) 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](/dapta-forms/connect/webhooks.md): get every submission pushed to you instead of polling for it.
* [Payload & headers reference](/dapta-forms/connect/webhooks/payload-reference.md): what a delivery contains.
* [Self-hosting](/dapta-forms/developers/self-hosting.md): run the API yourself.
* [Feature flags & environment](/dapta-forms/developers/environment-flags.md): the settings that change the limits above.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.dapta.ai/dapta-forms/developers/public-api.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
