Documentation

The web runtime for AI agents. Three primitives. One API.

You send JSON, you deploy pages, collect data, and visualize results. Pages, forms, dashboards, webhooks, embeds -- everything is an API call. Built for developers and AI agents.

Quick Start

Free API key, first form, under a minute. No signup.

1. Get a free API key

curl -X POST https://sutrena.com/api/trial

Returns an st_trial_ key with a 24-hour claim window. The response includes a claimUrl -- visit it in a browser to sign in and keep your data permanently.

2. Create a form from a template

curl -X POST https://sutrena.com/api/forms \
  -H "Authorization: Bearer st_trial_xxx" \
  -H "Content-Type: application/json" \
  -d '{"templateId":"waitlist","createDashboard":true}'

One call. You get back a form and a dashboard. The response includes submitUrl, hostedFormUrl, and dashboardUrl.

3. Or create a custom form with fields

curl -X POST https://sutrena.com/api/forms \
  -H "Authorization: Bearer st_trial_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Beta Signup",
    "fields": [
      {"name": "email", "label": "Email", "type": "email", "required": true},
      {"name": "company", "label": "Company", "type": "text", "required": false},
      {"name": "role", "label": "Role", "type": "select", "required": true, "options": ["Developer", "Designer", "PM", "Other"]}
    ]
  }'

4. Collect a submission

curl -X POST https://sutrena.com/api/forms/FORM_ID/submit \
  -H "Content-Type: application/json" \
  -d '{"email": "[email protected]", "company": "Acme", "role": "Developer"}'

Submit is public -- no API key needed. CORS is on, so browsers can POST directly.

Authentication

Three ways to authenticate. Pick one.

Authorization: Bearer YOUR_KEY

Free key (instant, no signup)

POST /api/trial to get an st_trial_ key. No expiry. 5 per 24 hours per IP.

API key (permanent)

Sign in with GitHub or Google, then create an st_live_ key via POST /api/keys (needs a session cookie) or from the dashboard settings. Never expires unless you revoke it.

Session cookie (browser)

OAuth sign-in sets a sutrena_session cookie. The dashboard and key management endpoints use this automatically.

Forms

CRUD for forms. Each one gets fields, a public submit URL, and a hosted page.

MethodEndpointDescription
POST/api/formsCreate a new form
GET/api/formsList all your forms
GET/api/forms/:idGet form details
PUT/api/forms/:idUpdate a form
DELETE/api/forms/:idDelete a form and its dashboards

Create a form

Send a templateId, or send name + fields. One or the other.

POST /api/forms
{
  "name": "Contact Form",
  "fields": [
    {"name": "name", "label": "Name", "type": "text", "required": true},
    {"name": "email", "label": "Email", "type": "email", "required": true},
    {"name": "message", "label": "Message", "type": "textarea", "required": true}
  ],
  "successMessage": "Thanks! We'll be in touch.",
  "submitLabel": "Send",
  "createDashboard": true,
  "closesAt": "2026-12-31T23:59:59Z",
  "uniqueBy": ["email"],
  "maxSubmissions": 1000,
  "publicResults": false,
  "redirectUrl": "https://example.com/thanks",
  "webhookIds": ["uuid-of-webhook"]
}

Field types

TypeValidation optionsNotes
textminLength, maxLength, patternGeneral text input
emailminLength, maxLengthValidates email format
textareaminLength, maxLengthMulti-line text
numbermin, maxNumeric value
selectoptions (required)Dropdown, single choice
multiselectoptions (required)Multiple choice (value is string[])
checkbox--Boolean toggle
urlminLength, maxLength, patternValidates URL format
telminLength, maxLength, patternPhone number
date--Date picker
hidden--Not shown to user, sent with submission
fileaccept, maxFileSizeFile upload (max 50 MB per file)

Update a form

PUT /api/forms/:id
{
  "fields": [...],
  "successMessage": "Updated message",
  "submitLabel": "Submit",
  "closesAt": null,
  "uniqueBy": null,
  "maxSubmissions": null,
  "publicResults": true,
  "redirectUrl": null,
  "webhookIds": ["uuid"]
}

Everything is optional. Set a value to null to clear it.

