# REST API reference | OneForm

> The OneForm REST API reference: authentication with API keys, errors and codes, rate limits, and every v1 endpoint for forms, prefilled links, form invites…

Source: https://oneform.si/developers/api/

---

1. [Home](https://oneform.si/)
2. [Developers](https://oneform.si/developers/)
3. REST API

[View as Markdown](https://oneform.si/developers/api.md)

Developers

# REST API

A small JSON API for one firm: read its forms, make prefilled links, email forms to clients, and subscribe URLs to events. Base URL: `https://app.oneform.si/api/v1`

[Get an API key](https://app.oneform.si/settings/api)[OpenAPI 3.1 spec](https://oneform.si/developers/openapi.json)

## Authentication

Every call needs an API key. An owner or admin makes one in **Settings → API keys**; it looks like `of_` followed by 43 characters and is shown once (OneForm keeps only its SHA-256 hash). Send it either way:

Headers

```bash
Authorization: Bearer of_…X-API-Key: of_…
```

- A key belongs to one firm. Everything it reads or changes is that firm's; another firm's ids answer 404, as if they didn't exist.
- A key acts for the person who made it. If they leave the firm, lose admin rights, or their account is disabled, the key is revoked.
- Revoking a key works at once: calls with it get 401, and its webhook subscriptions are removed.
- A firm can have 25 keys at a time.

Keep keys on the server

A key can read the firm's forms and email its clients. Never put it in a web page, a mobile app or a public repository.

## Errors

Errors are JSON with a sentence for people and a stable `code` for code. Branch on the code, not the message.

404 · application/json

```json
{  "error": "No form with that id.",  "code": "form_not_found"}
```

| Status | Code                   | When                                                                                                                     |
| ------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| 400    | invalid\_request       | The body isn't the JSON the endpoint expects.                                                                            |
| 400    | unknown\_trigger       | POST /hooks with a trigger that doesn't exist or isn't available.                                                        |
| 400    | invalid\_target        | target\_url isn't a public https URL of at most 2,000 characters.                                                        |
| 401    | unauthorized           | No key, a wrong key, or a revoked key (also when the person who made the key no longer manages the firm's integrations). |
| 403    | firm\_inactive         | The firm's account is suspended or closed.                                                                               |
| 404    | not\_found             | No subscription with that id made with this key.                                                                         |
| 404    | form\_not\_found       | No form with that id in this firm (another firm's ids look the same as missing ones).                                    |
| 404    | unknown\_trigger       | GET /triggers/{trigger}/sample for a trigger that doesn't exist or isn't available.                                      |
| 409    | too\_many\_hooks       | The firm already has the most subscriptions it can have.                                                                 |
| 409    | form\_not\_published   | POST /forms/{id}/send for a form that isn't published.                                                                   |
| 429    | rate\_limited          | Over the key's calls per minute, or too many calls with a wrong key from one address. See Retry-After.                   |
| 429    | invite\_limit\_reached | Over the firm's form invites for the day. Retry-After says when it resets (midnight UTC).                                |
| 502    | send\_failed           | The invite email couldn't be sent. Try again in a few minutes.                                                           |

## Rate limits

- **120 calls a minute per key**, counted across every OneForm server.
- More than 20 calls a minute with a wrong key from one address also get 429.
- `POST /forms/{id}/send` has its own limit: 300 invites a day per firm, reset at midnight UTC (`invite_limit_reached`).

Over a limit, the answer is `429` with a `Retry-After` header in seconds. Wait that long before trying again.

429

```text
HTTP/1.1 429 Too Many RequestsRetry-After: 23Content-Type: application/json {"error":"Over the limit of 120 calls a minute for this key.","code":"rate_limited"}
```

## Versioning and pagination

This is version 1 of the API, and the version is in the path (`/api/v1`). Write clients that ignore fields they don't know, and branch on error codes rather than messages.

There is no pagination in v1: `GET /forms` answers with the newest 500 forms, and `GET /hooks` with the subscriptions the key made (a firm has at most 100).

## Account

### Test the key

GET`/me`

Which firm and key the call was made with. Use it to test a connection and to label it with the firm's name.

Example request

curl

```bash
curl https://app.oneform.si/api/v1/me \  -H "Authorization: Bearer $ONEFORM_API_KEY"
```

Node

```js
const res = await fetch("https://app.oneform.si/api/v1/me", {  headers: {    Authorization: `Bearer ${process.env.ONEFORM_API_KEY}`,  },});if (!res.ok) throw new Error(`OneForm answered ${res.status}: ${await res.text()}`);const data = await res.json();
```

Python

```python
import osimport requests res = requests.get(    "https://app.oneform.si/api/v1/me",    headers={"Authorization": f"Bearer {os.environ['ONEFORM_API_KEY']}"},    timeout=30,)res.raise_for_status()data = res.json()
```

PHP

```php
<?php$ch = curl_init("https://app.oneform.si/api/v1/me");curl_setopt_array($ch, [    CURLOPT_CUSTOMREQUEST => "GET",    CURLOPT_HTTPHEADER => [        "Authorization: Bearer " . getenv("ONEFORM_API_KEY"),    ],    CURLOPT_RETURNTRANSFER => true,]);$data = json_decode(curl_exec($ch), true);$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
```

Go

```go
package main import (	"fmt"	"io"	"net/http"	"os") func main() {	req, err := http.NewRequest("GET", "https://app.oneform.si/api/v1/me", nil)	if err != nil {		panic(err)	}	req.Header.Set("Authorization", "Bearer "+os.Getenv("ONEFORM_API_KEY"))	res, err := http.DefaultClient.Do(req)	if err != nil {		panic(err)	}	defer res.Body.Close()	out, _ := io.ReadAll(res.Body)	fmt.Println(res.Status, string(out))}
```

Ruby

```ruby
require "net/http"require "json" uri = URI("https://app.oneform.si/api/v1/me")req = Net::HTTP::Get.new(uri)req["Authorization"] = "Bearer #{ENV.fetch('ONEFORM_API_KEY')}"res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }data = JSON.parse(res.body)
```

Response 200

200 · application/json

```json
{  "firm": {    "id": "00000000-0000-4000-8000-0000000000aa",    "name": "Lee & Partners CPA"  },  "key": {    "id": "00000000-0000-4000-8000-0000000000bb",    "name": "Zapier",    "prefix": "of_AbC123"  }}
```

Errors

| 401 | unauthorized   | No key, a wrong key, or a revoked key (also when the person who made the key no longer manages the firm's integrations). |
| --- | -------------- | ------------------------------------------------------------------------------------------------------------------------ |
| 403 | firm\_inactive | The firm's account is suspended or closed.                                                                               |
| 429 | rate\_limited  | Over the key's calls per minute, or too many calls with a wrong key from one address. See Retry-After.                   |

## Triggers

Triggers are the events you can subscribe to: `form_submitted`, `partial_submission`, `report_approved`, `report_sent`, `report_opened`, `payment_received`, `booking_made`, `booking_rescheduled`, `booking_cancelled`, `lead_hot`. See [Webhooks](https://oneform.si/developers/webhooks/#triggers) for what each one means.

### List triggers

GET`/triggers`

Every trigger a subscription can listen to, with a label, a hint and the address of its sample. These are the triggers OneForm's workflows use, as the workflow builder offers them, so a new trigger appears here without a new API version. The example shows the first two.

Example request

curl

```bash
curl https://app.oneform.si/api/v1/triggers \  -H "Authorization: Bearer $ONEFORM_API_KEY"
```

Node

```js
const res = await fetch("https://app.oneform.si/api/v1/triggers", {  headers: {    Authorization: `Bearer ${process.env.ONEFORM_API_KEY}`,  },});if (!res.ok) throw new Error(`OneForm answered ${res.status}: ${await res.text()}`);const data = await res.json();
```

Python

```python
import osimport requests res = requests.get(    "https://app.oneform.si/api/v1/triggers",    headers={"Authorization": f"Bearer {os.environ['ONEFORM_API_KEY']}"},    timeout=30,)res.raise_for_status()data = res.json()
```

PHP

```php
<?php$ch = curl_init("https://app.oneform.si/api/v1/triggers");curl_setopt_array($ch, [    CURLOPT_CUSTOMREQUEST => "GET",    CURLOPT_HTTPHEADER => [        "Authorization: Bearer " . getenv("ONEFORM_API_KEY"),    ],    CURLOPT_RETURNTRANSFER => true,]);$data = json_decode(curl_exec($ch), true);$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
```

Go

```go
package main import (	"fmt"	"io"	"net/http"	"os") func main() {	req, err := http.NewRequest("GET", "https://app.oneform.si/api/v1/triggers", nil)	if err != nil {		panic(err)	}	req.Header.Set("Authorization", "Bearer "+os.Getenv("ONEFORM_API_KEY"))	res, err := http.DefaultClient.Do(req)	if err != nil {		panic(err)	}	defer res.Body.Close()	out, _ := io.ReadAll(res.Body)	fmt.Println(res.Status, string(out))}
```

Ruby

```ruby
require "net/http"require "json" uri = URI("https://app.oneform.si/api/v1/triggers")req = Net::HTTP::Get.new(uri)req["Authorization"] = "Bearer #{ENV.fetch('ONEFORM_API_KEY')}"res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }data = JSON.parse(res.body)
```

Response 200

200 · application/json

```json
[  {    "key": "form_submitted",    "label": "Form is submitted",    "hint": "Runs as soon as someone finishes the form.",    "sample_url": "https://app.oneform.si/api/v1/triggers/form_submitted/sample"  },  {    "key": "partial_submission",    "label": "Form is left unfinished",    "hint": "Runs once when someone answers some questions, then stops for an hour without submitting. Stops if they finish later.",    "sample_url": "https://app.oneform.si/api/v1/triggers/partial_submission/sample"  }]
```

Errors

| 401 | unauthorized   | No key, a wrong key, or a revoked key (also when the person who made the key no longer manages the firm's integrations). |
| --- | -------------- | ------------------------------------------------------------------------------------------------------------------------ |
| 403 | firm\_inactive | The firm's account is suspended or closed.                                                                               |
| 429 | rate\_limited  | Over the key's calls per minute, or too many calls with a wrong key from one address. See Retry-After.                   |

### Sample event

GET`/triggers/{trigger}/sample`

One event in exactly the shape a webhook delivers, as a one-item array (what Zapier calls a “perform list”). It's built from the firm's latest submission, of `form_id` when given; a firm with no submissions yet gets made-up data.

Parameters

| Name             | Type   | Description                       |
| ---------------- | ------ | --------------------------------- |
| triggerrequired  | string | A trigger key from GET /triggers. |
| form\_idoptional | uuid   | Only this form's submissions.     |

Example request

curl

```bash
curl https://app.oneform.si/api/v1/triggers/form_submitted/sample \  -H "Authorization: Bearer $ONEFORM_API_KEY"
```

Node

```js
const res = await fetch("https://app.oneform.si/api/v1/triggers/form_submitted/sample", {  headers: {    Authorization: `Bearer ${process.env.ONEFORM_API_KEY}`,  },});if (!res.ok) throw new Error(`OneForm answered ${res.status}: ${await res.text()}`);const data = await res.json();
```

Python

```python
import osimport requests res = requests.get(    "https://app.oneform.si/api/v1/triggers/form_submitted/sample",    headers={"Authorization": f"Bearer {os.environ['ONEFORM_API_KEY']}"},    timeout=30,)res.raise_for_status()data = res.json()
```

PHP

```php
<?php$ch = curl_init("https://app.oneform.si/api/v1/triggers/form_submitted/sample");curl_setopt_array($ch, [    CURLOPT_CUSTOMREQUEST => "GET",    CURLOPT_HTTPHEADER => [        "Authorization: Bearer " . getenv("ONEFORM_API_KEY"),    ],    CURLOPT_RETURNTRANSFER => true,]);$data = json_decode(curl_exec($ch), true);$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
```

Go

```go
package main import (	"fmt"	"io"	"net/http"	"os") func main() {	req, err := http.NewRequest("GET", "https://app.oneform.si/api/v1/triggers/form_submitted/sample", nil)	if err != nil {		panic(err)	}	req.Header.Set("Authorization", "Bearer "+os.Getenv("ONEFORM_API_KEY"))	res, err := http.DefaultClient.Do(req)	if err != nil {		panic(err)	}	defer res.Body.Close()	out, _ := io.ReadAll(res.Body)	fmt.Println(res.Status, string(out))}
```

Ruby

```ruby
require "net/http"require "json" uri = URI("https://app.oneform.si/api/v1/triggers/form_submitted/sample")req = Net::HTTP::Get.new(uri)req["Authorization"] = "Bearer #{ENV.fetch('ONEFORM_API_KEY')}"res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }data = JSON.parse(res.body)
```

Response 200

200 · application/json

```json
[  {    "event": "form_submitted",    "form": {      "id": "00000000-0000-4000-8000-000000000001",      "title": "Tax check-up",      "url": "https://app.oneform.si/f/tax-check-up"    },    "response": {      "id": "00000000-0000-4000-8000-000000000002",      "submitted_at": "2026-10-01T15:04:05.000Z",      "device": "mobile",      "lang": "en",      "ending": "default"    },    "respondent": {      "name": "Maya Lee",      "first_name": "Maya",      "last_name": "Lee",      "email": "maya@example.com",      "phone": "+1 555 0100",      "company": ""    },    "answers": [      {        "id": "contact",        "question": "Your details",        "type": "contact_group",        "answer": "First name: Maya · Last name: Lee · Email: maya@example.com · Phone: +1 555 0100"      },      {        "id": "income",        "question": "Roughly what was your income last year?",        "type": "short_text",        "answer": "$120,000"      },      {        "id": "goal",        "question": "What would you like help with?",        "type": "long_text",        "answer": "Lower my taxes next year."      }    ],    "fields": {      "contact": "First name: Maya · Last name: Lee · Email: maya@example.com · Phone: +1 555 0100",      "income": "$120,000",      "goal": "Lower my taxes next year."    },    "variables": {},    "hidden": {      "utm_source": "newsletter"    },    "report": null  }]
```

Errors

| 401 | unauthorized     | No key, a wrong key, or a revoked key (also when the person who made the key no longer manages the firm's integrations). |
| --- | ---------------- | ------------------------------------------------------------------------------------------------------------------------ |
| 403 | firm\_inactive   | The firm's account is suspended or closed.                                                                               |
| 404 | form\_not\_found | No form with that id in this firm (another firm's ids look the same as missing ones).                                    |
| 404 | unknown\_trigger | GET /triggers/{trigger}/sample for a trigger that doesn't exist or isn't available.                                      |
| 429 | rate\_limited    | Over the key's calls per minute, or too many calls with a wrong key from one address. See Retry-After.                   |

## Forms

### List forms

GET`/forms`

The firm's forms, newest first, at most 500\. Each one lists the hidden fields its link accepts, so you can build inputs for them.

Example request

curl

```bash
curl https://app.oneform.si/api/v1/forms \  -H "Authorization: Bearer $ONEFORM_API_KEY"
```

Node

```js
const res = await fetch("https://app.oneform.si/api/v1/forms", {  headers: {    Authorization: `Bearer ${process.env.ONEFORM_API_KEY}`,  },});if (!res.ok) throw new Error(`OneForm answered ${res.status}: ${await res.text()}`);const data = await res.json();
```

Python

```python
import osimport requests res = requests.get(    "https://app.oneform.si/api/v1/forms",    headers={"Authorization": f"Bearer {os.environ['ONEFORM_API_KEY']}"},    timeout=30,)res.raise_for_status()data = res.json()
```

PHP

```php
<?php$ch = curl_init("https://app.oneform.si/api/v1/forms");curl_setopt_array($ch, [    CURLOPT_CUSTOMREQUEST => "GET",    CURLOPT_HTTPHEADER => [        "Authorization: Bearer " . getenv("ONEFORM_API_KEY"),    ],    CURLOPT_RETURNTRANSFER => true,]);$data = json_decode(curl_exec($ch), true);$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
```

Go

```go
package main import (	"fmt"	"io"	"net/http"	"os") func main() {	req, err := http.NewRequest("GET", "https://app.oneform.si/api/v1/forms", nil)	if err != nil {		panic(err)	}	req.Header.Set("Authorization", "Bearer "+os.Getenv("ONEFORM_API_KEY"))	res, err := http.DefaultClient.Do(req)	if err != nil {		panic(err)	}	defer res.Body.Close()	out, _ := io.ReadAll(res.Body)	fmt.Println(res.Status, string(out))}
```

Ruby

```ruby
require "net/http"require "json" uri = URI("https://app.oneform.si/api/v1/forms")req = Net::HTTP::Get.new(uri)req["Authorization"] = "Bearer #{ENV.fetch('ONEFORM_API_KEY')}"res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }data = JSON.parse(res.body)
```

Response 200

200 · application/json

```json
[  {    "id": "00000000-0000-4000-8000-000000000001",    "title": "Tax check-up",    "status": "published",    "created_at": "2026-10-01T09:00:00.000Z",    "slug": "tax-check-up",    "url": "https://app.oneform.si/f/tax-check-up",    "hidden_fields": [      {        "key": "client_id",        "label": "Client ID"      }    ]  }]
```

Errors

| 401 | unauthorized   | No key, a wrong key, or a revoked key (also when the person who made the key no longer manages the firm's integrations). |
| --- | -------------- | ------------------------------------------------------------------------------------------------------------------------ |
| 403 | firm\_inactive | The firm's account is suspended or closed.                                                                               |
| 429 | rate\_limited  | Over the key's calls per minute, or too many calls with a wrong key from one address. See Retry-After.                   |

### Get a form

GET`/forms/{id}`

One form, in the same shape.

Parameters

| Name       | Type | Description    |
| ---------- | ---- | -------------- |
| idrequired | uuid | The form's id. |

Example request

curl

```bash
curl https://app.oneform.si/api/v1/forms/00000000-0000-4000-8000-000000000001 \  -H "Authorization: Bearer $ONEFORM_API_KEY"
```

Node

```js
const res = await fetch("https://app.oneform.si/api/v1/forms/00000000-0000-4000-8000-000000000001", {  headers: {    Authorization: `Bearer ${process.env.ONEFORM_API_KEY}`,  },});if (!res.ok) throw new Error(`OneForm answered ${res.status}: ${await res.text()}`);const data = await res.json();
```

Python

```python
import osimport requests res = requests.get(    "https://app.oneform.si/api/v1/forms/00000000-0000-4000-8000-000000000001",    headers={"Authorization": f"Bearer {os.environ['ONEFORM_API_KEY']}"},    timeout=30,)res.raise_for_status()data = res.json()
```

PHP

```php
<?php$ch = curl_init("https://app.oneform.si/api/v1/forms/00000000-0000-4000-8000-000000000001");curl_setopt_array($ch, [    CURLOPT_CUSTOMREQUEST => "GET",    CURLOPT_HTTPHEADER => [        "Authorization: Bearer " . getenv("ONEFORM_API_KEY"),    ],    CURLOPT_RETURNTRANSFER => true,]);$data = json_decode(curl_exec($ch), true);$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
```

Go

```go
package main import (	"fmt"	"io"	"net/http"	"os") func main() {	req, err := http.NewRequest("GET", "https://app.oneform.si/api/v1/forms/00000000-0000-4000-8000-000000000001", nil)	if err != nil {		panic(err)	}	req.Header.Set("Authorization", "Bearer "+os.Getenv("ONEFORM_API_KEY"))	res, err := http.DefaultClient.Do(req)	if err != nil {		panic(err)	}	defer res.Body.Close()	out, _ := io.ReadAll(res.Body)	fmt.Println(res.Status, string(out))}
```

Ruby

```ruby
require "net/http"require "json" uri = URI("https://app.oneform.si/api/v1/forms/00000000-0000-4000-8000-000000000001")req = Net::HTTP::Get.new(uri)req["Authorization"] = "Bearer #{ENV.fetch('ONEFORM_API_KEY')}"res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }data = JSON.parse(res.body)
```

Response 200

200 · application/json

```json
{  "id": "00000000-0000-4000-8000-000000000001",  "title": "Tax check-up",  "status": "published",  "created_at": "2026-10-01T09:00:00.000Z",  "slug": "tax-check-up",  "url": "https://app.oneform.si/f/tax-check-up",  "hidden_fields": [    {      "key": "client_id",      "label": "Client ID"    }  ]}
```

Errors

| 401 | unauthorized     | No key, a wrong key, or a revoked key (also when the person who made the key no longer manages the firm's integrations). |
| --- | ---------------- | ------------------------------------------------------------------------------------------------------------------------ |
| 403 | firm\_inactive   | The firm's account is suspended or closed.                                                                               |
| 404 | form\_not\_found | No form with that id in this firm (another firm's ids look the same as missing ones).                                    |
| 429 | rate\_limited    | Over the key's calls per minute, or too many calls with a wrong key from one address. See Retry-After.                   |

### Create a prefilled link

POST`/forms/{id}/links`

A link to the form with hidden values filled in, such as a client id from your practice-management tool. Nothing is stored and nothing is sent. A link only carries the keys the form declares plus the five UTM tags; the rest come back in `ignored`. Values are cut to 300 characters.

Parameters

| Name       | Type | Description    |
| ---------- | ---- | -------------- |
| idrequired | uuid | The form's id. |

Body (JSON)

| Name           | Type   | Description                                                                    |
| -------------- | ------ | ------------------------------------------------------------------------------ |
| hiddenoptional | object | Hidden-field values, as { key: value }. Numbers and booleans are sent as text. |
| langoptional   | string | The language the form opens in: en, fr, es.                                    |

Example request

curl

```bash
curl -X POST https://app.oneform.si/api/v1/forms/00000000-0000-4000-8000-000000000001/links \  -H "Authorization: Bearer $ONEFORM_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "hidden": {    "client_id": "TD-1042",    "utm_source": "taxdome"  },  "lang": "en"}'
```

Node

```js
const res = await fetch("https://app.oneform.si/api/v1/forms/00000000-0000-4000-8000-000000000001/links", {  method: "POST",  headers: {    Authorization: `Bearer ${process.env.ONEFORM_API_KEY}`,    "Content-Type": "application/json",  },  body: JSON.stringify({    "hidden": {      "client_id": "TD-1042",      "utm_source": "taxdome"    },    "lang": "en"  }),});if (!res.ok) throw new Error(`OneForm answered ${res.status}: ${await res.text()}`);const data = await res.json();
```

Python

```python
import osimport requests res = requests.post(    "https://app.oneform.si/api/v1/forms/00000000-0000-4000-8000-000000000001/links",    headers={"Authorization": f"Bearer {os.environ['ONEFORM_API_KEY']}"},    json={        "hidden": {            "client_id": "TD-1042",            "utm_source": "taxdome",        },        "lang": "en",    },    timeout=30,)res.raise_for_status()data = res.json()
```

PHP

```php
<?php$ch = curl_init("https://app.oneform.si/api/v1/forms/00000000-0000-4000-8000-000000000001/links");curl_setopt_array($ch, [    CURLOPT_CUSTOMREQUEST => "POST",    CURLOPT_HTTPHEADER => [        "Authorization: Bearer " . getenv("ONEFORM_API_KEY"),        "Content-Type: application/json",    ],    CURLOPT_POSTFIELDS => json_encode([        "hidden" => [            "client_id" => "TD-1042",            "utm_source" => "taxdome",        ],        "lang" => "en",    ]),    CURLOPT_RETURNTRANSFER => true,]);$data = json_decode(curl_exec($ch), true);$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
```

Go

```go
package main import (	"fmt"	"io"	"net/http"	"os"	"strings") func main() {	body := strings.NewReader(`{	  "hidden": {	    "client_id": "TD-1042",	    "utm_source": "taxdome"	  },	  "lang": "en"	}`)	req, err := http.NewRequest("POST", "https://app.oneform.si/api/v1/forms/00000000-0000-4000-8000-000000000001/links", body)	if err != nil {		panic(err)	}	req.Header.Set("Authorization", "Bearer "+os.Getenv("ONEFORM_API_KEY"))	req.Header.Set("Content-Type", "application/json")	res, err := http.DefaultClient.Do(req)	if err != nil {		panic(err)	}	defer res.Body.Close()	out, _ := io.ReadAll(res.Body)	fmt.Println(res.Status, string(out))}
```

Ruby

```ruby
require "net/http"require "json" uri = URI("https://app.oneform.si/api/v1/forms/00000000-0000-4000-8000-000000000001/links")req = Net::HTTP::Post.new(uri)req["Authorization"] = "Bearer #{ENV.fetch('ONEFORM_API_KEY')}"req["Content-Type"] = "application/json"req.body = JSON.generate({  "hidden" => {    "client_id" => "TD-1042",    "utm_source" => "taxdome",  },  "lang" => "en",})res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }data = JSON.parse(res.body)
```

Response 201

201 · application/json

```json
{  "url": "https://app.oneform.si/f/tax-check-up?client_id=TD-1042&utm_source=taxdome&lang=en",  "form_id": "00000000-0000-4000-8000-000000000001",  "published": true,  "hidden": {    "client_id": "TD-1042",    "utm_source": "taxdome"  },  "ignored": []}
```

Errors

| 400 | invalid\_request | The body isn't the JSON the endpoint expects.                                                                            |
| --- | ---------------- | ------------------------------------------------------------------------------------------------------------------------ |
| 401 | unauthorized     | No key, a wrong key, or a revoked key (also when the person who made the key no longer manages the firm's integrations). |
| 403 | firm\_inactive   | The firm's account is suspended or closed.                                                                               |
| 404 | form\_not\_found | No form with that id in this firm (another firm's ids look the same as missing ones).                                    |
| 429 | rate\_limited    | Over the key's calls per minute, or too many calls with a wrong key from one address. See Retry-After.                   |

### Email a form

POST`/forms/{id}/send`

Emails someone a link to the form, in the firm's branding, from the firm's client emails. The answer is `202` with `status: "sent"`, or `"skipped"` and a `reason` when the address unsubscribed or bounced, the firm switched the email off, or the person already got this form today. Skips aren't errors: don't retry them.

Parameters

| Name       | Type | Description                                |
| ---------- | ---- | ------------------------------------------ |
| idrequired | uuid | The form's id. The form must be published. |

Body (JSON)

| Name                | Type   | Description                                                        |
| ------------------- | ------ | ------------------------------------------------------------------ |
| emailrequired       | string | Who gets the form.                                                 |
| first\_nameoptional | string | Used in the greeting.                                              |
| nameoptional        | string | Full name; its first word greets them when first\_name is missing. |
| messageoptional     | string | A short personal note in the email, up to 1,000 characters.        |
| hiddenoptional      | object | Hidden-field values for their link, as for prefilled links.        |
| langoptional        | string | The form's language: en, fr, es.                                   |

Example request

curl

```bash
curl -X POST https://app.oneform.si/api/v1/forms/00000000-0000-4000-8000-000000000001/send \  -H "Authorization: Bearer $ONEFORM_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "email": "maya@example.com",  "first_name": "Maya",  "hidden": {    "client_id": "TD-1042"  },  "message": "Here'\''s the short check-up we talked about."}'
```

Node

```js
const res = await fetch("https://app.oneform.si/api/v1/forms/00000000-0000-4000-8000-000000000001/send", {  method: "POST",  headers: {    Authorization: `Bearer ${process.env.ONEFORM_API_KEY}`,    "Content-Type": "application/json",  },  body: JSON.stringify({    "email": "maya@example.com",    "first_name": "Maya",    "hidden": {      "client_id": "TD-1042"    },    "message": "Here's the short check-up we talked about."  }),});if (!res.ok) throw new Error(`OneForm answered ${res.status}: ${await res.text()}`);const data = await res.json();
```

Python

```python
import osimport requests res = requests.post(    "https://app.oneform.si/api/v1/forms/00000000-0000-4000-8000-000000000001/send",    headers={"Authorization": f"Bearer {os.environ['ONEFORM_API_KEY']}"},    json={        "email": "maya@example.com",        "first_name": "Maya",        "hidden": {            "client_id": "TD-1042",        },        "message": "Here's the short check-up we talked about.",    },    timeout=30,)res.raise_for_status()data = res.json()
```

PHP

```php
<?php$ch = curl_init("https://app.oneform.si/api/v1/forms/00000000-0000-4000-8000-000000000001/send");curl_setopt_array($ch, [    CURLOPT_CUSTOMREQUEST => "POST",    CURLOPT_HTTPHEADER => [        "Authorization: Bearer " . getenv("ONEFORM_API_KEY"),        "Content-Type: application/json",    ],    CURLOPT_POSTFIELDS => json_encode([        "email" => "maya@example.com",        "first_name" => "Maya",        "hidden" => [            "client_id" => "TD-1042",        ],        "message" => "Here's the short check-up we talked about.",    ]),    CURLOPT_RETURNTRANSFER => true,]);$data = json_decode(curl_exec($ch), true);$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
```

Go

```go
package main import (	"fmt"	"io"	"net/http"	"os"	"strings") func main() {	body := strings.NewReader(`{	  "email": "maya@example.com",	  "first_name": "Maya",	  "hidden": {	    "client_id": "TD-1042"	  },	  "message": "Here's the short check-up we talked about."	}`)	req, err := http.NewRequest("POST", "https://app.oneform.si/api/v1/forms/00000000-0000-4000-8000-000000000001/send", body)	if err != nil {		panic(err)	}	req.Header.Set("Authorization", "Bearer "+os.Getenv("ONEFORM_API_KEY"))	req.Header.Set("Content-Type", "application/json")	res, err := http.DefaultClient.Do(req)	if err != nil {		panic(err)	}	defer res.Body.Close()	out, _ := io.ReadAll(res.Body)	fmt.Println(res.Status, string(out))}
```

Ruby

```ruby
require "net/http"require "json" uri = URI("https://app.oneform.si/api/v1/forms/00000000-0000-4000-8000-000000000001/send")req = Net::HTTP::Post.new(uri)req["Authorization"] = "Bearer #{ENV.fetch('ONEFORM_API_KEY')}"req["Content-Type"] = "application/json"req.body = JSON.generate({  "email" => "maya@example.com",  "first_name" => "Maya",  "hidden" => {    "client_id" => "TD-1042",  },  "message" => "Here's the short check-up we talked about.",})res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }data = JSON.parse(res.body)
```

Response 202

202 · application/json

```json
{  "ok": true,  "status": "sent",  "email_id": "00000000-0000-4000-8000-0000000000cc",  "url": "https://app.oneform.si/f/tax-check-up?client_id=TD-1042",  "ignored": []}
```

Errors

| 400 | invalid\_request       | The body isn't the JSON the endpoint expects.                                                                            |
| --- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| 401 | unauthorized           | No key, a wrong key, or a revoked key (also when the person who made the key no longer manages the firm's integrations). |
| 403 | firm\_inactive         | The firm's account is suspended or closed.                                                                               |
| 404 | form\_not\_found       | No form with that id in this firm (another firm's ids look the same as missing ones).                                    |
| 409 | form\_not\_published   | POST /forms/{id}/send for a form that isn't published.                                                                   |
| 429 | rate\_limited          | Over the key's calls per minute, or too many calls with a wrong key from one address. See Retry-After.                   |
| 429 | invite\_limit\_reached | Over the firm's form invites for the day. Retry-After says when it resets (midnight UTC).                                |
| 502 | send\_failed           | The invite email couldn't be sent. Try again in a few minutes.                                                           |

## Webhook subscriptions

REST hooks: subscribe a URL to a trigger, and OneForm POSTs each event to it. [Webhooks](https://oneform.si/developers/webhooks/) covers the payload, signatures and retries.

### List subscriptions

GET`/hooks`

The subscriptions made with this key, newest first. A key only sees its own.

Example request

curl

```bash
curl https://app.oneform.si/api/v1/hooks \  -H "Authorization: Bearer $ONEFORM_API_KEY"
```

Node

```js
const res = await fetch("https://app.oneform.si/api/v1/hooks", {  headers: {    Authorization: `Bearer ${process.env.ONEFORM_API_KEY}`,  },});if (!res.ok) throw new Error(`OneForm answered ${res.status}: ${await res.text()}`);const data = await res.json();
```

Python

```python
import osimport requests res = requests.get(    "https://app.oneform.si/api/v1/hooks",    headers={"Authorization": f"Bearer {os.environ['ONEFORM_API_KEY']}"},    timeout=30,)res.raise_for_status()data = res.json()
```

PHP

```php
<?php$ch = curl_init("https://app.oneform.si/api/v1/hooks");curl_setopt_array($ch, [    CURLOPT_CUSTOMREQUEST => "GET",    CURLOPT_HTTPHEADER => [        "Authorization: Bearer " . getenv("ONEFORM_API_KEY"),    ],    CURLOPT_RETURNTRANSFER => true,]);$data = json_decode(curl_exec($ch), true);$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
```

Go

```go
package main import (	"fmt"	"io"	"net/http"	"os") func main() {	req, err := http.NewRequest("GET", "https://app.oneform.si/api/v1/hooks", nil)	if err != nil {		panic(err)	}	req.Header.Set("Authorization", "Bearer "+os.Getenv("ONEFORM_API_KEY"))	res, err := http.DefaultClient.Do(req)	if err != nil {		panic(err)	}	defer res.Body.Close()	out, _ := io.ReadAll(res.Body)	fmt.Println(res.Status, string(out))}
```

Ruby

```ruby
require "net/http"require "json" uri = URI("https://app.oneform.si/api/v1/hooks")req = Net::HTTP::Get.new(uri)req["Authorization"] = "Bearer #{ENV.fetch('ONEFORM_API_KEY')}"res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }data = JSON.parse(res.body)
```

Response 200

200 · application/json

```json
[  {    "id": "00000000-0000-4000-8000-000000000005",    "trigger": "form_submitted",    "target_url": "https://example.com/oneform/webhook",    "form_id": null,    "created_at": "2026-10-01T09:00:00.000Z"  }]
```

Errors

| 401 | unauthorized   | No key, a wrong key, or a revoked key (also when the person who made the key no longer manages the firm's integrations). |
| --- | -------------- | ------------------------------------------------------------------------------------------------------------------------ |
| 403 | firm\_inactive | The firm's account is suspended or closed.                                                                               |
| 429 | rate\_limited  | Over the key's calls per minute, or too many calls with a wrong key from one address. See Retry-After.                   |

### Subscribe

POST`/hooks`

Subscribes a URL. Keep the id in the answer: it's how you unsubscribe. A firm can have 100 subscriptions.

Body (JSON)

| Name                | Type   | Description                                                                                                                   |
| ------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------- |
| target\_urlrequired | string | A public https URL, at most 2,000 characters, without a username or password. Its address is checked again at every delivery. |
| triggerrequired     | string | A trigger key. event is accepted in its place.                                                                                |
| form\_idoptional    | uuid   | Only this form's events. Leave it out for every form.                                                                         |

Example request

curl

```bash
curl -X POST https://app.oneform.si/api/v1/hooks \  -H "Authorization: Bearer $ONEFORM_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "target_url": "https://example.com/oneform/webhook",  "trigger": "form_submitted"}'
```

Node

```js
const res = await fetch("https://app.oneform.si/api/v1/hooks", {  method: "POST",  headers: {    Authorization: `Bearer ${process.env.ONEFORM_API_KEY}`,    "Content-Type": "application/json",  },  body: JSON.stringify({    "target_url": "https://example.com/oneform/webhook",    "trigger": "form_submitted"  }),});if (!res.ok) throw new Error(`OneForm answered ${res.status}: ${await res.text()}`);const data = await res.json();
```

Python

```python
import osimport requests res = requests.post(    "https://app.oneform.si/api/v1/hooks",    headers={"Authorization": f"Bearer {os.environ['ONEFORM_API_KEY']}"},    json={        "target_url": "https://example.com/oneform/webhook",        "trigger": "form_submitted",    },    timeout=30,)res.raise_for_status()data = res.json()
```

PHP

```php
<?php$ch = curl_init("https://app.oneform.si/api/v1/hooks");curl_setopt_array($ch, [    CURLOPT_CUSTOMREQUEST => "POST",    CURLOPT_HTTPHEADER => [        "Authorization: Bearer " . getenv("ONEFORM_API_KEY"),        "Content-Type: application/json",    ],    CURLOPT_POSTFIELDS => json_encode([        "target_url" => "https://example.com/oneform/webhook",        "trigger" => "form_submitted",    ]),    CURLOPT_RETURNTRANSFER => true,]);$data = json_decode(curl_exec($ch), true);$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
```

Go

```go
package main import (	"fmt"	"io"	"net/http"	"os"	"strings") func main() {	body := strings.NewReader(`{	  "target_url": "https://example.com/oneform/webhook",	  "trigger": "form_submitted"	}`)	req, err := http.NewRequest("POST", "https://app.oneform.si/api/v1/hooks", body)	if err != nil {		panic(err)	}	req.Header.Set("Authorization", "Bearer "+os.Getenv("ONEFORM_API_KEY"))	req.Header.Set("Content-Type", "application/json")	res, err := http.DefaultClient.Do(req)	if err != nil {		panic(err)	}	defer res.Body.Close()	out, _ := io.ReadAll(res.Body)	fmt.Println(res.Status, string(out))}
```

Ruby

```ruby
require "net/http"require "json" uri = URI("https://app.oneform.si/api/v1/hooks")req = Net::HTTP::Post.new(uri)req["Authorization"] = "Bearer #{ENV.fetch('ONEFORM_API_KEY')}"req["Content-Type"] = "application/json"req.body = JSON.generate({  "target_url" => "https://example.com/oneform/webhook",  "trigger" => "form_submitted",})res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }data = JSON.parse(res.body)
```

Response 201

201 · application/json

```json
{  "id": "00000000-0000-4000-8000-000000000005",  "trigger": "form_submitted",  "target_url": "https://example.com/oneform/webhook",  "form_id": null,  "created_at": "2026-10-01T09:00:00.000Z"}
```

Errors

| 400 | invalid\_request | The body isn't the JSON the endpoint expects.                                                                            |
| --- | ---------------- | ------------------------------------------------------------------------------------------------------------------------ |
| 400 | unknown\_trigger | POST /hooks with a trigger that doesn't exist or isn't available.                                                        |
| 400 | invalid\_target  | target\_url isn't a public https URL of at most 2,000 characters.                                                        |
| 401 | unauthorized     | No key, a wrong key, or a revoked key (also when the person who made the key no longer manages the firm's integrations). |
| 403 | firm\_inactive   | The firm's account is suspended or closed.                                                                               |
| 404 | form\_not\_found | No form with that id in this firm (another firm's ids look the same as missing ones).                                    |
| 409 | too\_many\_hooks | The firm already has the most subscriptions it can have.                                                                 |
| 429 | rate\_limited    | Over the key's calls per minute, or too many calls with a wrong key from one address. See Retry-After.                   |

### Unsubscribe

DELETE`/hooks/{id}`

Removes a subscription made with this key, with any deliveries still queued for it.

Parameters

| Name       | Type | Description            |
| ---------- | ---- | ---------------------- |
| idrequired | uuid | The subscription's id. |

Example request

curl

```bash
curl -X DELETE https://app.oneform.si/api/v1/hooks/00000000-0000-4000-8000-000000000005 \  -H "Authorization: Bearer $ONEFORM_API_KEY"
```

Node

```js
const res = await fetch("https://app.oneform.si/api/v1/hooks/00000000-0000-4000-8000-000000000005", {  method: "DELETE",  headers: {    Authorization: `Bearer ${process.env.ONEFORM_API_KEY}`,  },});if (!res.ok) throw new Error(`OneForm answered ${res.status}: ${await res.text()}`);const data = await res.json();
```

Python

```python
import osimport requests res = requests.delete(    "https://app.oneform.si/api/v1/hooks/00000000-0000-4000-8000-000000000005",    headers={"Authorization": f"Bearer {os.environ['ONEFORM_API_KEY']}"},    timeout=30,)res.raise_for_status()data = res.json()
```

PHP

```php
<?php$ch = curl_init("https://app.oneform.si/api/v1/hooks/00000000-0000-4000-8000-000000000005");curl_setopt_array($ch, [    CURLOPT_CUSTOMREQUEST => "DELETE",    CURLOPT_HTTPHEADER => [        "Authorization: Bearer " . getenv("ONEFORM_API_KEY"),    ],    CURLOPT_RETURNTRANSFER => true,]);$data = json_decode(curl_exec($ch), true);$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
```

Go

```go
package main import (	"fmt"	"io"	"net/http"	"os") func main() {	req, err := http.NewRequest("DELETE", "https://app.oneform.si/api/v1/hooks/00000000-0000-4000-8000-000000000005", nil)	if err != nil {		panic(err)	}	req.Header.Set("Authorization", "Bearer "+os.Getenv("ONEFORM_API_KEY"))	res, err := http.DefaultClient.Do(req)	if err != nil {		panic(err)	}	defer res.Body.Close()	out, _ := io.ReadAll(res.Body)	fmt.Println(res.Status, string(out))}
```

Ruby

```ruby
require "net/http"require "json" uri = URI("https://app.oneform.si/api/v1/hooks/00000000-0000-4000-8000-000000000005")req = Net::HTTP::Delete.new(uri)req["Authorization"] = "Bearer #{ENV.fetch('ONEFORM_API_KEY')}"res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }data = JSON.parse(res.body)
```

Response 200

200 · application/json

```json
{  "ok": true}
```

Errors

| 401 | unauthorized   | No key, a wrong key, or a revoked key (also when the person who made the key no longer manages the firm's integrations). |
| --- | -------------- | ------------------------------------------------------------------------------------------------------------------------ |
| 403 | firm\_inactive | The firm's account is suspended or closed.                                                                               |
| 404 | not\_found     | No subscription with that id made with this key.                                                                         |
| 429 | rate\_limited  | Over the key's calls per minute, or too many calls with a wrong key from one address. See Retry-After.                   |

[← PreviousOverview](https://oneform.si/developers/)[Next →Webhooks](https://oneform.si/developers/webhooks/)

Questions about the API, webhooks or embeds? [Ask a developer](https://oneform.si/contact/?topic=developers).

DevelopersREST API
- [Overview](https://oneform.si/developers/)
- [REST API](https://oneform.si/developers/api/)
- [Webhooks](https://oneform.si/developers/webhooks/)
- [Embed](https://oneform.si/developers/embed/)
- [AI agents (MCP)](https://oneform.si/developers/mcp/)
- [OAuth](https://oneform.si/developers/oauth/)
- [Zapier](https://oneform.si/developers/zapier/)
- [Form schema](https://oneform.si/developers/form-schema/)

Developers

- [Overview](https://oneform.si/developers/)
- [REST API](https://oneform.si/developers/api/)
- [Webhooks](https://oneform.si/developers/webhooks/)
- [Embed](https://oneform.si/developers/embed/)
- [AI agents (MCP)](https://oneform.si/developers/mcp/)
- [OAuth](https://oneform.si/developers/oauth/)
- [Zapier](https://oneform.si/developers/zapier/)
- [Form schema](https://oneform.si/developers/form-schema/)

[OpenAPI spec](https://oneform.si/developers/openapi.json)[Ask a developer](https://oneform.si/contact/?topic=developers)
