Start free
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

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

404 · application/json
{  "error": "No form with that id.",  "code": "form_not_found"}
StatusCodeWhen
400invalid_requestThe body isn't the JSON the endpoint expects.
400unknown_triggerPOST /hooks with a trigger that doesn't exist or isn't available.
400invalid_targettarget_url isn't a public https URL of at most 2,000 characters.
401unauthorizedNo key, a wrong key, or a revoked key (also when the person who made the key no longer manages the firm's integrations).
403firm_inactiveThe firm's account is suspended or closed.
404not_foundNo subscription with that id made with this key.
404form_not_foundNo form with that id in this firm (another firm's ids look the same as missing ones).
404unknown_triggerGET /triggers/{trigger}/sample for a trigger that doesn't exist or isn't available.
409too_many_hooksThe firm already has the most subscriptions it can have.
409form_not_publishedPOST /forms/{id}/send for a form that isn't published.
429rate_limitedOver the key's calls per minute, or too many calls with a wrong key from one address. See Retry-After.
429invite_limit_reachedOver the firm's form invites for the day. Retry-After says when it resets (midnight UTC).
502send_failedThe 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
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
curl https://app.oneform.si/api/v1/me \  -H "Authorization: Bearer $ONEFORM_API_KEY"
Node
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
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$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
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
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
{  "firm": {    "id": "00000000-0000-4000-8000-0000000000aa",    "name": "Lee & Partners CPA"  },  "key": {    "id": "00000000-0000-4000-8000-0000000000bb",    "name": "Zapier",    "prefix": "of_AbC123"  }}
Errors
401unauthorizedNo key, a wrong key, or a revoked key (also when the person who made the key no longer manages the firm's integrations).
403firm_inactiveThe firm's account is suspended or closed.
429rate_limitedOver 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

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
curl https://app.oneform.si/api/v1/triggers \  -H "Authorization: Bearer $ONEFORM_API_KEY"
Node
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
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$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
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
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
[  {    "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
401unauthorizedNo key, a wrong key, or a revoked key (also when the person who made the key no longer manages the firm's integrations).
403firm_inactiveThe firm's account is suspended or closed.
429rate_limitedOver 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

NameTypeDescription
triggerrequiredstringA trigger key from GET /triggers.
form_idoptionaluuidOnly this form's submissions.

Example request

curl
curl https://app.oneform.si/api/v1/triggers/form_submitted/sample \  -H "Authorization: Bearer $ONEFORM_API_KEY"
Node
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
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$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
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
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
[  {    "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
401unauthorizedNo key, a wrong key, or a revoked key (also when the person who made the key no longer manages the firm's integrations).
403firm_inactiveThe firm's account is suspended or closed.
404form_not_foundNo form with that id in this firm (another firm's ids look the same as missing ones).
404unknown_triggerGET /triggers/{trigger}/sample for a trigger that doesn't exist or isn't available.
429rate_limitedOver 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
curl https://app.oneform.si/api/v1/forms \  -H "Authorization: Bearer $ONEFORM_API_KEY"
Node
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
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$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
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
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
[  {    "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
401unauthorizedNo key, a wrong key, or a revoked key (also when the person who made the key no longer manages the firm's integrations).
403firm_inactiveThe firm's account is suspended or closed.
429rate_limitedOver 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

NameTypeDescription
idrequireduuidThe form's id.

Example request

curl
curl https://app.oneform.si/api/v1/forms/00000000-0000-4000-8000-000000000001 \  -H "Authorization: Bearer $ONEFORM_API_KEY"
Node
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
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$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
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
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
{  "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
401unauthorizedNo key, a wrong key, or a revoked key (also when the person who made the key no longer manages the firm's integrations).
403firm_inactiveThe firm's account is suspended or closed.
404form_not_foundNo form with that id in this firm (another firm's ids look the same as missing ones).
429rate_limitedOver the key's calls per minute, or too many calls with a wrong key from one address. See Retry-After.
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

NameTypeDescription
idrequireduuidThe form's id.

Body (JSON)

NameTypeDescription
hiddenoptionalobjectHidden-field values, as { key: value }. Numbers and booleans are sent as text.
langoptionalstringThe language the form opens in: en, fr, es.

Example request

curl
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
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
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$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
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
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
{  "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
400invalid_requestThe body isn't the JSON the endpoint expects.
401unauthorizedNo key, a wrong key, or a revoked key (also when the person who made the key no longer manages the firm's integrations).
403firm_inactiveThe firm's account is suspended or closed.
404form_not_foundNo form with that id in this firm (another firm's ids look the same as missing ones).
429rate_limitedOver 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

NameTypeDescription
idrequireduuidThe form's id. The form must be published.

Body (JSON)

NameTypeDescription
emailrequiredstringWho gets the form.
first_nameoptionalstringUsed in the greeting.
nameoptionalstringFull name; its first word greets them when first_name is missing.
messageoptionalstringA short personal note in the email, up to 1,000 characters.
hiddenoptionalobjectHidden-field values for their link, as for prefilled links.
langoptionalstringThe form's language: en, fr, es.

Example request

curl
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."}'
Node
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();
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": "[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
<?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);
Go
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))}
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" => "[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

202 · application/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
400invalid_requestThe body isn't the JSON the endpoint expects.
401unauthorizedNo key, a wrong key, or a revoked key (also when the person who made the key no longer manages the firm's integrations).
403firm_inactiveThe firm's account is suspended or closed.
404form_not_foundNo form with that id in this firm (another firm's ids look the same as missing ones).
409form_not_publishedPOST /forms/{id}/send for a form that isn't published.
429rate_limitedOver the key's calls per minute, or too many calls with a wrong key from one address. See Retry-After.
429invite_limit_reachedOver the firm's form invites for the day. Retry-After says when it resets (midnight UTC).
502send_failedThe 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

GET/hooks

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

Example request

curl
curl https://app.oneform.si/api/v1/hooks \  -H "Authorization: Bearer $ONEFORM_API_KEY"
Node
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
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$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
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
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
[  {    "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
401unauthorizedNo key, a wrong key, or a revoked key (also when the person who made the key no longer manages the firm's integrations).
403firm_inactiveThe firm's account is suspended or closed.
429rate_limitedOver 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)

NameTypeDescription
target_urlrequiredstringA public https URL, at most 2,000 characters, without a username or password. Its address is checked again at every delivery.
triggerrequiredstringA trigger key. event is accepted in its place.
form_idoptionaluuidOnly this form's events. Leave it out for every form.

Example request

curl
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
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
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$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
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
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
{  "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
400invalid_requestThe body isn't the JSON the endpoint expects.
400unknown_triggerPOST /hooks with a trigger that doesn't exist or isn't available.
400invalid_targettarget_url isn't a public https URL of at most 2,000 characters.
401unauthorizedNo key, a wrong key, or a revoked key (also when the person who made the key no longer manages the firm's integrations).
403firm_inactiveThe firm's account is suspended or closed.
404form_not_foundNo form with that id in this firm (another firm's ids look the same as missing ones).
409too_many_hooksThe firm already has the most subscriptions it can have.
429rate_limitedOver 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

NameTypeDescription
idrequireduuidThe subscription's id.

Example request

curl
curl -X DELETE https://app.oneform.si/api/v1/hooks/00000000-0000-4000-8000-000000000005 \  -H "Authorization: Bearer $ONEFORM_API_KEY"
Node
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
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$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
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
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
{  "ok": true}
Errors
401unauthorizedNo key, a wrong key, or a revoked key (also when the person who made the key no longer manages the firm's integrations).
403firm_inactiveThe firm's account is suspended or closed.
404not_foundNo subscription with that id made with this key.
429rate_limitedOver the key's calls per minute, or too many calls with a wrong key from one address. See Retry-After.

Questions about the API, webhooks or embeds? Ask a developer.