Submissions

Submit from browsers, mobile apps, or server-side code. The submit endpoint is public with CORS on.

MethodEndpointDescription
POST/api/forms/:id/submitSubmit data (public, no auth, CORS enabled)
GET/api/forms/:id/submissionsSearch and list submissions (auth required)
DELETE/api/forms/:id/submissions?email=XGDPR deletion by email (Builder+)
GET/api/forms/:id/exportCSV export (Builder+)
GET/api/forms/:id/resultsPublic aggregated results (if enabled)

Submit data

POST JSON with field values as keys. No auth. Validated against the form's field definitions.

curl -X POST https://sutrena.com/api/forms/FORM_ID/submit \
  -H "Content-Type: application/json" \
  -d '{"email": "[email protected]", "name": "Alice"}'

Search submissions

Filter with query params. Cursor-based pagination.

ParameterTypeDescription
searchstringFull-text search across all fields
fieldstringFilter by a specific field name
valuestringFilter by field value (use with field)
fromISO 8601Start date filter
toISO 8601End date filter
statusstring"clean" or "spam"
limitnumberResults per page (default varies)
cursorstringPagination cursor from previous response
curl "https://sutrena.com/api/forms/FORM_ID/submissions?field=email&[email protected]&limit=10" \
  -H "Authorization: Bearer YOUR_KEY"

GDPR deletion

Deletes all submissions matching an email. Builder plan and above.

curl -X DELETE "https://sutrena.com/api/forms/FORM_ID/[email protected]" \
  -H "Authorization: Bearer YOUR_KEY"

CSV export

All submissions as CSV. Builder plan and above.

curl "https://sutrena.com/api/forms/FORM_ID/export" \
  -H "Authorization: Bearer YOUR_KEY" \
  -o submissions.csv

Public results

When publicResults: true, anyone can see aggregated counts for select, checkbox, and multiselect fields. No auth needed. Good for polls.

curl https://sutrena.com/api/forms/FORM_ID/results

Returns total count and per-field breakdowns:

{
  "total": 142,
  "fields": {
    "rating": [
      {"value": "5", "count": 58},
      {"value": "4", "count": 41},
      {"value": "3", "count": 28},
      {"value": "2", "count": 10},
      {"value": "1", "count": 5}
    ]
  }
}

File Uploads

Two-step presigned URL flow. Get a URL, upload to it, include the object ID in your submission. 50 MB max per file.

MethodEndpointDescription
POST/api/forms/:id/uploadGet a presigned upload URL (public, rate-limited)
GET/api/forms/:id/files/:objectIdDownload a file (302 redirect)

Step 1: Get a presigned URL

curl -X POST https://sutrena.com/api/forms/FORM_ID/upload \
  -H "Content-Type: application/json" \
  -d '{
    "filename": "resume.pdf",
    "contentType": "application/pdf",
    "fieldName": "resume"
  }'

Returns uploadUrl and objectId. The URL expires quickly, so upload right away.

Step 2: Upload the file

curl -X PUT "UPLOAD_URL" \
  -H "Content-Type: application/pdf" \
  --data-binary @resume.pdf

Step 3: Submit with the object ID

Use the objectId as the value for the file field.

curl -X POST https://sutrena.com/api/forms/FORM_ID/submit \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "[email protected]", "resume": "OBJECT_ID"}'

Pages

Deploy static HTML pages via API. Served at /p/:slug with no auth needed to view. Pages deploy to a subdomain. You control which subdomain by passing subdomainId (optional).

Create a page

curl -X POST https://sutrena.com/api/pages \
  -H "Authorization: Bearer st_trial_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "my-landing",
    "title": "My Landing Page",
    "html": "<h1>Welcome</h1><p>Sign up below.</p>",
    "css": "body { font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 2rem; }"
  }'

Returns a pageUrl at /p/my-landing. No deploy step. If subdomainId is omitted, deploys to your first/default subdomain.

Deploy to a specific subdomain

# Step 1: List all your subdomains
curl https://sutrena.com/api/account/subdomains \
  -H "Authorization: Bearer st_trial_xxx"

