Appearance
Forms
Create, retrieve, update, and delete forms.
Base URL: https://app.krafter.dev/api/v1
List Forms
Retrieve all forms for your team.
GET /formsRequired scope: forms:read
Example Request
bash
curl https://app.krafter.dev/api/v1/forms \
-H "Authorization: Bearer kr_live_abc123def456"Example Response
json
{
"data": [
{
"id": "b1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
"name": "Contact Form",
"slug": "contact",
"status": "active",
"endpoint_url": "/f/contact",
"fields": {},
"settings": {},
"notifications": {
"email_enabled": true,
"email_to": "team@yourcompany.com"
},
"submission_count": 42,
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T10:00:00Z"
},
{
"id": "c2d3e4f5-6a7b-8c9d-0e1f-2a3b4c5d6e7f",
"name": "Newsletter Signup",
"slug": "newsletter",
"status": "paused",
"endpoint_url": "/f/newsletter",
"fields": {},
"settings": {},
"notifications": {
"email_enabled": false,
"email_to": null
},
"submission_count": 128,
"created_at": "2025-01-10T08:00:00Z",
"updated_at": "2025-01-12T14:30:00Z"
}
]
}Create Form
Create a new form.
POST /formsRequired scope: forms:write
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name for the form |
slug | string | Yes | URL-friendly identifier. Must be globally unique across all teams on the platform — the public submission endpoint /f/{slug} resolves slugs without a team scope, so two teams cannot share the same slug. |
fields | object | No | Field definitions for the form builder |
settings | object | No | Form settings (e.g., rate_limit) |
notifications | object | No | Email notification settings |
spam_protection | object | No | Spam protection configuration |
status | string | No | One of "active", "paused", "archived". Defaults to "active". See Pausing a form. |
Example Request
bash
curl -X POST https://app.krafter.dev/api/v1/forms \
-H "Authorization: Bearer kr_live_abc123def456" \
-H "Content-Type: application/json" \
-d '{
"name": "Contact Form",
"slug": "contact",
"notifications": {
"email_enabled": true,
"email_to": "team@yourcompany.com"
},
"settings": {
"rate_limit": 10
}
}'Example Response
json
// 201 Created
{
"id": "b1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
"name": "Contact Form",
"slug": "contact",
"status": "active",
"endpoint_url": "/f/contact",
"fields": {},
"settings": {
"rate_limit": 10
},
"notifications": {
"email_enabled": true,
"email_to": "team@yourcompany.com"
},
"submission_count": 0,
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T10:00:00Z"
}WARNING
The slug must be globally unique across the platform, not just within your team. The public /f/{slug} endpoint resolves a form by slug without a team scope, so two teams cannot share the same slug. If the slug is already taken — by your team or by anyone else — the API returns 422 Unprocessable Entity with {"errors": {"slug": ["has already been taken"]}}.
Get Form
Retrieve details of a specific form, including its submission count.
GET /forms/:idRequired scope: forms:read
Example Request
bash
curl https://app.krafter.dev/api/v1/forms/b1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e \
-H "Authorization: Bearer kr_live_abc123def456"Example Response
json
{
"id": "b1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
"name": "Contact Form",
"slug": "contact",
"status": "active",
"endpoint_url": "/f/contact",
"fields": {},
"settings": {
"rate_limit": 10
},
"notifications": {
"email_enabled": true,
"email_to": "team@yourcompany.com"
},
"submission_count": 42,
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T10:00:00Z"
}Update Form
Update an existing form. Only include the fields you want to change.
PATCH /forms/:idRequired scope: forms:write
Request Body
| Field | Type | Description |
|---|---|---|
name | string | Display name |
slug | string | URL slug. Must be globally unique across the platform (see the create-form note). |
fields | object | Field definitions |
settings | object | Form settings |
notifications | object | Email notification settings |
spam_protection | object | Spam protection configuration |
status | string | One of "active", "paused", "archived". See Pausing a form. |
Example Request
bash
curl -X PATCH https://app.krafter.dev/api/v1/forms/b1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e \
-H "Authorization: Bearer kr_live_abc123def456" \
-H "Content-Type: application/json" \
-d '{
"name": "Contact Us",
"notifications": {
"email_enabled": true,
"email_to": "team@yourcompany.com,support@yourcompany.com"
}
}'Example Response
json
{
"id": "b1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
"name": "Contact Us",
"slug": "contact",
"status": "active",
"endpoint_url": "/f/contact",
"fields": {},
"settings": {
"rate_limit": 10
},
"notifications": {
"email_enabled": true,
"email_to": "team@yourcompany.com,support@yourcompany.com"
},
"submission_count": 42,
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-16T09:15:00Z"
}Delete Form
Permanently delete a form and all its submissions.
DELETE /forms/:idRequired scope: forms:write
Example Request
bash
curl -X DELETE https://app.krafter.dev/api/v1/forms/b1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e \
-H "Authorization: Bearer kr_live_abc123def456"Example Response
204 No ContentWARNING
This action is irreversible. All submissions, uploaded files, and webhooks associated with the form will be permanently deleted.
Pausing a form
A form's status field controls whether the public submission endpoint accepts new submissions. Set status: "paused" to temporarily reject submissions while keeping the form configuration and historical submissions intact. Set it back to "active" to resume.
| Status | Public POST /f/:slug | Visible in dashboard | Listed by GET /forms |
|---|---|---|---|
active | accepted (201) | yes | yes |
paused | rejected — 422 Unprocessable Entity {"error": "submission_failed"} | yes | yes |
archived | rejected — 422 Unprocessable Entity {"error": "submission_failed"} | yes | yes |
Paused/archived forms return the same 422 as a missing slug
The 422 response body for inactive forms is {"error": "submission_failed"} — identical to the response a real unknown slug gets. There is no separate form_inactive response: Forms.get_form_by_slug_global/1 filters status == "active" directly in the lookup query (lib/krafter/forms.ex:278), so any non-active form falls through the controller's nil branch. The submitter has no way to distinguish "slug doesn't exist" from "form is paused/archived" — both are deliberately indistinguishable to discourage slug enumeration.
Example: pause a form
bash
curl -X PATCH https://app.krafter.dev/api/v1/forms/b1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e \
-H "Authorization: Bearer kr_live_abc123def456" \
-H "Content-Type: application/json" \
-d '{"status": "paused"}'json
{
"id": "b1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
"name": "Contact Form",
"slug": "contact",
"status": "paused",
"endpoint_url": "/f/contact"
}Form Object
| Field | Type | Description |
|---|---|---|
id | string | Unique form identifier (UUID) |
name | string | Display name |
slug | string | URL slug used in the public endpoint |
status | string | Lifecycle status: "active", "paused", or "archived". Default "active". |
endpoint_url | string | Relative path to the public submission endpoint (/f/{slug}) |
fields | object | Field definitions configured via the form builder |
settings | object | Form settings (e.g., rate_limit) |
notifications | object | Email notification configuration |
submission_count | integer | Total number of submissions. Incremented atomically inside the same transaction as the submission insert; read-only via the API. |
created_at | string | ISO 8601 creation timestamp |
updated_at | string | ISO 8601 last update timestamp |
Error Responses
422 Unprocessable Entity
Returned when validation fails (e.g., duplicate slug, missing required fields):
json
{
"errors": {
"slug": ["has already been taken"]
}
}404 Not Found
Returned when the form ID does not exist or does not belong to your team:
json
{
"error": "Not found"
}