# API REST

> Leia formulários e envios, mova envios para dentro e para fora do spam e exclua-os com uma chave secreta.

A API REST permite que o seu servidor leia formulários e envios, mova envios para dentro e para fora do spam e os exclua. Ela usa JSON via HTTPS em `https://ikiform.com/api/v1`. O documento OpenAPI 3.1 está em [/api/v1/openapi.json](https://ikiform.com/api/v1/openapi.json).

> Criar envios não faz parte de `/api/v1`. Para isso, envie para o endpoint do formulário com uma chave secreta; veja [A partir de um servidor](https://ikiform.com/pt-br/docs/guides/server.md).

## Chaves de API

Crie chaves no painel em **API keys**. Uma chave pertence a um workspace e só enxerga esse workspace. A chave completa é exibida uma única vez; o Ikiform guarda só o hash dela.

| Tipo | Prefixo | Uso |
| --- | --- | --- |
| Secreta | `ik_sk_` | Para servidores. Chama a API REST com os escopos que você escolher e pode criar envios. |
| Publicável | `ik_pub_` | Para navegadores. Só pode enviar formulários, e só a partir dos domínios que você lista na chave. |

### Escopos

| Escopo | Permite |
| --- | --- |
| `forms:read` | `GET /forms`, `GET /forms/:formId` |
| `submissions:read` | `GET /submissions`, `GET /submissions/:id` |
| `submissions:write` | `DELETE /submissions/:id`, `POST /submissions/:id/spam` e `/not-spam`, e envios para `/f/:formId` |

## Autenticação

Envie a chave secreta como token Bearer. A API é só de servidor para servidor: ela não envia headers de CORS e responde preflights do navegador com `405`, então uma chave secreta nunca funciona a partir de uma página web.

`Terminal`:

```bash
curl https://ikiform.com/api/v1/forms \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"
```

- Uma chave ausente, malformada, desconhecida, revogada ou expirada recebe `401 unauthorized` com `WWW-Authenticate: Bearer`.
- Uma chave publicável recebe `403 forbidden` (`Publishable keys can only submit forms.`). Uma chave sem o escopo necessário recebe `403 forbidden` com o escopo indicado na mensagem.
- As respostas são enviadas com `Cache-Control: no-store`.

Os exemplos com o SDK abaixo compartilham um único client:

`ikiform.ts`:

```ts
import { createClient } from "@ikiform/sdk";

const ikiform = createClient({ apiKey: process.env.IKIFORM_SECRET_KEY! });
```

## Montador de requisições

O montador de requisições interativo desta página monta uma requisição da API REST a partir de um endpoint, filtros e ids, e a mostra como código curl, fetch e SDK. Ele nunca envia nada: a API REST é de servidor para servidor e recusa requisições do navegador por design.

## Endpoints

| Método | Caminho | Escopo | Retorna |
| --- | --- | --- | --- |
| `GET` | [`/forms`](#list-forms) | `forms:read` | Lista formulários |
| `GET` | [`/forms/:formId`](#get-form) | `forms:read` | Obtém um formulário |
| `GET` | [`/submissions`](#list-submissions) | `submissions:read` | Lista envios |
| `GET` | [`/submissions/:id`](#get-submission) | `submissions:read` | Obtém um envio |
| `POST` | [`/submissions/:id/spam`](#mark-spam) | `submissions:write` | Marca como spam |
| `POST` | [`/submissions/:id/not-spam`](#mark-not-spam) | `submissions:write` | Marca como não spam |
| `DELETE` | [`/submissions/:id`](#delete-submission) | `submissions:write` | Exclui um envio |
| `GET` | `/openapi.json` | Nenhum | O documento OpenAPI |
| `GET` | `/health` | Nenhum | `{ "ok": true }` |

### Lista formulários

`GET /forms` com o escopo `forms:read`.

- Mais recentes primeiro. Aceita `limit` (de 1 a 100, padrão 25) e `cursor`.

**curl** (`Terminal`)

```bash
curl "https://ikiform.com/api/v1/forms?limit=10" \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"
```

**SDK** (`ikiform.ts`)

```ts
const page = await ikiform.forms.list({ limit: 10 });
```

**fetch** (`fetch.js`)

```js
const response = await fetch("https://ikiform.com/api/v1/forms?limit=10", {
  headers: { Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}` },
});
const body = await response.json();
```

**Resposta** (`200 OK`)

```json
{
  "object": "list",
  "data": [
    {
      "object": "form",
      "id": "frm_xxxxxxxxxxxxxxxx",
      "name": "Contact",
      "status": "active",
      "submission_count": 42,
      "created_at": "2026-10-01T09:30:00.000Z"
    }
  ],
  "has_more": true,
  "next_cursor": "c3ViXzAxazd2MmM5dDNlOG01cTR4NnowYTFiMmMz"
}
```

### Obtém um formulário

`GET /forms/:formId` com o escopo `forms:read`.

- Um id de formulário de outro workspace retorna `404`, exatamente como um que não existe.

**curl** (`Terminal`)

```bash
curl https://ikiform.com/api/v1/forms/frm_xxxxxxxxxxxxxxxx \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"
```

**SDK** (`ikiform.ts`)

```ts
const form = await ikiform.forms.get("frm_xxxxxxxxxxxxxxxx");
```

**fetch** (`fetch.js`)

```js
const response = await fetch("https://ikiform.com/api/v1/forms/frm_xxxxxxxxxxxxxxxx", {
  headers: { Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}` },
});
const body = await response.json();
```

**Resposta** (`200 OK`)

```json
{
  "object": "form",
  "id": "frm_xxxxxxxxxxxxxxxx",
  "name": "Contact",
  "status": "active",
  "submission_count": 42,
  "created_at": "2026-10-01T09:30:00.000Z"
}
```

### Lista envios

`GET /submissions` com o escopo `submissions:read`.

| Parâmetro | Valores | Padrão |
| --- | --- | --- |
| `form_id` | Um id de formulário | Todos os formulários do workspace |
| `status` | `received` ou `spam` | Ambos |
| `limit` | De 1 a 100 | 25 |
| `cursor` | `next_cursor` da página anterior | A primeira página |

- Mais recentes primeiro. Um `form_id` de outro workspace retorna uma lista vazia em vez de um erro, para que ids não possam ser sondados.

**curl** (`Terminal`)

```bash
curl "https://ikiform.com/api/v1/submissions?form_id=frm_xxxxxxxxxxxxxxxx&status=received&limit=50" \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"
```

**SDK** (`ikiform.ts`)

```ts
const page = await ikiform.submissions.list({
  formId: "frm_xxxxxxxxxxxxxxxx",
  status: "received",
  limit: 50,
});
```

**fetch** (`fetch.js`)

```js
const response = await fetch("https://ikiform.com/api/v1/submissions?form_id=frm_xxxxxxxxxxxxxxxx&status=received&limit=50", {
  headers: { Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}` },
});
const body = await response.json();
```

**Resposta** (`200 OK`)

```json
{
  "object": "list",
  "data": [
    {
      "object": "submission",
      "id": "sub_01k7v2c9t3e8m5q4x6z0a1b2c3",
      "form_id": "frm_xxxxxxxxxxxxxxxx",
      "status": "received",
      "fields": { "name": "Ada", "email": "ada@example.com" },
      "meta": {
        "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
        "origin": "https://example.com",
        "referrer": "https://example.com/contact",
        "country": "GB",
        "subject": null,
        "reply_to": "ada@example.com"
      },
      "spam": { "score": 0, "reasons": [] },
      "created_at": "2026-10-17T12:00:00.000Z"
    }
  ],
  "has_more": true,
  "next_cursor": "c3ViXzAxazd2MmM5dDNlOG01cTR4NnowYTFiMmMz"
}
```

### Obtém um envio

`GET /submissions/:id` com o escopo `submissions:read`.

- Ids de outro workspace retornam `404`, exatamente como ids que não existem.

**curl** (`Terminal`)

```bash
curl https://ikiform.com/api/v1/submissions/sub_01k7v2c9t3e8m5q4x6z0a1b2c3 \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"
```

**SDK** (`ikiform.ts`)

```ts
const submission = await ikiform.submissions.get("sub_01k7v2c9t3e8m5q4x6z0a1b2c3");
```

**fetch** (`fetch.js`)

```js
const response = await fetch("https://ikiform.com/api/v1/submissions/sub_01k7v2c9t3e8m5q4x6z0a1b2c3", {
  headers: { Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}` },
});
const body = await response.json();
```

**Resposta** (`200 OK`)

```json
{
  "object": "submission",
  "id": "sub_01k7v2c9t3e8m5q4x6z0a1b2c3",
  "form_id": "frm_xxxxxxxxxxxxxxxx",
  "status": "received",
  "fields": { "name": "Ada", "email": "ada@example.com" },
  "meta": {
    "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
    "origin": "https://example.com",
    "referrer": "https://example.com/contact",
    "country": "GB",
    "subject": null,
    "reply_to": "ada@example.com"
  },
  "spam": { "score": 0, "reasons": [] },
  "created_at": "2026-10-17T12:00:00.000Z"
}
```

### Marca como spam

`POST /submissions/:id/spam` com o escopo `submissions:write`.

- Move um envio recebido para spam, diminui o `submission_count` do formulário e cancela as entregas de webhook pendentes dele.
- Chamar em um envio que já é spam o retorna sem mudanças.

**curl** (`Terminal`)

```bash
curl -X POST https://ikiform.com/api/v1/submissions/sub_01k7v2c9t3e8m5q4x6z0a1b2c3/spam \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"
```

**SDK** (`ikiform.ts`)

```ts
const submission = await ikiform.submissions.markSpam("sub_01k7v2c9t3e8m5q4x6z0a1b2c3");
```

**fetch** (`fetch.js`)

```js
const response = await fetch("https://ikiform.com/api/v1/submissions/sub_01k7v2c9t3e8m5q4x6z0a1b2c3/spam", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}` },
});
const body = await response.json();
```

**Resposta** (`200 OK`)

```json
{
  "object": "submission",
  "id": "sub_01k7v2c9t3e8m5q4x6z0a1b2c3",
  "form_id": "frm_xxxxxxxxxxxxxxxx",
  "status": "spam",
  "fields": { "name": "Ada", "email": "ada@example.com" },
  "meta": {
    "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
    "origin": "https://example.com",
    "referrer": "https://example.com/contact",
    "country": "GB",
    "subject": null,
    "reply_to": "ada@example.com"
  },
  "spam": { "score": 0, "reasons": [] },
  "created_at": "2026-10-17T12:00:00.000Z"
}
```

### Marca como não spam

`POST /submissions/:id/not-spam` com o escopo `submissions:write`.

- Move o envio de volta para recebido, limpa a pontuação e os motivos de spam e envia `submission.created` para todos os endpoints de webhook correspondentes.
- Chamar em um envio recebido o retorna sem mudanças.

**curl** (`Terminal`)

```bash
curl -X POST https://ikiform.com/api/v1/submissions/sub_01k7v2c9t3e8m5q4x6z0a1b2c3/not-spam \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"
```

**SDK** (`ikiform.ts`)

```ts
const submission = await ikiform.submissions.markNotSpam("sub_01k7v2c9t3e8m5q4x6z0a1b2c3");
```

**fetch** (`fetch.js`)

```js
const response = await fetch("https://ikiform.com/api/v1/submissions/sub_01k7v2c9t3e8m5q4x6z0a1b2c3/not-spam", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}` },
});
const body = await response.json();
```

**Resposta** (`200 OK`)

```json
{
  "object": "submission",
  "id": "sub_01k7v2c9t3e8m5q4x6z0a1b2c3",
  "form_id": "frm_xxxxxxxxxxxxxxxx",
  "status": "received",
  "fields": { "name": "Ada", "email": "ada@example.com" },
  "meta": {
    "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
    "origin": "https://example.com",
    "referrer": "https://example.com/contact",
    "country": "GB",
    "subject": null,
    "reply_to": "ada@example.com"
  },
  "spam": { "score": 0, "reasons": [] },
  "created_at": "2026-10-17T12:00:00.000Z"
}
```

### Exclui um envio

`DELETE /submissions/:id` com o escopo `submissions:write`.

- Remove permanentemente o envio e as entregas de webhook dele. Não há como desfazer. Excluir de novo retorna `404`.

**curl** (`Terminal`)

```bash
curl -X DELETE https://ikiform.com/api/v1/submissions/sub_01k7v2c9t3e8m5q4x6z0a1b2c3 \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY"
```

**SDK** (`ikiform.ts`)

```ts
const result = await ikiform.submissions.delete("sub_01k7v2c9t3e8m5q4x6z0a1b2c3");
```

**fetch** (`fetch.js`)

```js
const response = await fetch("https://ikiform.com/api/v1/submissions/sub_01k7v2c9t3e8m5q4x6z0a1b2c3", {
  method: "DELETE",
  headers: { Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}` },
});
const body = await response.json();
```

**Resposta** (`200 OK`)

```json
{ "object": "submission", "id": "sub_01k7v2c9t3e8m5q4x6z0a1b2c3", "deleted": true }
```

## Objetos

Os objetos são JSON em snake_case com um campo `object`. Timestamps são ISO 8601 em UTC com milissegundos. Ids de envio são ordenados por tempo (`sub_` mais 26 caracteres); ids de formulário são aleatórios (`frm_` mais 16 caracteres).

| Campo do formulário | Descrição |
| --- | --- |
| `status` | `active`, `paused` ou `disabled` |
| `submission_count` | Envios recebidos, sem spam |

| Campo do envio | Descrição |
| --- | --- |
| `status` | `received` ou `spam` |
| `fields` | Os valores armazenados |
| `meta` | `user_agent`, `origin`, `referrer`, `country`, `subject`, `reply_to`. Todas as chaves estão presentes e qualquer valor pode ser `null`. O hash do IP nunca é retornado |
| `spam.score`, `spam.reasons` | A pontuação e os motivos de spam, como `honeypot` ou `origin_mismatch` |

## Paginação

As listas vêm das mais recentes para as mais antigas e usam cursores opacos.

1. Faça a requisição sem cursor.
2. Se `has_more` for true, envie `next_cursor` como `cursor` com os mesmos filtros.
3. Pare quando `next_cursor` for `null`.

As páginas ficam estáveis enquanto novos envios chegam: os novos aparecem na primeira página, nunca como duplicados mais abaixo. Não monte nem interprete cursores você mesmo; um cursor inválido retorna `400 invalid_request`.

**SDK** (`export.ts`)

```ts
for await (const submission of ikiform.submissions.iterate({ formId: "frm_xxxxxxxxxxxxxxxx" })) {
  console.log(submission.id, submission.fields);
}
```

**fetch** (`export.js`)

```js
let cursor = null;

do {
  const url = new URL("https://ikiform.com/api/v1/submissions");
  url.searchParams.set("limit", "100");
  if (cursor) url.searchParams.set("cursor", cursor);
  const response = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}` },
  });
  const page = await response.json();
  for (const submission of page.data) console.log(submission.id);
  cursor = page.has_more ? page.next_cursor : null;
} while (cursor);
```

## Erros

Os erros usam o mesmo envelope dos envios de formulário. Toda resposta tem um header `X-Request-Id`, que os erros repetem como `request_id`.

`403 Forbidden`:

```json
{
  "ok": false,
  "error": { "code": "forbidden", "message": "This API key is missing the forms:read scope." },
  "request_id": "req_01k7v2c9t3e8m5q4x6z0a1b2c3"
}
```

| Status | Código | Quando |
| --- | --- | --- |
| 400 | `invalid_request` | `limit`, `status`, `form_id` ou `cursor` é inválido; a mensagem indica qual |
| 401 | `unauthorized` | Chave ausente ou inválida |
| 403 | `forbidden` | Chave publicável, ou escopo ausente |
| 404 | `not_found` | Rota ou id desconhecido, ou um id de outro workspace |
| 405 | `method_not_allowed` | Um preflight do navegador |
| 429 | `rate_limited` | Acima do limite de taxa por chave |
| 500 | `server_error` | Algo falhou do nosso lado; informe o `request_id` |

## Limite de taxa

Cada chave pode fazer 100 requisições a cada 60 segundos. Acima do limite, a API retorna `429 rate_limited` com um header `Retry-After` em segundos. O SDK o expõe como `error.retryAfter`.