# Step 2: Create page with subdomainId
curl -X POST https://sutrena.com/api/pages \
  -H "Authorization: Bearer st_trial_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "my-landing",
    "title": "My Landing Page",
    "html": "<h1>Welcome</h1>",
    "subdomainId": "sub_xyz"
  }'

Workflow: List subdomains → pick one → create page with that subdomainId. Use this to deploy pages to different subdomains (e.g. alice.sutrena.com vs blog.sutrena.com).

Limits

Slug: lowercase alphanumeric + hyphens, 3-100 chars. HTML max: 512KB. CSS max: 128KB. Pages count toward the project pool. Free: 10 projects. Builder: 50. Pro: 200. Scale: unlimited.

Endpoints

MethodPathNotes
POST/api/pagesCreate page
GET/api/pagesList your pages
GET/api/pages/:idPage details with full HTML/CSS
PUT/api/pages/:idUpdate HTML/CSS/title (slug immutable)
DELETE/api/pages/:idDelete page

Subdomains

Serve pages on your own subdomain. Available on all plans. Each subdomain is a separate site. You have full control over which subdomain each page deploys to.

MethodEndpointDescription
GET/api/account/subdomainsList all subdomains with page counts
POST/api/account/subdomainsCreate a new subdomain
GET/api/account/subdomainGet default subdomain (deprecated)
PUT/api/account/subdomainChange default subdomain (deprecated)

List all subdomains

curl https://sutrena.com/api/account/subdomains \
  -H "Authorization: Bearer YOUR_KEY"

Returns all your subdomains with page counts. Use the id field when creating pages to specify which subdomain to deploy to.

Create a new subdomain

curl -X POST https://sutrena.com/api/account/subdomains \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "blog"}'

Creates blog.sutrena.com. Then use the returned id when creating pages to deploy them to this subdomain.

Workflow for multi-subdomain sites

1. GET /api/account/subdomains — see all your subdomains
2. POST /api/account/subdomains — create new ones if needed
3. POST /api/pages with subdomainId — deploy to specific subdomain
4. If subdomainId is omitted, page deploys to your first/default subdomain

Example: alice.sutrena.com (personal site), blog.sutrena.com (blog), docs.sutrena.com (documentation) — all from one API key.

Change default subdomain (deprecated)

curl -X PUT https://sutrena.com/api/account/subdomain \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"subdomain": "alice"}'

Pages are then available at alice.sutrena.com/slug. The root path (alice.sutrena.com/) serves the page with slug index. Note: This changes your default subdomain. Prefer using POST /api/account/subdomains + specifying subdomainId on pages for better control.

Rules

3-30 characters. Lowercase alphanumeric and hyphens only. Cannot start or end with a hyphen.

Custom Domains

Point your own domain at Sutrena pages. Builder plan and above.

MethodEndpointDescription
POST/api/account/domainsAdd a domain (returns CNAME instructions)
GET/api/account/domainsList domains with DNS/SSL status
GET/api/account/domains/:idCheck domain status
DELETE/api/account/domains/:idRemove a domain

Add a domain

curl -X POST https://sutrena.com/api/account/domains \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain": "pages.example.com"}'

Returns CNAME instructions. Point your domain to cname.sutrena.com. SSL is provisioned automatically once DNS propagates.

Plan limits

Builder: 1 custom domain. Pro: 5 custom domains. Scale: unlimited.

Page Assets

Upload images, CSS, and other static files for your pages. Served from a CDN.

MethodEndpointDescription
POST/api/pages/assetsPresign an upload (returns uploadUrl + publicUrl)
GET/api/pages/assetsList assets (optional ?pageId= filter)
DELETE/api/pages/assets/:idDelete an asset

Presign and upload

Step 1: Get a presigned upload URL and a public CDN URL.

curl -X POST https://sutrena.com/api/pages/assets \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filename": "hero.png", "contentType": "image/png"}'

Returns uploadUrl and publicUrl. Upload the file to the presigned URL, then reference the public URL in your page HTML.

Step 2: Upload the file to the presigned URL.

curl -X PUT "UPLOAD_URL" \
  -H "Content-Type: image/png" \
  --data-binary @hero.png

Size limits

