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.
Free API key, first form, under a minute. No signup.
curl -X POST https://sutrena.com/api/trialReturns 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.
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.
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"]}
]
}'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.
Three ways to authenticate. Pick one.
Authorization: Bearer YOUR_KEYPOST /api/trial to get an st_trial_ key. No expiry. 5 per 24 hours per IP.
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.
OAuth sign-in sets a sutrena_session cookie. The dashboard and key management endpoints use this automatically.
CRUD for forms. Each one gets fields, a public submit URL, and a hosted page.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/forms | Create a new form |
| GET | /api/forms | List all your forms |
| GET | /api/forms/:id | Get form details |
| PUT | /api/forms/:id | Update a form |
| DELETE | /api/forms/:id | Delete a form and its dashboards |
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"]
}| Type | Validation options | Notes |
|---|---|---|
| text | minLength, maxLength, pattern | General text input |
| minLength, maxLength | Validates email format | |
| textarea | minLength, maxLength | Multi-line text |
| number | min, max | Numeric value |
| select | options (required) | Dropdown, single choice |
| multiselect | options (required) | Multiple choice (value is string[]) |
| checkbox | -- | Boolean toggle |
| url | minLength, maxLength, pattern | Validates URL format |
| tel | minLength, maxLength, pattern | Phone number |
| date | -- | Date picker |
| hidden | -- | Not shown to user, sent with submission |
| file | accept, maxFileSize | File upload (max 50 MB per file) |
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.
Submit from browsers, mobile apps, or server-side code. The submit endpoint is public with CORS on.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/forms/:id/submit | Submit data (public, no auth, CORS enabled) |
| GET | /api/forms/:id/submissions | Search and list submissions (auth required) |
| DELETE | /api/forms/:id/submissions?email=X | GDPR deletion by email (Builder+) |
| GET | /api/forms/:id/export | CSV export (Builder+) |
| GET | /api/forms/:id/results | Public aggregated results (if enabled) |
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"}'Filter with query params. Cursor-based pagination.
| Parameter | Type | Description |
|---|---|---|
| search | string | Full-text search across all fields |
| field | string | Filter by a specific field name |
| value | string | Filter by field value (use with field) |
| from | ISO 8601 | Start date filter |
| to | ISO 8601 | End date filter |
| status | string | "clean" or "spam" |
| limit | number | Results per page (default varies) |
| cursor | string | Pagination cursor from previous response |
curl "https://sutrena.com/api/forms/FORM_ID/submissions?field=email&[email protected]&limit=10" \
-H "Authorization: Bearer YOUR_KEY"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"All submissions as CSV. Builder plan and above.
curl "https://sutrena.com/api/forms/FORM_ID/export" \
-H "Authorization: Bearer YOUR_KEY" \
-o submissions.csvWhen 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/resultsReturns 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}
]
}
}Two-step presigned URL flow. Get a URL, upload to it, include the object ID in your submission. 50 MB max per file.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/forms/:id/upload | Get a presigned upload URL (public, rate-limited) |
| GET | /api/forms/:id/files/:objectId | Download a file (302 redirect) |
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.
curl -X PUT "UPLOAD_URL" \
-H "Content-Type: application/pdf" \
--data-binary @resume.pdfUse 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"}'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).
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.
# 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).
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.
| Method | Path | Notes |
|---|---|---|
| POST | /api/pages | Create page |
| GET | /api/pages | List your pages |
| GET | /api/pages/:id | Page details with full HTML/CSS |
| PUT | /api/pages/:id | Update HTML/CSS/title (slug immutable) |
| DELETE | /api/pages/:id | Delete page |
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.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/account/subdomains | List all subdomains with page counts |
| POST | /api/account/subdomains | Create a new subdomain |
| GET | /api/account/subdomain | Get default subdomain (deprecated) |
| PUT | /api/account/subdomain | Change default subdomain (deprecated) |
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.
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.
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.
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.
3-30 characters. Lowercase alphanumeric and hyphens only. Cannot start or end with a hyphen.
Point your own domain at Sutrena pages. Builder plan and above.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/account/domains | Add a domain (returns CNAME instructions) |
| GET | /api/account/domains | List domains with DNS/SSL status |
| GET | /api/account/domains/:id | Check domain status |
| DELETE | /api/account/domains/:id | Remove 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.
Builder: 1 custom domain. Pro: 5 custom domains. Scale: unlimited.
Upload images, CSS, and other static files for your pages. Served from a CDN.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/pages/assets | Presign an upload (returns uploadUrl + publicUrl) |
| GET | /api/pages/assets | List assets (optional ?pageId= filter) |
| DELETE | /api/pages/assets/:id | Delete an asset |
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| Plan | Max file size | Total storage |
|---|---|---|
| Free | 2 MB | 50 MB |
| Builder | 5 MB | 500 MB |
| Pro | 20 MB | 5 GB |
| Scale | 50 MB | Unlimited |
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.
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.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/dashboards | Create a dashboard (formId, data array, or csvObjectId) |
| POST | /api/dashboards/upload | Presign CSV upload for data dashboard |
| GET | /api/dashboards | List all your dashboards |
| GET | /api/dashboards/:id | Get dashboard details |
| PUT | /api/dashboards/:id | Update title, DSL, or visibility |
| DELETE | /api/dashboards/:id | Delete a dashboard |
| GET | /api/dashboards/:id/download | Download original CSV file |
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"}
]
}
}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"]}
]
}
}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)"}
]
}
}Version must be 1. Max 20 widgets. Private by default -- set isPublic: true to share.
| Widget type | Required fields | Optional fields | Description |
|---|---|---|---|
| metric_card | title, value | status | Single number. value is count(*), count(field), sum(field), avg(field), min(field), or max(field). status filters by "clean" or "spam". |
| data_table | title, columns | limit, sort, sortBy, sortOrder | Table of submissions. columns is an array of field names (max 20). sort: "newest" or "oldest". limit: 1-100. |
| text_block | title, content | -- | Static text content (max 5000 chars) |
| pie_chart | title, groupBy | aggregate | Pie chart grouped by a field name (e.g. industry). Optional aggregate: sum(field), avg(field), min(field), max(field). Defaults to counting rows. |
| bar_chart | title, groupBy | aggregate | Bar 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_chart | title, groupBy | aggregate | Line 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. |
14 built-in templates. Pre-configured fields and a matching dashboard DSL. One call creates both.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/templates | List all 14 templates (public, no auth) |
| GET | /api/templates/:id | Get template details including fields and dashboard DSL (public) |
| POST | /api/templates/:id | Create a form and dashboard from a template (auth required) |
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}'Get notified when submissions come in. HMAC-SHA256 signed. Slack and Discord templates built in. Custom field mapping if you need different key names.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/webhooks | Create a webhook (returns secret once) |
| GET | /api/webhooks | List your webhooks |
| GET | /api/webhooks/:id | Get webhook details |
| PATCH | /api/webhooks/:id | Update URL, events, active status, or field mapping |
| DELETE | /api/webhooks/:id | Delete a webhook |
| POST | /api/webhooks/:id/test | Send a test delivery (Builder+) |
| GET | /api/webhooks/:id/deliveries | View delivery history (Builder+) |
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.
Set template to "slack" or "discord" and the payload formats itself for that platform. "default" gives you raw JSON.
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"]
}'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"
}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);
});40 MCP tools at /api/mcp. AI agents (Claude Code, Cursor, Windsurf, Codex) can create forms, read submissions, build dashboards, and manage webhooks directly.
MCP config lives at /.well-known/mcp.json.
claude mcp add sutrena --transport streamable-http https://sutrena.com/api/mcp \
--header "Authorization: Bearer YOUR_KEY"Add to .cursor/mcp.json or equivalent:
{
"mcpServers": {
"sutrena": {
"url": "https://sutrena.com/api/mcp",
"headers": {
"Authorization": "Bearer YOUR_KEY"
}
}
}
}| Tool | Description |
|---|---|
| sutrena_create_form | Create a new form with fields |
| sutrena_list_forms | List all your forms |
| sutrena_update_form | Update form fields or settings |
| sutrena_delete_form | Delete a form |
| sutrena_duplicate_form | Duplicate an existing form |
| sutrena_create_from_template | Create form + dashboard from a template |
| sutrena_get_submissions | Get submissions for a form |
| sutrena_search_submissions | Search submissions with filters |
| sutrena_export_csv | Export submissions as CSV |
| sutrena_create_dashboard | Create a dashboard with DSL |
| sutrena_update_dashboard | Update dashboard DSL or settings |
| sutrena_delete_dashboard | Delete a dashboard |
| sutrena_create_webhook | Create a webhook endpoint |
| sutrena_list_webhooks | List your webhooks |
| sutrena_delete_webhook | Delete a webhook |
| sutrena_test_webhook | Send a test delivery |
| sutrena_get_webhook_deliveries | View delivery history |
| sutrena_set_subdomain | Set your subdomain |
| sutrena_add_custom_domain | Add a custom domain (Builder+) |
| sutrena_list_custom_domains | List custom domains with status |
| sutrena_delete_custom_domain | Remove a custom domain |
| sutrena_create_page | Create an HTML page |
| sutrena_update_page | Update a page's HTML or metadata |
| sutrena_list_pages | List all pages |
| sutrena_delete_page | Delete a page |
| sutrena_upsert_submission | Update or insert a submission by unique key |
| sutrena_upload_page_asset | Presign a page asset upload |
| sutrena_list_page_assets | List page assets |
| sutrena_delete_page_asset | Delete a page asset |
| sutrena_check_usage | Check plan usage and quotas |
Streamable HTTP. POST JSON-RPC to /api/mcp with your Authorization header. Sessions are managed automatically -- max 5 per user, 30-minute inactivity timeout.
Check all your quotas in a single call. Returns current usage for forms, pages, dashboards, webhooks, custom domains, and storage.
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": { ... }
}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.
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.
Key management needs a session cookie (sign in first). The account endpoint works with either API key or session.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/account | Get plan info, email, provider, subscription status |
| POST | /api/keys | Generate a new API key (session auth only) |
| GET | /api/keys | List all your API keys (session auth only) |
| DELETE | /api/keys/:id | Revoke an API key (session auth only) |
| POST | /api/keys/:id/rotate | Atomically rotate a key (deactivates old, creates new) |
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}
}
}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"}'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"Free plan has no expiry. Paid plans start at $9/month.
| Feature | Free ($0) | Builder ($9/mo) | Pro ($29/mo) | Scale ($79/mo) |
|---|---|---|---|---|
| Duration | No expiry | Unlimited | Unlimited | Unlimited |
| Projects (forms + pages + dashboards) | 10 | 50 | 200 | Unlimited |
| Submissions | 500 per form | 5,000 per form | Unlimited | Unlimited |
| Webhooks | 1 | 5 | 10 | Unlimited |
| CSV export | -- | Yes | Yes | Yes |
| GDPR deletion | -- | Yes | Yes | Yes |
| MCP tools | Yes | Yes | Yes | Yes |
| Upsert API | -- | Yes | Yes | Yes |
| Custom Domains | -- | 1 | 5 | Unlimited |
| Storage | 50 MB | 500 MB | 5 GB | Unlimited |
| Max Asset Size | 2 MB | 5 MB | 20 MB | 50 MB |
| Hosted forms & embeds | Yes | Yes | Yes | Yes |
Two options. Drop-in embed snippet, or build your own form and POST to the submit endpoint. Pick whichever fits.
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>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>The embed iframe talks to the parent page via postMessage. Three events:
| Event type | Data | Description |
|---|---|---|
| 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!");
});