REST API v1
GRTforms API
Programmatic access to your forms, submissions, and webhooks. This page is server-rendered documentation for the live /api/v1 surface. An interactive try-it playground is not part of v1.
Prefer a machine-readable spec? Download the OpenAPI document.
Overview
Authenticate with an API key, call HTTPS endpoints under /api/v1, and receive JSON. Form ids and entry ids are cuids. List endpoints use cursor pagination.
- Base URL
- https://grtforms.com
- Versioning
- Current version is v1, expressed in the path as
/api/v1. There are no other versions. - Authentication
Authorization: Bearer grt_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx- Rate limits
- 100 requests per minute per API key. See Rate limits.
Authentication
The v1 API uses API keys only. Session cookies from the dashboard are not accepted on /api/v1.
- Sign in and open Account → API Keys at
/account/api-keys. - Create a key, choose scopes, and copy it immediately. The full secret is shown once.
- Send it on every request as a Bearer token. Keys look like
grt_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
Authorization: Bearer grt_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| Scope | Description |
|---|---|
| forms:read | Read form metadata and schema |
| forms:write | Create unpublished draft forms via the API or MCP |
| entries:read | Read submissions (sensitive fields redacted) |
| entries:write | Create submissions via the API |
| webhooks:write | Create, list, and delete webhooks |
| entries:read:sensitive | Read decrypted values of sensitive fields |
Revoked or expired keys return 401. A valid key missing a required scope returns 403.
Errors
Errors use a consistent JSON body. Optional details may include validation issues or the missing scope.
{
"error": {
"code": "unauthorized",
"message": "Invalid API key"
}
}{
"error": {
"code": "forbidden",
"message": "Missing required scope: forms:read",
"details": {
"requiredScope": "forms:read",
"providedScopes": ["entries:read"]
}
}
}| Status | Code | When |
|---|---|---|
| 400 | validation_error | The request body or query parameters failed validation. |
| 401 | unauthorized | Missing, malformed, revoked, expired, or unknown API key. |
| 403 | forbidden | The API key is valid but missing a required scope. |
| 404 | not_found | The form, entry, webhook, or user was not found. |
| 429 | rate_limit_exceeded | Too many requests in the current minute window. |
| 500 | internal_error | An unexpected server error occurred. |
Rate limits
Authenticated v1 requests are limited to 100 requests per minute per API key. Successful responses include remaining-quota headers. A 429 also includes Retry-After.
X-RateLimit-Remaining— requests left in the current windowX-RateLimit-Reset— Unix timestamp when the window resetsRetry-After— seconds to wait (429 only)
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded"
}
}Examples
Worked curls against production. Replace the placeholder key and ids with values from your account. Submissions are called entries in the API.
List forms
Requires forms:read.
curl "https://grtforms.com/api/v1/forms?limit=20" \
-H "Authorization: Bearer grt_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"{
"data": [
{
"id": "cm2form8k0001abcdefghijk",
"name": "Event registration",
"description": "RSVP for the October meetup",
"slug": "event-registration-y2gt02",
"isPublished": true,
"submissionCount": 128,
"createdAt": "2026-09-12T09:14:00.000Z",
"updatedAt": "2026-10-01T16:22:00.000Z"
}
],
"pagination": {
"hasMore": false,
"nextCursor": null
}
}Get a form
Includes schema and settings so you can map field ids before reading or creating entries.
curl "https://grtforms.com/api/v1/forms/cm2form8k0001abcdefghijk" \
-H "Authorization: Bearer grt_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Create a form
Requires forms:write. The form is created as an unpublished draft.
curl -X POST "https://grtforms.com/api/v1/forms" \
-H "Authorization: Bearer grt_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"name":"Event registration","description":"RSVP for the October meetup"}'{
"data": {
"id": "cm2form8k0001abcdefghijk",
"name": "Event registration",
"description": "RSVP for the October meetup",
"slug": "event-registration-y2gt02",
"schema": [
{
"id": "full_name",
"type": "short_text",
"label": "Full name",
"validation": { "required": true }
}
],
"settings": {
"submitButtonText": "Submit",
"successMessage": "Thank you for your submission!"
},
"isPublished": false,
"submissionCount": 0,
"createdAt": "2026-10-09T12:00:00.000Z",
"updatedAt": "2026-10-09T12:00:00.000Z"
}
}List submissions
GET /api/v1/forms/{formId}/entries requires entries:read.
curl "https://grtforms.com/api/v1/forms/cm2form8k0001abcdefghijk/entries?limit=20&sort=desc&field=email&[email protected]" \
-H "Authorization: Bearer grt_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Get a submission
curl "https://grtforms.com/api/v1/forms/cm2form8k0001abcdefghijk/entries/cm2entry9m0002abcdefghijk" \
-H "Authorization: Bearer grt_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Webhooks
Create an HTTPS endpoint, then register it. Deliveries include X-GRTforms-Signature, X-GRTforms-Event, and X-GRTforms-Delivery. Payload version is 2026-10.
curl -X POST "https://grtforms.com/api/v1/webhooks" \
-H "Authorization: Bearer grt_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"formId": "cm2form8k0001abcdefghijk",
"url": "https://example.com/hooks/grtforms",
"events": ["entry.created"]
}'{
"version": "2026-10",
"event": "entry.created",
"timestamp": "2026-10-02T14:03:11.000Z",
"data": {
"form": {
"id": "cm2form8k0001abcdefghijk",
"name": "Event registration",
"slug": "event-registration-y2gt02"
},
"entry": {
"id": "cm2entry9m0002abcdefghijk",
"data": {
"full_name": "Ada Lovelace",
"email": "[email protected]"
},
"metadata": { "source": "api" },
"createdAt": "2026-10-02T14:03:11.000Z",
"updatedAt": "2026-10-02T14:03:11.000Z"
}
}
}const crypto = require("crypto");
function hmacKey(secret) {
if (String(secret).startsWith("whsec_")) {
return crypto.createHash("sha256").update(secret).digest("hex");
}
return secret;
}
function verifySignature(payload, signature, secret) {
const key = hmacKey(secret);
const [tPart, vPart] = signature.split(",");
const timestamp = tPart.substring(2);
const providedSig = vPart.substring(3);
const expectedSig = crypto
.createHmac("sha256", key)
.update(`${timestamp}.${payload}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(providedSig, "hex"),
Buffer.from(expectedSig, "hex")
);
}REST reference
Every live public or authenticated /api/v1 route. Dashboard-only routes (session login, builder, Excel export) are not part of this API.
/api/v1/me
Get current user
Returns the authenticated account and the API key used for the request. Any valid key works; no extra scope is required.
- Auth
- Bearer API key
- Scope
- Any valid key
Request
curl "https://grtforms.com/api/v1/me" \
-H "Authorization: Bearer grt_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Response
{
"data": {
"user": {
"id": "cm2user7q0000abcdefghijk",
"email": "[email protected]",
"name": "Ada Lovelace",
"createdAt": "2026-09-01T12:00:00.000Z"
},
"apiKey": {
"id": "cm2key1r0004abcdefghijk",
"name": "CI pipeline",
"scopes": ["forms:read", "entries:read"],
"expiresAt": null,
"usageCount": 42
}
}
}- 200 User and API key info
- 401 Unauthorized
/api/v1/forms
List forms
Returns a cursor-paginated list of forms owned by the authenticated user, newest first.
- Auth
- Bearer API key
- Scope
- forms:read
| Name | In | Required | Description |
|---|---|---|---|
| limit | query | No | Page size. Default 50, maximum 100. |
| cursor | query | No | Form id from pagination.nextCursor to fetch the next page. |
Request
curl "https://grtforms.com/api/v1/forms?limit=20" \
-H "Authorization: Bearer grt_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Response
{
"data": [
{
"id": "cm2form8k0001abcdefghijk",
"name": "Event registration",
"description": "RSVP for the October meetup",
"slug": "event-registration-y2gt02",
"isPublished": true,
"submissionCount": 128,
"createdAt": "2026-09-12T09:14:00.000Z",
"updatedAt": "2026-10-01T16:22:00.000Z"
}
],
"pagination": {
"hasMore": false,
"nextCursor": null
}
}- 200 List of forms
- 401 Unauthorized
- 403 Missing forms:read
/api/v1/forms
Create a form
Creates an unpublished draft form. Requires forms:write. The schema is validated with the same form schema validators as the builder. Plan form-count limits are enforced. The form is never published by this endpoint.
- Auth
- Bearer API key
- Scope
- forms:write
Request
curl -X POST "https://grtforms.com/api/v1/forms" \
-H "Authorization: Bearer grt_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"name":"Event registration","description":"RSVP for the October meetup"}'{
"name": "Event registration",
"description": "RSVP for the October meetup",
"schema": [
{
"id": "full_name",
"type": "short_text",
"label": "Full name",
"validation": { "required": true }
}
]
}Response
{
"data": {
"id": "cm2form8k0001abcdefghijk",
"name": "Event registration",
"description": "RSVP for the October meetup",
"slug": "event-registration-y2gt02",
"schema": [
{
"id": "full_name",
"type": "short_text",
"label": "Full name",
"validation": { "required": true }
}
],
"settings": {
"submitButtonText": "Submit",
"successMessage": "Thank you for your submission!"
},
"isPublished": false,
"submissionCount": 0,
"createdAt": "2026-10-09T12:00:00.000Z",
"updatedAt": "2026-10-09T12:00:00.000Z"
}
}- 201 Draft form created
- 400 Validation error
- 401 Unauthorized
- 403 Missing forms:write or plan form limit
/api/v1/forms/{formId}
Get form
Returns one form, including its field schema and settings. Settings omit secrets such as the form password hash. The public fill URL is /f/{slug}.
- Auth
- Bearer API key
- Scope
- forms:read
| Name | In | Required | Description |
|---|---|---|---|
| formId | path | Yes | Form id (cuid). |
Request
curl "https://grtforms.com/api/v1/forms/cm2form8k0001abcdefghijk" \
-H "Authorization: Bearer grt_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Response
{
"data": {
"id": "cm2form8k0001abcdefghijk",
"name": "Event registration",
"description": "RSVP for the October meetup",
"slug": "event-registration-y2gt02",
"schema": [
{
"id": "full_name",
"type": "short_text",
"label": "Full name",
"validation": { "required": true }
},
{
"id": "email",
"type": "email",
"label": "Email",
"validation": { "required": true }
}
],
"settings": {
"submitButtonText": "Register",
"successMessage": "You're on the list.",
"hideHeader": false,
"hideTitle": false,
"hideDescription": false
},
"isPublished": true,
"submissionCount": 128,
"createdAt": "2026-09-12T09:14:00.000Z",
"updatedAt": "2026-10-01T16:22:00.000Z"
}
}- 200 Form details
- 401 Unauthorized
- 403 Missing forms:read
- 404 Form not found
/api/v1/forms/{formId}/entries
List entries
Returns a cursor-paginated list of submissions for a form. Fields marked sensitive are returned as "[REDACTED]" unless the key has entries:read:sensitive. Search by field value with the field and value query parameters together.
- Auth
- Bearer API key
- Scope
- entries:read
| Name | In | Required | Description |
|---|---|---|---|
| formId | path | Yes | Form id (cuid). |
| limit | query | No | Page size. Default 50, maximum 100. |
| cursor | query | No | Entry id from pagination.nextCursor. |
| from | query | No | Inclusive start date (ISO 8601). |
| to | query | No | Inclusive end date (ISO 8601). The full end day is included. |
| sort | query | No | createdAt order: desc (default) or asc. |
| fields | query | No | Comma-separated field ids to include in data. |
| field | query | No | Field id to search. Must be sent together with value. |
| value | query | No | Field value to match. Must be sent together with field. |
Request
curl "https://grtforms.com/api/v1/forms/cm2form8k0001abcdefghijk/entries?limit=20&sort=desc&field=email&[email protected]" \
-H "Authorization: Bearer grt_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Response
{
"data": [
{
"id": "cm2entry9m0002abcdefghijk",
"data": {
"full_name": "Ada Lovelace",
"email": "[email protected]"
},
"metadata": { "source": "web" },
"createdAt": "2026-10-02T14:03:11.000Z",
"updatedAt": "2026-10-02T14:03:11.000Z"
}
],
"pagination": {
"hasMore": true,
"nextCursor": "cm2entry9m0002abcdefghijk"
}
}- 200 List of entries
- 400 field and value must be sent together
- 401 Unauthorized
- 403 Missing entries:read
- 404 Form not found
/api/v1/forms/{formId}/entries
Create entry
Creates a submission for a form. Keys in data should match field ids from the form schema. The server applies the same validation and conditional logic as a public submit: hidden and skipped fields are dropped, then remaining fields are validated. The server stores source: "api" on the entry metadata after merging any caller metadata, or source: "make" or "n8n" when the caller sets those values. A caller cannot mark the entry as a public-form submission. Entries marked make or n8n are treated as API-created for consent. Sending true (or { "agreed": true }) for a Consent field stores the form's current agreement text, hash, version, timestamp, and source: "api". That means the API caller is vouching that the person consented to that wording. Mailchimp skips consent on API-created entries — both Consent fields and a single-option checkbox used for opt-in — unless the form owner turns on Allow API-created consent. A successful create queues an entry.created webhook. Rate limits and the entries:write scope apply.
- Auth
- Bearer API key
- Scope
- entries:write
| Name | In | Required | Description |
|---|---|---|---|
| formId | path | Yes | Form id (cuid). |
Request
curl -X POST "https://grtforms.com/api/v1/forms/cm2form8k0001abcdefghijk/entries" \
-H "Authorization: Bearer grt_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"data": {
"full_name": "Ada Lovelace",
"email": "[email protected]"
}
}'{
"data": {
"full_name": "Ada Lovelace",
"email": "[email protected]"
},
"metadata": {
"source": "crm"
}
}Response
{
"data": {
"id": "cm2entry9m0002abcdefghijk",
"data": {
"full_name": "Ada Lovelace",
"email": "[email protected]"
},
"metadata": { "source": "crm" },
"createdAt": "2026-10-02T14:03:11.000Z",
"updatedAt": "2026-10-02T14:03:11.000Z"
}
}- 201 Entry created
- 400 Invalid request body or form validation failed
- 401 Unauthorized
- 403 Missing entries:write
- 404 Form not found
/api/v1/forms/{formId}/entries/count
Count entries
Returns the number of submissions for a form, optionally filtered by createdAt range.
- Auth
- Bearer API key
- Scope
- entries:read
| Name | In | Required | Description |
|---|---|---|---|
| formId | path | Yes | Form id (cuid). |
| from | query | No | Inclusive start date (ISO 8601). |
| to | query | No | Inclusive end date (ISO 8601). |
Request
curl "https://grtforms.com/api/v1/forms/cm2form8k0001abcdefghijk/entries/count" \
-H "Authorization: Bearer grt_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Response
{
"data": {
"count": 128
}
}- 200 Entry count
- 401 Unauthorized
- 403 Missing entries:read
- 404 Form not found
/api/v1/forms/{formId}/entries/{entryId}
Get entry
Returns a single submission. Sensitive fields follow the same redaction rules as list entries.
- Auth
- Bearer API key
- Scope
- entries:read
| Name | In | Required | Description |
|---|---|---|---|
| formId | path | Yes | Form id (cuid). |
| entryId | path | Yes | Entry (submission) id (cuid). |
Request
curl "https://grtforms.com/api/v1/forms/cm2form8k0001abcdefghijk/entries/cm2entry9m0002abcdefghijk" \
-H "Authorization: Bearer grt_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Response
{
"data": {
"id": "cm2entry9m0002abcdefghijk",
"data": {
"full_name": "Ada Lovelace",
"email": "[email protected]"
},
"metadata": { "source": "web" },
"createdAt": "2026-10-02T14:03:11.000Z",
"updatedAt": "2026-10-02T14:03:11.000Z"
}
}- 200 Entry details
- 401 Unauthorized
- 403 Missing entries:read
- 404 Form or entry not found
/api/v1/webhooks
List webhooks
Returns webhooks configured for a form. The formId query parameter is required. Listing uses the webhooks:write scope (there is no separate read-only webhook scope).
- Auth
- Bearer API key
- Scope
- webhooks:write
| Name | In | Required | Description |
|---|---|---|---|
| formId | query | Yes | Form id whose webhooks to list. |
Request
curl "https://grtforms.com/api/v1/webhooks?formId=cm2form8k0001abcdefghijk" \
-H "Authorization: Bearer grt_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Response
{
"data": [
{
"id": "cm2hook0p0003abcdefghijk",
"url": "https://example.com/hooks/grtforms",
"events": ["entry.created"],
"isEnabled": true,
"failureCount": 0,
"lastFailureAt": null,
"createdAt": "2026-10-01T10:00:00.000Z",
"updatedAt": "2026-10-01T10:00:00.000Z"
}
]
}- 200 List of webhooks
- 400 Missing formId
- 401 Unauthorized
- 403 Missing webhooks:write
- 404 Form not found
/api/v1/webhooks
Create webhook
Creates a webhook for a form. The signing secret is returned once and is never shown again. In production the URL must use HTTPS. Events: entry.created, entry.updated, entry.deleted.
- Auth
- Bearer API key
- Scope
- webhooks:write
Request
curl -X POST "https://grtforms.com/api/v1/webhooks" \
-H "Authorization: Bearer grt_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"formId": "cm2form8k0001abcdefghijk",
"url": "https://example.com/hooks/grtforms",
"events": ["entry.created"]
}'{
"formId": "cm2form8k0001abcdefghijk",
"url": "https://example.com/hooks/grtforms",
"events": ["entry.created"]
}Response
{
"data": {
"id": "cm2hook0p0003abcdefghijk",
"formId": "cm2form8k0001abcdefghijk",
"url": "https://example.com/hooks/grtforms",
"events": ["entry.created"],
"secret": "whsec_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"isEnabled": true,
"createdAt": "2026-10-01T10:00:00.000Z"
}
}- 201 Webhook created (secret included once)
- 400 Invalid body or non-HTTPS URL in production
- 401 Unauthorized
- 403 Missing webhooks:write
- 404 Form not found
/api/v1/webhooks/{webhookId}
Delete webhook
Permanently deletes a webhook you own.
- Auth
- Bearer API key
- Scope
- webhooks:write
| Name | In | Required | Description |
|---|---|---|---|
| webhookId | path | Yes | Webhook id (cuid). |
Request
curl -X DELETE "https://grtforms.com/api/v1/webhooks/cm2hook0p0003abcdefghijk" \
-H "Authorization: Bearer grt_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Response
{
"data": {
"success": true
}
}- 200 Webhook deleted
- 401 Unauthorized
- 403 Missing webhooks:write
- 404 Webhook not found
/api/v1/openapi
OpenAPI document
Public OpenAPI 3.1 JSON for the v1 API. No API key is required. Useful for code generation and machine-readable clients.
- Auth
- Public — no API key
- Scope
- —
Request
curl "https://grtforms.com/api/v1/openapi"Response
{
"openapi": "3.1.0",
"info": {
"title": "GRTforms API",
"version": "1.0.0"
},
"paths": {
"/api/v1/forms": {}
}
}- 200 OpenAPI 3.1 document
Coming soon
These endpoints are coming soon and are not available yet. Payments, DocuSign, and other third-party product APIs are not part of GRTforms v1.
/api/v1/forms/{formId}
Coming soonUpdate a form
Updating schema or settings via the API is not available. Use the form builder.
/api/v1/forms/{formId}
Coming soonDelete a form
Deleting forms via the API is not available.
Zapier
The GRTforms Zapier app is not available yet. Contact Support to hear when it is.
What the app can do
- New Entry — REST hook on
entry.createdwith a polling fallback. Form dropdown, schema-generated output fields, page breaks skipped. - New Form — polling trigger.
- Create Entry — same validation and hidden/skipped-field rules as a public submit.
- Find Entry — search by field value.
MCP server
A remote Model Context Protocol server at /api/mcp wraps this same REST API. Authenticate with a Bearer API key — there is no separate OAuth path. MCP setup for Claude Desktop and Cursor.
OpenAPI spec
The same contract is published as OpenAPI 3.1 JSON. If this page ever fails to render in a restricted browser, the spec is still available at the URL below.
curl "https://grtforms.com/api/v1/openapi"