PlanMax file sizeTotal storage
Free2 MB50 MB
Builder5 MB500 MB
Pro20 MB5 GB
Scale50 MBUnlimited

Upsert API

Insert or update a submission by external ID. Builder plan and above.

curl -X PUT https://sutrena.com/api/forms/FORM_ID/submissions/upsert \
  -H "Authorization: Bearer st_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{"externalId": "customer-123", "payload": {"name": "Jane", "score": 95}}'

# First call: { "data": submission, "created": true }
# Second call (same externalId): { "data": submission, "created": false }

externalId is unique per form. Use for CRM sync, inventory tracking, leaderboards, or any data that changes over time. Free plan returns 403 with upgrade hint.

Dashboards

Define dashboards in JSON. Three data sources: form submissions (live, auto-refresh), inline JSON (static, up to 1000 rows), or uploaded CSV (static, up to 100K rows). Charts, tables, and metrics rendered with recharts. Public dashboards get a shareable URL.

MethodEndpointDescription
POST/api/dashboardsCreate a dashboard (formId, data array, or csvObjectId)
POST/api/dashboards/uploadPresign CSV upload for data dashboard
GET/api/dashboardsList all your dashboards
GET/api/dashboards/:idGet dashboard details
PUT/api/dashboards/:idUpdate title, DSL, or visibility
DELETE/api/dashboards/:idDelete a dashboard
GET/api/dashboards/:id/downloadDownload original CSV file

Create a dashboard from form data

POST /api/dashboards
{
  "title": "Waitlist Dashboard",
  "formId": "FORM_ID",
  "isPublic": true,
  "dsl": {
    "version": 1,
    "widgets": [
      {"type": "metric_card", "title": "Total Signups", "value": "count(*)"},
      {"type": "line_chart", "title": "Signups Over Time", "groupBy": "$submitted_at:day"},
      {"type": "pie_chart", "title": "By Role", "groupBy": "role"},
      {"type": "bar_chart", "title": "Weekly Trend", "groupBy": "$submitted_at:week"},
      {"type": "data_table", "title": "Recent Signups", "columns": ["email", "company", "role"], "limit": 20, "sort": "newest"}
    ]
  }
}

Create a dashboard from inline data

Pass a data array instead of formId. Up to 1,000 rows. Static (no auto-refresh).

POST /api/dashboards
{
  "title": "Q1 Sales",
  "isPublic": true,
  "data": [
    {"region": "US", "revenue": 120000, "deals": 34},
    {"region": "EU", "revenue": 87000, "deals": 22},
    {"region": "APAC", "revenue": 45000, "deals": 11}
  ],
  "dsl": {
    "version": 1,
    "widgets": [
      {"type": "bar_chart", "title": "By Region", "groupBy": "region"},
      {"type": "data_table", "title": "Breakdown", "columns": ["region", "revenue", "deals"]}
    ]
  }
}

Create a dashboard from CSV

Upload a CSV (max 10MB, 100K rows), then reference it. Data is pre-aggregated on upload -- dashboard renders instantly.

# Step 1: Presign upload
POST /api/dashboards/upload
{"filename": "metrics.csv", "sizeBytes": 524288}
# → {"csvObjectId": "obj_xyz", "uploadUrl": "https://..."}

# Step 2: PUT the file to uploadUrl

# Step 3: Create dashboard
POST /api/dashboards
{
  "title": "Monthly Metrics",
  "csvObjectId": "obj_xyz",
  "isPublic": true,
  "dsl": {
    "version": 1,
    "widgets": [
      {"type": "metric_card", "title": "Total Rows", "value": "count(*)"},
      {"type": "metric_card", "title": "Total Revenue", "value": "sum(amount)"},
      {"type": "line_chart", "title": "Trend", "groupBy": "$date:month"},
      {"type": "pie_chart", "title": "Revenue by Category", "groupBy": "category", "aggregate": "sum(amount)"}
    ]
  }
}

Dashboard DSL reference

Version must be 1. Max 20 widgets. Private by default -- set isPublic: true to share.

