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
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:
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.
Errors
Errors are JSON with a sentence for people and a stable code for code. Branch on the code, not the message.
{ "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}/sendhas 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.
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
/meWhich 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 https://app.oneform.si/api/v1/me \ -H "Authorization: Bearer $ONEFORM_API_KEY"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();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$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);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))}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
{ "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 for what each one means.
List triggers
/triggersEvery 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 https://app.oneform.si/api/v1/triggers \ -H "Authorization: Bearer $ONEFORM_API_KEY"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();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$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);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))}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
[ { "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
/triggers/{trigger}/sampleOne 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 https://app.oneform.si/api/v1/triggers/form_submitted/sample \ -H "Authorization: Bearer $ONEFORM_API_KEY"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();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$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);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))}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
[ { "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": "[email protected]", "phone": "+1 555 0100", "company": "" }, "answers": [ { "id": "contact", "question": "Your details", "type": "contact_group", "answer": "First name: Maya · Last name: Lee · Email: [email protected] · 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: [email protected] · 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
/formsThe 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 https://app.oneform.si/api/v1/forms \ -H "Authorization: Bearer $ONEFORM_API_KEY"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();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$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);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))}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
[ { "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
/forms/{id}One form, in the same shape.
Parameters
| Name | Type | Description |
|---|---|---|
idrequired | uuid | The form's id. |
Example request
curl https://app.oneform.si/api/v1/forms/00000000-0000-4000-8000-000000000001 \ -H "Authorization: Bearer $ONEFORM_API_KEY"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();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$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);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))}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
{ "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
/forms/{id}/linksA 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 -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"}'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();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$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);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))}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
{ "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
/forms/{id}/sendEmails 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 -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": "[email protected]", "first_name": "Maya", "hidden": { "client_id": "TD-1042" }, "message": "Here'\''s the short check-up we talked about."}'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": "[email protected]", "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();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": "[email protected]", "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$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" => "[email protected]", "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);package main import ( "fmt" "io" "net/http" "os" "strings") func main() { body := strings.NewReader(`{ "email": "[email protected]", "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))}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" => "[email protected]", "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
{ "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 covers the payload, signatures and retries.
List subscriptions
/hooksThe subscriptions made with this key, newest first. A key only sees its own.
Example request
curl https://app.oneform.si/api/v1/hooks \ -H "Authorization: Bearer $ONEFORM_API_KEY"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();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$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);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))}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
[ { "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
/hooksSubscribes 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 -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"}'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();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$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);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))}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
{ "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
/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 -X DELETE https://app.oneform.si/api/v1/hooks/00000000-0000-4000-8000-000000000005 \ -H "Authorization: Bearer $ONEFORM_API_KEY"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();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$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);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))}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
{ "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. |