On this page
Developers
Ways to connect, from simplest to most control: one sentence to your AI agent, a form by hand, the setup API, or webhooks into your own systems. Machine-readable: the OpenAPI 3.1 document, generated from the server's validation rules, and the agent instructions.
Quick start
The fastest path, even for developers. Paste this into Claude Code, Cursor, Codex or any coding agent, then sign in when it shows a link:
The agent reads ellieforms.com/agent, connects your form in your framework's own style, builds the success and error states, and sends a test. Nothing is added to your environment variables: its token lives only in that session.
To send messages on to Zapier, Make, n8n or your own server, tell it: “Send my Ellieforms messages to https://…”. You click Allow in the email Ellieforms sends you.
Connect a form by hand
- Sign in at app.ellieforms.com, open Forms → Add website, then + Form → Code.
- Add
data-ellieforms,action,method="POST"and the hidden_fh_hpfield to your form, and the script tag to the page. - Show your own success and error messages from Ellieforms' events, or call
ellieforms.send(form)from your submit handler. - Publish, send one message, then press Go live in Forms.
<form data-ellieforms="frm_YOUR_FORM_ID" action="https://in.ellieforms.com/f/frm_YOUR_FORM_ID" method="POST" accept-charset="UTF-8">
<label>Name <input name="name" autocomplete="name" required></label>
<label>Email <input type="email" name="email" autocomplete="email" required></label>
<label>Message <textarea name="message" required></textarea></label>
<!-- Bot trap: keep it hidden and empty -->
<input type="text" name="_fh_hp" tabindex="-1" autocomplete="off" aria-hidden="true" style="position:absolute;left:-9999px">
<button type="submit">Send</button>
</form>
<p class="form-success" role="status" hidden>Thanks! We got your message.</p>
<p class="form-error" role="alert" hidden>Sorry, that didn't send. Please try again.</p>
<p class="form-badge"><a href="YOUR_BADGE_LINK">Sent with Ellieforms</a></p>
<script src="https://in.ellieforms.com/v1/f.js" defer></script>
<script>
// Ellieforms shows nothing to visitors. This code shows the result.
document.addEventListener("ellieforms:sent", (event) => {
event.target.hidden = true;
document.querySelector(".form-success").hidden = false;
});
document.addEventListener("ellieforms:error", () => {
document.querySelector(".form-error").hidden = false;
});
</script>Every guide has copyable code for its stack, plus the full script reference:
- React
- Next.js
- Vue
- Nuxt
- Svelte
- Astro
- Angular
- React Router
- Preact
- SolidJS
- Alpine.js
- htmx
- Eleventy
- Hugo
- Jekyll
- VitePress
- Plain HTML
- Website builders
Set up with the API
The same steps your agent takes, for scripts and CI. Sign-in uses a device flow: your script starts a session, the account owner opens one link and signs in, and your script gets a token. It lasts 2 hours and can only do setup work, and there are no long-lived API keys. Keep it in memory or your shell, never in the site's code or hosting settings.
# 1. Start a sign-in session (no token needed)
curl -s -X POST https://api-staging.ellieforms.com/v1/agent/sessions
# → { "session_id": "dev_…", "device_code": "…", "user_code": "BCDF-GHJK",
# "sign_in_url": "https://app.ellieforms.com/connect?code=BCDF-GHJK", "poll_url": "…", "interval": 5 }
# Open sign_in_url, sign in, and check the page shows the same user_code.
# 2. Poll every 5 seconds until you get a token (202 means keep waiting)
curl -s -X POST "$POLL_URL" -H "Content-Type: application/json" \
-d '{"device_code":"PASTE_DEVICE_CODE"}'
# → { "token": "fh_…", "expires_at": "…", "account": { "plan": "free", … } }
export ELLIEFORMS_TOKEN=fh_… # lasts 2 hours; keep it in your shell, never in code
# 3. Register the site (pass url so its messages are trusted straight away)
curl -s -X POST https://api-staging.ellieforms.com/v1/sites \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN" -H "Content-Type: application/json" \
-d '{"name":"Bakery website","url":"https://danasbakery.com"}'
# → { "site_id": "site_…", "website": "danasbakery.com" }
# 4. Create the form
curl -s -X POST https://api-staging.ellieforms.com/v1/forms \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN" -H "Content-Type: application/json" \
-d '{"site_id":"site_…","name":"Contact form"}'
# → { "form_id": "frm_…", "snippet": "<form data-ellieforms=…", "badge_url": "…", … }
# Add the snippet's attributes and script to your form (see the guides).
# 5. Send a setup test with the form's real field names
curl -s -X POST https://api-staging.ellieforms.com/v1/forms/frm_…/test \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN" -H "Content-Type: application/json" \
-d '{"fields":{"name":"Test","email":"test@example.com","message":"Hello"}}'
# → { "learned_fields": ["name","email","message"], "alerts": [{ "channel": "email", "status": "sent" }] }
# 6. Go live
curl -s -X POST https://api-staging.ellieforms.com/v1/forms/frm_…/live -H "Authorization: Bearer $ELLIEFORMS_TOKEN"The same flow as a Node.js script
// setup-ellieforms.mjs: node setup-ellieforms.mjs (Node 18 or newer)
const BASE = "https://api-staging.ellieforms.com";
async function api(path, { token, body, method = body ? "POST" : "GET" } = {}) {
const res = await fetch(BASE + path, {
method,
headers: { "content-type": "application/json", ...(token && { authorization: `Bearer ${token}` }) },
body: body && JSON.stringify(body),
});
const data = await res.json();
if (res.status >= 400) throw new Error(`${data.error.code}: ${data.error.message} ${data.error.fix ?? ""}`);
return { status: res.status, data };
}
// Sign in: the owner opens the link; the token stays in this process.
const { data: session } = await api("/v1/agent/sessions", { method: "POST" });
console.log(`Open ${session.sign_in_url} and check the code is ${session.user_code}`);
let token;
let wait = session.interval;
while (!token) {
await new Promise((r) => setTimeout(r, wait * 1000));
const { status, data } = await api(new URL(session.poll_url).pathname, { body: { device_code: session.device_code } });
if (status === 200) token = data.token;
else if (data.error.code === "slow_down") wait += 5;
}
const { data: site } = await api("/v1/sites", { token, body: { name: "Bakery website", url: "https://danasbakery.com" } });
const { data: form } = await api("/v1/forms", { token, body: { site_id: site.site_id, name: "Contact form" } });
console.log("Add this to your form:\n" + form.snippet);
console.log("Badge link:", form.badge_url);
// After the snippet is on your site:
const { data: test } = await api(`/v1/forms/${form.form_id}/test`, { token, body: { fields: { name: "Test", email: "test@example.com", message: "Hello" } } });
console.log("Learned fields:", test.learned_fields, test.alerts);
await api(`/v1/forms/${form.form_id}/live`, { token, method: "POST" });
console.log("Live.");A form starts in learning: the setup test teaches Ellieforms its field names. Going live locks them in. If the site's form changes later, call POST /v1/forms/{id}/learning, update the site, test again and go live.
Webhooks
A webhook sends each new message to an address you choose as a POST with a JSON body. Pro and Team send each message the moment it arrives; Free batches them into one send a day. Setup tests are instant on every plan and carry "test": true. The body is always a list of messages, so nothing breaks when a plan changes.
Add one
- Simplest: ask your agent, as above.
- By API:
POST /v1/webhookswith{ "url": "https://…" }(addform_idfor one form only). It answers403 needs_owner_allow; the owner clicks Allow in the email, then you repeat the same call withgrant_id. - No-code tools: Zapier's “Catch Hook” (paid Zapier plan), Make's “Custom webhook” and n8n's “Webhook” trigger all give you an address. In Make or n8n, add an iterator or “split out” step on
messages.
Example delivery (JSON)
{
"event": "messages.created",
"delivery_id": "alr_8hW2…",
"timing": "instant",
"count": 1,
"messages": [
{
"id": "msg_Q4m9…",
"test": false,
"created_at": "2026-10-20T09:15:00.000Z",
"website": "shop.example.com",
"form_id": "frm_7Tx1…",
"form_name": "Contact",
"fields": {
"name": "Ana",
"email": "ana@example.com",
"message": "Do you ship to Lisbon?"
},
"reply_to": "ana@example.com",
"inbox_url": "https://app.ellieforms.com/app/messages/msg_Q4m9…"
}
]
}The delivery
| Field | Type | Meaning |
|---|---|---|
event | string | |
delivery_id | string | Same on every retry of this delivery. |
timing | string | instant: one message as it arrives (Pro, Team, and setup tests). daily: the day's messages at once (Free). |
count | integer | |
messages | object[] | Always a list, oldest first, so the shape never changes between plans. |
Each message
| Field | Type | Meaning |
|---|---|---|
id | string | Message ID. The same message always has the same ID; use it to skip duplicates. |
test | boolean | True for setup tests sent by an AI agent. |
created_at | string | When Ellieforms received it (UTC). |
website | string | null | Website the form is on, e.g. shop.example.com. |
form_id | string | |
form_name | string | null | |
fields | object | The form's fields, by the names used on the site. |
reply_to | string | null | The visitor's email address, when the form has a valid one. |
inbox_url | string | Opens this message in the Ellieforms inbox (sign-in required). |
Answering and retries
- Answer with any
2xxwithin 10 seconds; do slow work after answering. Anything else is retried with growing gaps for 24 hours.delivery_idand each messageidstay the same on retries, so you can skip duplicates. - Answer
410to remove the webhook. - After 24 hours of failures the webhook is paused and the owner gets one email. Resume with
POST /v1/webhooks/{id}/resume. Every message is always kept in the Ellieforms inbox. - Up to 10 webhooks per account. Addresses must be public https; redirects are not followed.
Checking the signature
Needed only when your own code receives the webhook; Zapier, Make and n8n don't need it. Each request carries Ellieforms-Signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of t, a dot, and the raw request body, keyed with the webhook's signing secret. Reject a t older than 5 minutes. The owner copies the secret from Account → Webhooks into your server's secret settings (here ELLIEFORMS_WEBHOOK_SECRET); never commit it.
Always verify the raw body exactly as received. Parsing the JSON and serializing it again changes the bytes and breaks the check.
// Express. The signature covers the raw body, so read it before any JSON parser.
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
const app = express();
const seen = new Set(); // use your database in production
function fromEllieforms(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries((header ?? "").split(",").map((p) => p.split("=")));
if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // older than 5 minutes
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}
app.post("/ellieforms", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
if (!fromEllieforms(raw, req.get("ellieforms-signature"), process.env.ELLIEFORMS_WEBHOOK_SECRET)) return res.sendStatus(401);
const delivery = JSON.parse(raw);
for (const message of delivery.messages) {
if (seen.has(message.id)) continue; // retries reuse the same id
seen.add(message.id);
console.log(message.form_name, message.fields);
}
res.sendStatus(200); // answer within 10 seconds; do slow work afterwards
});
app.listen(3000);Cloudflare Workers, Next.js, Deno or Bun (Web Crypto)
// Web standard Request/Response: Cloudflare Workers, Next.js route handlers
// (app/api/ellieforms/route.ts), Deno, Bun, Hono.
async function fromEllieforms(rawBody: string, header: string | null, secret: string) {
const { t, v1 } = Object.fromEntries((header ?? "").split(",").map((p) => p.split("=") as [string, string]));
if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
const sig = Uint8Array.from(v1.match(/../g) ?? [], (h) => parseInt(h, 16));
return crypto.subtle.verify("HMAC", key, sig, new TextEncoder().encode(`${t}.${rawBody}`)); // constant-time
}
export async function POST(request: Request) {
const raw = await request.text();
if (!(await fromEllieforms(raw, request.headers.get("ellieforms-signature"), process.env.ELLIEFORMS_WEBHOOK_SECRET!))) {
return new Response("bad signature", { status: 401 });
}
const delivery = JSON.parse(raw) as { delivery_id: string; messages: { id: string; fields: Record<string, string> }[] };
for (const message of delivery.messages) {
// skip message.id values you have already stored, then save or forward the rest
}
return new Response("ok");
}
// Cloudflare Workers: read the secret from env.ELLIEFORMS_WEBHOOK_SECRET instead of process.env.Python (Flask)
# Flask
import hashlib, hmac, json, os, time
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["ELLIEFORMS_WEBHOOK_SECRET"].encode()
def from_ellieforms(raw: bytes, header: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
t, v1 = parts.get("t", ""), parts.get("v1", "")
if not t.isdigit() or abs(time.time() - int(t)) > 300:
return False
expected = hmac.new(SECRET, f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
@app.post("/ellieforms")
def ellieforms():
raw = request.get_data() # raw bytes, before any parsing
if not from_ellieforms(raw, request.headers.get("Ellieforms-Signature", "")):
abort(401)
for message in json.loads(raw)["messages"]:
print(message["id"], message["fields"]) # skip ids you have already stored
return "ok"PHP
<?php
// ellieforms.php
$raw = file_get_contents('php://input');
$secret = getenv('ELLIEFORMS_WEBHOOK_SECRET');
parse_str(str_replace(',', '&', $_SERVER['HTTP_ELLIEFORMS_SIGNATURE'] ?? ''), $sig);
$fresh = isset($sig['t']) && abs(time() - (int) $sig['t']) <= 300;
$expected = hash_hmac('sha256', ($sig['t'] ?? '') . '.' . $raw, $secret);
if (!$fresh || !hash_equals($expected, $sig['v1'] ?? '')) {
http_response_code(401);
exit;
}
foreach (json_decode($raw, true)['messages'] as $message) {
error_log($message['id'] . ' ' . json_encode($message['fields'])); // skip ids you have already stored
}
http_response_code(200);API reference
Base address https://api-staging.ellieforms.com. JSON in, JSON out. Except where marked, send Authorization: Bearer $ELLIEFORMS_TOKEN (from the sign-in flow). Reading or exporting messages, inviting team members and adding webhooks also need the owner's Allow.
GET /v1/openapi.json
This API description (OpenAPI 3.1). No token needed.
POST /v1/agent/sessions
Start a sign-in session; show the user sign_in_url. No token needed.
Example
curl -s -X POST "https://api-staging.ellieforms.com/v1/agent/sessions"Response
| Field | Type | Meaning |
|---|---|---|
session_id | string | |
device_code | string | Secret: send it only to the poll URL. |
user_code | string | Shown on the sign-in page; tell the user to check it matches. |
sign_in_url | string | The one link the user opens. |
poll_url | string | |
interval | integer | Seconds between polls. |
expires_in | integer | Seconds until the session expires. |
POST /v1/agent/sessions/{id}/poll
Poll until the user approves; returns the agent token. No token needed.
Example
curl -s -X POST "https://api-staging.ellieforms.com/v1/agent/sessions/dev_…/poll" \
-H "Content-Type: application/json" \
-d '{"device_code":"…"}'Request body
| Field | Type | Meaning |
|---|---|---|
device_code | string |
Response
| Field | Type | Meaning |
|---|---|---|
token | string | Agent token: send as `Authorization: Bearer <token>`. Lasts 2 hours; never write it into files. |
expires_at | string | |
account | object |
GET /v1/account
Plan, usage, sites and badge link.
Example
curl -s -X GET "https://api-staging.ellieforms.com/v1/account" \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN"Response
| Field | Type | Meaning |
|---|---|---|
email_masked | string | |
plan | string | |
billing_status | string | |
alerts | string | |
usage_this_month | object | |
sites | object[] | |
badge_url | string | |
badge_required | boolean |
GET /v1/sites
List sites.
Example
curl -s -X GET "https://api-staging.ellieforms.com/v1/sites" \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN"Response
| Field | Type | Meaning |
|---|---|---|
sites | object[] |
POST /v1/sites
Register a site.
Example
curl -s -X POST "https://api-staging.ellieforms.com/v1/sites" \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Bakery website","url":"https://danasbakery.com"}'Request body
| Field | Type | Meaning |
|---|---|---|
name | string | |
url (optional) | string | null | The website's address, if known. |
Response
| Field | Type | Meaning |
|---|---|---|
site_id | string | |
name | string | |
website | string | null |
GET /v1/forms
List forms.
Example
curl -s -X GET "https://api-staging.ellieforms.com/v1/forms" \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN"Response
| Field | Type | Meaning |
|---|---|---|
forms | object[] |
POST /v1/forms
Create a form (starts in learning).
Example
curl -s -X POST "https://api-staging.ellieforms.com/v1/forms" \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"site_id":"site_…","name":"Contact form"}'Request body
| Field | Type | Meaning |
|---|---|---|
site_id | string | |
name | string | |
thank_you_url (optional) | string | null | Only on the site's own domain. |
Response
| Field | Type | Meaning |
|---|---|---|
form_id | string | |
state | string | |
submit_url | string | |
snippet | string | The form attributes and the one script tag to add to the site. |
recipe_url | string | |
badge_url | string | |
badge_required | boolean | |
next_steps | string[] |
GET /v1/forms/{id}/status
Form health.
Example
curl -s -X GET "https://api-staging.ellieforms.com/v1/forms/frm_…/status" \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN"Response
| Field | Type | Meaning |
|---|---|---|
form_id | string | |
name | string | |
state | string | |
fields | string[] | |
last_message_at | string | null | |
last_24h | object | Message counts by status: new, handled, spam, held. |
pending_websites | string[] | |
in_burst | boolean |
PATCH /v1/forms/{id}
Rename, thank-you page, Team recipients.
Example
curl -s -X PATCH "https://api-staging.ellieforms.com/v1/forms/frm_…" \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Contact form","thank_you_url":"https://danasbakery.com/thanks"}'Request body
| Field | Type | Meaning |
|---|---|---|
name (optional) | string | |
thank_you_url (optional) | string | null | |
recipients (optional) | string[] | Team plan: up to 3 existing team members' emails. |
Response
| Field | Type | Meaning |
|---|---|---|
ok | boolean | |
form_id | string | |
name | string | |
state | string | |
fields | string[] | |
last_message_at | string | null | |
last_24h | object | Message counts by status: new, handled, spam, held. |
pending_websites | string[] | |
in_burst | boolean |
DELETE /v1/forms/{id}
Two-step delete (add ?confirm=true); restorable for 30 days.
Example
curl -s -X DELETE "https://api-staging.ellieforms.com/v1/forms/frm_…?confirm=…" \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN"POST /v1/forms/{id}/live
Lock the learned fields; the form goes live.
Example
curl -s -X POST "https://api-staging.ellieforms.com/v1/forms/frm_…/live" \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN"Response
| Field | Type | Meaning |
|---|---|---|
ok | boolean | |
state | string | |
fields | string[] | |
tell_the_user | string |
POST /v1/forms/{id}/learning
Back to learning after the site's form changed.
Example
curl -s -X POST "https://api-staging.ellieforms.com/v1/forms/frm_…/learning" \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN"Response
| Field | Type | Meaning |
|---|---|---|
ok | boolean | |
state | string | |
next_step | string |
POST /v1/forms/{id}/test
Send a setup test through the real pipeline.
Example
curl -s -X POST "https://api-staging.ellieforms.com/v1/forms/frm_…/test" \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"fields":{"name":"Test","email":"test@example.com","message":"Hello"}}'Request body
| Field | Type | Meaning |
|---|---|---|
fields (optional) | object | The form's real field names with sample values. |
Response
| Field | Type | Meaning |
|---|---|---|
submission_id | string | |
learned_fields | string[] | |
alerts | object[] | |
next_step | string |
GET /v1/messages
Latest 50 messages (2-hour Allow). Needs the owner's Allow.
Example
curl -s -X GET "https://api-staging.ellieforms.com/v1/messages?form_id=…" \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN"Response
| Field | Type | Meaning |
|---|---|---|
messages | object[] |
GET /v1/messages/export
All messages as CSV (single-use Allow). Needs the owner's Allow.
Example
curl -s -X GET "https://api-staging.ellieforms.com/v1/messages/export?grant_id=…" \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN"POST /v1/members/invite
Team plan: invite link for a new member (single-use Allow). Needs the owner's Allow.
Example
curl -s -X POST "https://api-staging.ellieforms.com/v1/members/invite" \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"email":"sam@example.com"}'Request body
| Field | Type | Meaning |
|---|---|---|
email | string | |
grant_id (optional) | string |
Response
| Field | Type | Meaning |
|---|---|---|
invite_url | string | |
tell_the_user | string |
GET /v1/grants/{id}
Status of an Allow request.
Example
curl -s -X GET "https://api-staging.ellieforms.com/v1/grants/grt_…" \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN"Response
| Field | Type | Meaning |
|---|---|---|
grant_id | string | |
kind | string | |
status | string | |
next_step (optional) | string |
GET /v1/webhooks
List webhooks.
Example
curl -s -X GET "https://api-staging.ellieforms.com/v1/webhooks" \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN"Response
| Field | Type | Meaning |
|---|---|---|
webhooks | object[] |
POST /v1/webhooks
Add a webhook (single-use Allow for this url). Needs the owner's Allow.
Example
curl -s -X POST "https://api-staging.ellieforms.com/v1/webhooks" \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url":"https://hooks.zapier.com/hooks/catch/…"}'Request body
| Field | Type | Meaning |
|---|---|---|
url | string | Public https address from the user's tool. |
form_id (optional) | string | null | Only this form; omit for every form. |
grant_id (optional) | string | After the owner's Allow, for this same url. |
Response
| Field | Type | Meaning |
|---|---|---|
webhook_id | string | |
url | string | |
form_id | string | null | null: every form. |
state | string | |
last_result | string | null | ok, timeout, network or http_<status> of the last attempt. |
last_attempt_at | string | null | |
timing | string | |
tell_the_user | string |
DELETE /v1/webhooks/{id}
Remove a webhook.
Example
curl -s -X DELETE "https://api-staging.ellieforms.com/v1/webhooks/whk_…" \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN"Response
| Field | Type | Meaning |
|---|---|---|
ok | boolean |
POST /v1/webhooks/{id}/resume
Turn a paused webhook back on.
Example
curl -s -X POST "https://api-staging.ellieforms.com/v1/webhooks/whk_…/resume" \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN"Response
| Field | Type | Meaning |
|---|---|---|
ok | boolean | |
state | string |
POST /v1/channels
Phone alerts: returns the install link.
Example
curl -s -X POST "https://api-staging.ellieforms.com/v1/channels" \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"type":"push"}'Request body
| Field | Type | Meaning |
|---|---|---|
type | string |
Response
| Field | Type | Meaning |
|---|---|---|
install_url | string | |
devices | integer | |
tell_the_user | string |
POST /v1/billing/checkout
Upgrade link (or the plan-management link when already paying).
Example
curl -s -X POST "https://api-staging.ellieforms.com/v1/billing/checkout" \
-H "Authorization: Bearer $ELLIEFORMS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"plan":"pro","interval":"yearly"}'Request body
| Field | Type | Meaning |
|---|---|---|
plan | string | |
interval (optional) | string |
Errors
Every error is { "error": { "code", "message", "fix", "docs" } }. fix says what to do next.
| Code | Meaning and fix |
|---|---|
unauthorized | Token missing, wrong or older than 2 hours. Start a new sign-in session. |
invalid_request | The body or query didn't validate. message says which field. |
not_found | Wrong ID, or it belongs to another account. |
plan_limit | The plan doesn't include this (for example a fourth website on Pro). The owner can upgrade with POST /v1/billing/checkout. |
needs_owner_allow | 403 with a grant_id. The owner gets an email; poll GET /v1/grants/{id} until status is allowed, then retry with grant_id. |
rate_limited | 429. Wait 60 seconds. |
While signing in, polling also answers authorization_pending and slow_down (202, keep polling), or expired_token and access_denied (400, start again).