Widget typeRequired fieldsOptional fieldsDescription
metric_cardtitle, valuestatusSingle number. value is count(*), count(field), sum(field), avg(field), min(field), or max(field). status filters by "clean" or "spam".
data_tabletitle, columnslimit, sort, sortBy, sortOrderTable of submissions. columns is an array of field names (max 20). sort: "newest" or "oldest". limit: 1-100.
text_blocktitle, content--Static text content (max 5000 chars)
pie_charttitle, groupByaggregatePie chart grouped by a field name (e.g. industry). Optional aggregate: sum(field), avg(field), min(field), max(field). Defaults to counting rows.
bar_charttitle, groupByaggregateBar chart. groupBy can be a field name or a time bucket like $submitted_at:day or $date:week. Optional aggregate: sum(field), avg(field), min(field), max(field). Defaults to counting rows.
line_charttitle, groupByaggregateLine chart. groupBy must be a time bucket: $submitted_at:day, $submitted_at:week, $submitted_at:month, or any date column like $date:day. Optional aggregate: sum(field), avg(field), min(field), max(field). Defaults to counting rows.

Templates

14 built-in templates. Pre-configured fields and a matching dashboard DSL. One call creates both.

MethodEndpointDescription
GET/api/templatesList all 14 templates (public, no auth)
GET/api/templates/:idGet template details including fields and dashboard DSL (public)
POST/api/templates/:idCreate a form and dashboard from a template (auth required)

Available templates

Contact Formcontact
Feedback Formfeedback
Bug Reportbug-report
Waitlistwaitlist
Surveysurvey
Pollpoll
RSVPrsvp
NPSnps
Quizquiz
Newsletternewsletter
Bookingbooking
Client Intakeclient-intake
Orderorder
Preorderpreorder

Create from template

curl -X POST https://sutrena.com/api/templates/waitlist \
  -H "Authorization: Bearer YOUR_KEY"

Returns formId, hostedFormUrl, dashboardId, and dashboardUrl.

Or use templateId when creating via POST /api/forms:

curl -X POST https://sutrena.com/api/forms \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"templateId": "feedback", "createDashboard": true}'

Webhooks

Get notified when submissions come in. HMAC-SHA256 signed. Slack and Discord templates built in. Custom field mapping if you need different key names.

MethodEndpointDescription
POST/api/webhooksCreate a webhook (returns secret once)
GET/api/webhooksList your webhooks
GET/api/webhooks/:idGet webhook details
PATCH/api/webhooks/:idUpdate URL, events, active status, or field mapping
DELETE/api/webhooks/:idDelete a webhook
POST/api/webhooks/:id/testSend a test delivery (Builder+)
GET/api/webhooks/:id/deliveriesView delivery history (Builder+)

Create a webhook

curl -X POST https://sutrena.com/api/webhooks \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hook",
    "events": ["form.submission"],
    "description": "Notify on new submissions",
    "template": "default",
    "fieldMapping": {"email": "user_email", "name": "user_name"}
  }'

The response includes a secret for HMAC verification. Save it -- it is only shown once.

Templates

Set template to "slack" or "discord" and the payload formats itself for that platform. "default" gives you raw JSON.

Link webhooks to forms

Pass webhookIds when creating or updating a form.

curl -X POST https://sutrena.com/api/forms \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "templateId": "contact",
    "webhookIds": ["WEBHOOK_ID"]
  }'

Webhook payload

Each delivery is JSON with submission data. Signed with X-Sutrena-Signature-256.

{
  "event": "form.submission",
  "formId": "abc-123",
  "submissionId": "sub-456",
  "payload": {
    "email": "[email protected]",
    "name": "Alice",
    "message": "Hello!"
  },
  "submittedAt": "2026-03-01T12:00:00Z"
}

HMAC verification (Node.js)

Check the signature against your webhook secret to confirm the request came from Sutrena.

import crypto from "crypto";

function verifyWebhook(body: string, signature: string, secret: string): boolean {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// In your webhook handler:
app.post("/hook", (req, res) => {
  const signature = req.headers["x-nishkala-signature-256"];
  const rawBody = JSON.stringify(req.body);
  if (!verifyWebhook(rawBody, signature, WEBHOOK_SECRET)) {
    return res.status(401).send("Invalid signature");
  }
  // Process the webhook...
  res.sendStatus(200);
});

MCP

40 MCP tools at /api/mcp. AI agents (Claude Code, Cursor, Windsurf, Codex) can create forms, read submissions, build dashboards, and manage webhooks directly.

Discovery

MCP config lives at /.well-known/mcp.json.

Setup for Claude Code

claude mcp add sutrena --transport streamable-http https://sutrena.com/api/mcp \
  --header "Authorization: Bearer YOUR_KEY"

Setup for Cursor / Windsurf

Add to .cursor/mcp.json or equivalent:

{
  "mcpServers": {
    "sutrena": {
      "url": "https://sutrena.com/api/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_KEY"
      }
    }
  }
}

Available tools

ToolDescription
sutrena_create_formCreate a new form with fields
sutrena_list_formsList all your forms
sutrena_update_formUpdate form fields or settings
sutrena_delete_formDelete a form
sutrena_duplicate_formDuplicate an existing form
sutrena_create_from_templateCreate form + dashboard from a template
sutrena_get_submissionsGet submissions for a form
sutrena_search_submissionsSearch submissions with filters
sutrena_export_csvExport submissions as CSV
sutrena_create_dashboardCreate a dashboard with DSL
sutrena_update_dashboardUpdate dashboard DSL or settings
sutrena_delete_dashboardDelete a dashboard
sutrena_create_webhookCreate a webhook endpoint
sutrena_list_webhooksList your webhooks
sutrena_delete_webhookDelete a webhook
sutrena_test_webhookSend a test delivery
sutrena_get_webhook_deliveriesView delivery history
sutrena_set_subdomainSet your subdomain
sutrena_add_custom_domainAdd a custom domain (Builder+)
sutrena_list_custom_domainsList custom domains with status
sutrena_delete_custom_domainRemove a custom domain
sutrena_create_pageCreate an HTML page
sutrena_update_pageUpdate a page's HTML or metadata
sutrena_list_pagesList all pages
sutrena_delete_pageDelete a page
sutrena_upsert_submissionUpdate or insert a submission by unique key
sutrena_upload_page_assetPresign a page asset upload
sutrena_list_page_assetsList page assets
sutrena_delete_page_assetDelete a page asset
sutrena_check_usageCheck plan usage and quotas

Transport

Streamable HTTP. POST JSON-RPC to /api/mcp with your Authorization header. Sessions are managed automatically -- max 5 per user, 30-minute inactivity timeout.

Account Usage

Check all your quotas in a single call. Returns current usage for forms, pages, dashboards, webhooks, custom domains, and storage.

GET /api/account/usage

Requires authentication (Bearer token or session).

curl -H "Authorization: Bearer st_trial_xxx" \
  https://sutrena.com/api/account/usage

# Response:
{
  "data": {
    "plan": "free",
    "claimed": false,
    "usage": {
      "forms": { "current": 2, "limit": 3, "remaining": 1 },
      "pages": { "current": 1, "limit": 3, "remaining": 2 },
      "dashboards": { "current": 0, "limit": 3, "remaining": 3 },
      "webhooks": { "current": 0, "limit": 1, "remaining": 1 },
      "customDomains": { "current": 0, "limit": 0, "remaining": 0 },
      "storage": {
        "current": 0, "limit": 10485760, "remaining": 10485760,
        "currentFormatted": "0B", "limitFormatted": "10MB"
      }
    },
    "restrictions": ["no_export", "no_upsert"]
  },
  "_meta": { ... }
}

Claim Flow

When an agent creates an account via POST /api/trial, the user gets a 24-hour window to claim their data by signing in with GitHub or Google. Unclaimed accounts are automatically deleted.

How it works

1. Agent calls POST /api/trial — response includes claimUrl and claimDeadline.

2. Agent builds forms, pages, dashboards with the key.

3. Every API response includes _meta.claim with remaining time.

4. Agent tells the user: "Visit this URL to keep your Sutrena data."

5. User visits the claimUrl in a browser → signs in with GitHub/Google → data migrated → permanent account.

6. Unclaimed accounts are cleaned up every 30 minutes after the 24-hour deadline.

Account & Keys

Key management needs a session cookie (sign in first). The account endpoint works with either API key or session.

MethodEndpointDescription
GET/api/accountGet plan info, email, provider, subscription status
POST/api/keysGenerate a new API key (session auth only)
GET/api/keysList all your API keys (session auth only)
DELETE/api/keys/:idRevoke an API key (session auth only)
POST/api/keys/:id/rotateAtomically rotate a key (deactivates old, creates new)

Get account info

curl https://sutrena.com/api/account \
  -H "Authorization: Bearer YOUR_KEY"

Example response:

{
  "data": {
    "plan": "pro",
    "email": "[email protected]",
    "name": "Alice",
    "provider": "github",
    "subscriptionStatus": "active",
    "createdAt": "2026-01-15T10:00:00Z"
  },
  "_meta": {
    "plan": "pro",
    "limits": {"forms": 20, "dashboards": 20, "webhooks": 3}
  }
}

Generate an API key

Needs a session cookie. The raw key is shown once and never again.

curl -X POST https://sutrena.com/api/keys \
  -H "Content-Type: application/json" \
  -H "Cookie: sutrena_session=SESSION_ID" \
  -d '{"label": "production"}'

Rotate a key

Kills the old key, creates a new one with the same label. Atomic. New raw key returned once.

curl -X POST https://sutrena.com/api/keys/KEY_ID/rotate \
  -H "Authorization: Bearer YOUR_KEY"

Plans & Pricing

Free plan has no expiry. Paid plans start at $9/month.

FeatureFree ($0)Builder ($9/mo)Pro ($29/mo)Scale ($79/mo)
DurationNo expiryUnlimitedUnlimitedUnlimited
Projects (forms + pages + dashboards)1050200Unlimited
Submissions500 per form5,000 per formUnlimitedUnlimited
Webhooks1510Unlimited
CSV export--YesYesYes
GDPR deletion--YesYesYes
MCP toolsYesYesYesYes
Upsert API--YesYesYes
Custom Domains--15Unlimited
Storage50 MB500 MB5 GBUnlimited
Max Asset Size2 MB5 MB20 MB50 MB
Hosted forms & embedsYesYesYesYes

Embedding

Two options. Drop-in embed snippet, or build your own form and POST to the submit endpoint. Pick whichever fits.

Option 1: Embed snippet (2 lines)

Paste this HTML. It loads an iframe with auto-resizing.

<div data-sutrena-form="FORM_ID"></div>
<script src="https://sutrena.com/embed.js"></script>

Custom host, if you need it:

<div data-sutrena-form="FORM_ID" data-sutrena-host="https://your-domain.com"></div>
<script src="https://sutrena.com/embed.js"></script>

Option 2: Custom form with fetch

Your HTML, your styling. Just POST JSON to the submit URL. No SDK.

<form id="my-form">
  <input name="email" type="email" required />
  <input name="name" type="text" required />
  <textarea name="message" required></textarea>
  <button type="submit">Send</button>
</form>

<script>
  document.getElementById("my-form").addEventListener("submit", async (e) => {
    e.preventDefault();
    const data = Object.fromEntries(new FormData(e.target));
    const res = await fetch("https://sutrena.com/api/forms/FORM_ID/submit", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(data),
    });
    if (res.ok) {
      e.target.innerHTML = "<p>Thank you!</p>";
    }
  });
</script>

postMessage events

The embed iframe talks to the parent page via postMessage. Three events:

Event typeDataDescription
sutrena:resize{ height: number }Iframe requests height change for content fit
sutrena:submitted{}Form was successfully submitted
sutrena:redirect{ url: string }Form requests a redirect (if redirectUrl is set)
// Listen for embed events
window.addEventListener("message", (e) => {
  if (e.data?.type === "sutrena:submitted") {
    console.log("Form submitted!");
    // Track conversion, show thank-you, etc.
  }
});

Also fires a sutrena:submitted CustomEvent on the container element:

document.querySelector("[data-sutrena-form]")
  .addEventListener("sutrena:submitted", () => {
    console.log("Form submitted!");
  });
Documentation -- Sutrena | Sutrena