> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.pinnacle.sh/v-2/api-reference/forms/create-form/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.pinnacle.sh/_mcp/server. # Create Form POST https://api.pinnacle.sh/forms Content-Type: application/json Create a hosted form without sending it. Returns the form object including its public URL — `https://forms.pinnacle.sh/{form_id}`. To also deliver the URL to a recipient over SMS or RCS in a single call, use [`POST /forms/send`](/api-reference/forms/send-form). Reference: https://docs.pinnacle.sh/api-reference/forms/create-form ## Authentication - `PINNACLE-API-KEY` header (required) — API Key authentication via header ## Request ### Body (application/json) This endpoint expects a CreateFormRequest. - `fields` (list of FormField, required) — Ordered list of fields the recipient will fill out. At least one field is required. - `can_update` (boolean, optional, default: false) — If true, the recipient can reopen the submission URL and edit their answers. When false (default), the submission URL becomes read-only (`Already submitted`) after the first submit. - `description` (string, optional) — Longer description rendered below the title. - `expires_at` (string, optional) — After this timestamp the form stops accepting submissions. Omit for no expiration. - `name` (string, optional) — Human-readable name for the form. Rendered as the form's title. - `theme_override` (FormThemeOverride, optional, nullable) — Per-form theme tweaks layered on top of your team's default theme. Omit or set to null to use team defaults. ## Response ### 200 Form created. - `id` (string, required) — Form id (starts with `form_`). - `url` (string, required) — Public shareable URL of the form (`https://forms.pinnacle.sh/{form_id}`). - `name` (string, required, nullable) — Human-readable name for the form. Rendered as the form's title. - `description` (string, required, nullable) — Longer description rendered below the title. - `fields` (list of FormField, required) — Ordered list of fields the recipient fills out. - `can_update` (boolean, required) — When true, the recipient can reopen the submission URL and edit their answers. When false, the URL becomes read-only after the first submit. - `expires_at` (string, required, nullable) — After this timestamp the form stops accepting submissions. Null means no expiration. - `theme_override` (FormThemeOverride, required, nullable) — Per-form theme tweaks layered on top of your team's default theme. Null means the team defaults are used as-is. - `submission_count` (integer, required) — Count of distinct completed submissions for this form. Does not increment on edits to an existing `can_update=true` submission. - `last_submitted_at` (string, required, nullable) — Timestamp of the most recent submission event. Updated on each new submit and on each edit of a `can_update=true` submission. - `archived_at` (string, required, nullable) — When set, the form is archived (soft-deleted) and cannot accept new submissions or be updated. Restore by PATCHing `archived_at` back to null. - `created_at` (string, required) — ISO 8601 timestamp of when the form was created. - `updated_at` (string, required) — ISO 8601 timestamp of the most recent form mutation. ## Errors ### 400 Bad Request Error Validation failed. The payload has missing required fields and/or invalid types. See [https://zod.dev/error-formatting](https://zod.dev/error-formatting) for more information. - `description` (string, required) — Human-readable summary of validation failures. - `errors` (ZodErrorErrors, required) — Structured dictionary of issues containing two main sections: - `errors`: Array of global validation errors not tied to specific fields - `properties`: Object mapping field names to their specific validation errors, where each field contains an `errors` array ### 401 Unauthorized Error The request lacks valid authentication credentials or the provided credentials are invalid. Ensure you're including a valid API key in the request headers and that your account has the necessary permissions to access this endpoint. - `error` (string, required) — Human-readable description of the error that occurred, corresponding to the HTTP status code. ### 500 Internal Server Error An unexpected error occurred on Pinnacle's servers while processing your request. If this error persists, please contact support with the request details. - `error` (string, required) — Human-readable description of the error that occurred, corresponding to the HTTP status code. ## Types ### FormField A single field definition inside a form. Discriminated by `type`. - `type`: `text` (text) - `key` (string, required) — Programmatic key used as the property name in the submission payload. Must start with a letter or underscore; letters, digits, and underscores only. - `label` (string, required) — Text shown above the input. - `help_text` (string, optional) — Small caption rendered under the input to give the submitter extra context. - `max_length` (integer, optional) — Maximum number of characters the submitter may enter. - `min_length` (integer, optional) — Minimum number of characters the submitter must enter. - `pattern` (string, optional) — Regex the value must match. Validated server-side with RE2, so patterns that enable catastrophic backtracking (backreferences, lookarounds) are rejected at form creation. - `placeholder` (string, optional) — Ghost text shown inside the input while it's empty. - `required` (boolean, optional, default: false) — When true, the submitter must provide a value. Default false. - `type`: `textarea` (textarea) - `key` (string, required) — Programmatic key used as the property name in the submission payload. Must start with a letter or underscore; letters, digits, and underscores only. - `label` (string, required) — Text shown above the input. - `help_text` (string, optional) — Small caption rendered under the input to give the submitter extra context. - `max_length` (integer, optional) — Maximum number of characters the submitter may enter. - `min_length` (integer, optional) — Minimum number of characters the submitter must enter. - `pattern` (string, optional) — Regex the value must match. Same RE2 safety check as `TextField.pattern`. - `placeholder` (string, optional) — Ghost text shown inside the input while it's empty. - `required` (boolean, optional, default: false) — When true, the submitter must provide a value. Default false. - `type`: `address` (address) - `key` (string, required) — Programmatic key used as the property name in the submission payload. Must start with a letter or underscore; letters, digits, and underscores only. - `label` (string, required) — Text shown above the input. - `help_text` (string, optional) — Small caption rendered under the input to give the submitter extra context. - `max_length` (integer, optional) — Maximum number of characters in the selected address. - `min_length` (integer, optional) — Minimum number of characters in the selected address. - `pattern` (string, optional) — Regex the value must match. Same RE2 safety check as `TextField.pattern`. Rarely needed for addresses — usually left off. - `placeholder` (string, optional) — Ghost text shown inside the input while it's empty. - `required` (boolean, optional, default: false) — When true, the submitter must provide a value. Default false. - `type`: `email` (email) - `key` (string, required) — Programmatic key used as the property name in the submission payload. Must start with a letter or underscore; letters, digits, and underscores only. - `label` (string, required) — Text shown above the input. - `help_text` (string, optional) — Small caption rendered under the input to give the submitter extra context. - `placeholder` (string, optional) — Ghost text shown inside the input while it's empty. - `required` (boolean, optional, default: false) — When true, the submitter must provide a value. Default false. - `type`: `url` (url) - `key` (string, required) — Programmatic key used as the property name in the submission payload. Must start with a letter or underscore; letters, digits, and underscores only. - `label` (string, required) — Text shown above the input. - `help_text` (string, optional) — Small caption rendered under the input to give the submitter extra context. - `placeholder` (string, optional) — Ghost text shown inside the input while it's empty. - `required` (boolean, optional, default: false) — When true, the submitter must provide a value. Default false. - `type`: `phone` (phone) - `key` (string, required) — Programmatic key used as the property name in the submission payload. Must start with a letter or underscore; letters, digits, and underscores only. - `label` (string, required) — Text shown above the input. - `help_text` (string, optional) — Small caption rendered under the input to give the submitter extra context. - `placeholder` (string, optional) — Ghost text shown inside the input while it's empty. - `required` (boolean, optional, default: false) — When true, the submitter must provide a value. Default false. - `type`: `number` (number) - `key` (string, required) — Programmatic key used as the property name in the submission payload. Must start with a letter or underscore; letters, digits, and underscores only. - `label` (string, required) — Text shown above the input. - `help_text` (string, optional) — Small caption rendered under the input to give the submitter extra context. - `max` (double, optional) — Maximum accepted value. - `min` (double, optional) — Minimum accepted value. - `placeholder` (string, optional) — Ghost text shown inside the input while it's empty. - `required` (boolean, optional, default: false) — When true, the submitter must provide a value. Default false. - `step` (double, optional) — Increment between valid values. For example, `step: 0.5` lets the user enter 1, 1.5, 2, etc. - `type`: `range` (range) - `key` (string, required) — Programmatic key used as the property name in the submission payload. Must start with a letter or underscore; letters, digits, and underscores only. - `label` (string, required) — Text shown above the input. - `help_text` (string, optional) — Small caption rendered under the input to give the submitter extra context. - `max` (double, optional, default: 100) — Upper bound of the slider. - `min` (double, optional, default: 0) — Lower bound of the slider. - `required` (boolean, optional, default: false) — When true, the submitter must provide a value. Default false. - `step` (double, optional) — Increment between adjacent slider stops. - `type`: `rating` (rating) - `key` (string, required) — Programmatic key used as the property name in the submission payload. Must start with a letter or underscore; letters, digits, and underscores only. - `label` (string, required) — Text shown above the input. - `help_text` (string, optional) — Small caption rendered under the input to give the submitter extra context. - `max` (integer, optional, default: 5) — Number of stars rendered. - `min` (integer, optional, default: 0) — Minimum allowed rating. `0` means the submitter can clear their selection. - `required` (boolean, optional, default: false) — When true, the submitter must provide a value. Default false. - `type`: `date` (date) - `key` (string, required) — Programmatic key used as the property name in the submission payload. Must start with a letter or underscore; letters, digits, and underscores only. - `label` (string, required) — Text shown above the input. - `help_text` (string, optional) — Small caption rendered under the input to give the submitter extra context. - `max` (string, optional) — Latest selectable date as `YYYY-MM-DD`. - `min` (string, optional) — Earliest selectable date as `YYYY-MM-DD`. - `placeholder` (string, optional) — Ghost text shown inside the input while it's empty. - `required` (boolean, optional, default: false) — When true, the submitter must provide a value. Default false. - `type`: `time` (time) - `key` (string, required) — Programmatic key used as the property name in the submission payload. Must start with a letter or underscore; letters, digits, and underscores only. - `label` (string, required) — Text shown above the input. - `help_text` (string, optional) — Small caption rendered under the input to give the submitter extra context. - `max` (string, optional) — Latest selectable time as `HH:MM` (24-hour). - `min` (string, optional) — Earliest selectable time as `HH:MM` (24-hour). - `placeholder` (string, optional) — Ghost text shown inside the input while it's empty. - `required` (boolean, optional, default: false) — When true, the submitter must provide a value. Default false. - `type`: `datetime` (datetime) - `key` (string, required) — Programmatic key used as the property name in the submission payload. Must start with a letter or underscore; letters, digits, and underscores only. - `label` (string, required) — Text shown above the input. - `help_text` (string, optional) — Small caption rendered under the input to give the submitter extra context. - `max` (string, optional) — Latest selectable instant as an ISO 8601 timestamp. - `min` (string, optional) — Earliest selectable instant as an ISO 8601 timestamp. - `placeholder` (string, optional) — Ghost text shown inside the input while it's empty. - `required` (boolean, optional, default: false) — When true, the submitter must provide a value. Default false. - `type`: `color` (color) - `key` (string, required) — Programmatic key used as the property name in the submission payload. Must start with a letter or underscore; letters, digits, and underscores only. - `label` (string, required) — Text shown above the input. - `help_text` (string, optional) — Small caption rendered under the input to give the submitter extra context. - `required` (boolean, optional, default: false) — When true, the submitter must provide a value. Default false. - `type`: `select` (select) - `key` (string, required) — Programmatic key used as the property name in the submission payload. Must start with a letter or underscore; letters, digits, and underscores only. - `label` (string, required) — Text shown above the input. - `options` (list of FormFieldOption, required) — Choices shown in the dropdown. At least one is required. - `help_text` (string, optional) — Small caption rendered under the input to give the submitter extra context. - `placeholder` (string, optional) — Placeholder row shown when no option is selected (e.g. "Select…"). - `required` (boolean, optional, default: false) — When true, the submitter must provide a value. Default false. - `type`: `radio` (radio) - `key` (string, required) — Programmatic key used as the property name in the submission payload. Must start with a letter or underscore; letters, digits, and underscores only. - `label` (string, required) — Text shown above the input. - `options` (list of FormFieldOption, required) — Choices shown as radio buttons. At least one is required. - `help_text` (string, optional) — Small caption rendered under the input to give the submitter extra context. - `required` (boolean, optional, default: false) — When true, the submitter must provide a value. Default false. - `type`: `checkbox` (checkbox) - `key` (string, required) — Programmatic key used as the property name in the submission payload. Must start with a letter or underscore; letters, digits, and underscores only. - `label` (string, required) — Text shown above the input. - `help_text` (string, optional) — Small caption rendered under the input to give the submitter extra context. - `max_selected` (integer, optional) — Maximum number of boxes the submitter may check. Extra boxes become uninteractive once the cap is reached. - `min_selected` (integer, optional) — Minimum number of boxes the submitter must check. - `options` (list of FormFieldOption, optional) — Choices shown as checkboxes. Omit to render a single boolean checkbox. - `required` (boolean, optional, default: false) — When true, the submitter must provide a value. Default false. ### FormThemeOverride Sparse override layered on top of your team's theme. Every field is optional; provided fields replace the corresponding team defaults at render time. URL fields must use `https://`. - `logo_url` (string, optional, nullable) — Logo displayed at the top of the form. Square images render as an inline mark; wide landscape images render as a full-width banner. - `favicon_url` (string, optional, nullable) — Favicon shown in the browser tab when the form page loads. - `theme_mode` (enum, optional) — Rendering mode. `light` forces the light palette, `dark` forces dark, and `auto` follows the respondent's OS-level appearance setting. - Allowed values: `light`, `dark`, `auto` - `colors` (FormThemeOverrideColors, optional) — Per-mode palette overrides. Each mode is an object with optional `primary`, `background`, and `text` hex colors. Omit a mode to inherit the team default for it. - `font_family` (enum, optional) — Typeface stack. `system` uses the respondent's OS default sans-serif. - Allowed values: `inter`, `system`, `serif`, `mono`, `rounded` - `corner_radius` (enum, optional) — Border-radius preset for cards, inputs, and buttons. - Allowed values: `sharp`, `rounded`, `pill` - `content_alignment` (enum, optional) — Horizontal alignment for the form header (title, description, logo). - Allowed values: `left`, `center`, `right` - `background` (FormBackground, optional) — Background layer for the page. Choose solid, gradient, image, or a built-in pattern. - `submit_button_label` (string, optional) — Text shown on the submit button. Defaults to `Submit`. - `success_message` (string, optional) — Message shown on the success panel after a successful submit (unless `redirect_url` is set, in which case the respondent is redirected). - `redirect_url` (string, optional, nullable) — When set, the submitter is redirected here after a successful submit instead of seeing the success panel. - `privacy_url` (string, optional, nullable) — Link target for the `Privacy` footer link. Omit or set to null to hide the link. - `terms_url` (string, optional, nullable) — Link target for the `Terms` footer link. Omit or set to null to hide the link. - `og_image_url` (string, optional, nullable) — Image shown when the form URL is shared in social / messaging link previews (Open Graph). - `og_description` (string, optional, nullable) — Open Graph description used in link previews. Falls back to the form's `description` when unset. ### ZodErrorErrors Structured dictionary of issues containing two main sections: - `errors`: Array of global validation errors not tied to specific fields - `properties`: Object mapping field names to their specific validation errors, where each field contains an `errors` array ### FormFieldOption A single choice inside a `select`, `radio`, or `checkbox` field. - `value` (string, required) — Machine value sent back in the submission payload. - `label` (string, required) — Human label rendered in the form. ### FormThemeOverrideColors Per-mode palette overrides. Each mode is an object with optional `primary`, `background`, and `text` hex colors. Omit a mode to inherit the team default for it. - `light` (FormColorPalette, optional) — Palette used when the form renders in light mode. - `dark` (FormColorPalette, optional) — Palette used when the form renders in dark mode. ### FormBackground Background layer for the form page. Discriminated by `type`. - `type`: `solid` (solid) - `type`: `gradient` (gradient) - `dark` (FormGradient, required) — Gradient used when the form renders in dark mode. - `light` (FormGradient, required) — Gradient used when the form renders in light mode. - `type`: `image` (image) - `blur` (boolean, required) — When true, blurs the background image. - `tint_opacity` (integer, required) — Darkening overlay intensity from 0 (no overlay) to 80 (heavy). Helps preserve text contrast over busy images. - `url` (string, required) — HTTPS URL of the background image. Must start with `https://`. - `zoom` (integer, required) — Scale percentage. 100 is native size; higher values crop the image tighter. - `type`: `pattern` (pattern) - `preset` (enum, required) — Which built-in pattern to render. - Allowed values: `noise`, `dots`, `mesh`, `grid`, `waves`, `topo` ### FormColorPalette Per-mode palette. All three colors are optional; any you omit fall back to the team default for that mode. - `primary` (string, optional) — Accent color used for the submit button, focus rings, filled checkboxes, and other interactive surfaces. - `background` (string, optional) — Page background color. Fallback solid color used by Safari chrome tinting and safe-area regions. - `text` (string, optional) — Body text and label color. ### FormGradient A single light- or dark-mode gradient stop pair. - `from` (string, required) — Starting color of the gradient. - `to` (string, required) — Ending color of the gradient. - `angle` (enum, required) — Direction the gradient runs. `top-bottom` is vertical; `diagonal` runs top-left → bottom-right. - Allowed values: `top-bottom`, `diagonal` ## Examples **Request** ```json { "fields": [ { "type": "text", "key": "full_name", "label": "Full name", "max_length": 60, "min_length": 2, "placeholder": "Ada Lovelace", "required": true } ] } ``` **Response** ```json { "id": "form_abc123", "url": "https://forms.pinnacle.sh/form_abc123", "name": "Contact request", "description": null, "fields": [ { "type": "text", "key": "full_name", "label": "Full name", "min_length": 2, "required": true }, { "type": "email", "key": "email", "label": "Email", "required": true } ], "can_update": false, "expires_at": null, "theme_override": null, "submission_count": 0, "last_submitted_at": null, "archived_at": null, "created_at": "2026-04-24T00:00:00Z", "updated_at": "2026-04-24T00:00:00Z" } ``` **SDK Code** ```python Created Form import requests url = "https://api.pinnacle.sh/forms" payload = { "fields": [ { "type": "text", "key": "full_name", "label": "Full name", "max_length": 60, "min_length": 2, "placeholder": "Ada Lovelace", "required": True } ] } headers = { "PINNACLE-API-KEY": "", "Content-Type": "application/json" } response = requests.post(url, json=payload, headers=headers) print(response.json()) ``` ```javascript Created Form const url = 'https://api.pinnacle.sh/forms'; const options = { method: 'POST', headers: {'PINNACLE-API-KEY': '', 'Content-Type': 'application/json'}, body: '{"fields":[{"type":"text","key":"full_name","label":"Full name","max_length":60,"min_length":2,"placeholder":"Ada Lovelace","required":true}]}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ```go Created Form package main import ( "fmt" "strings" "net/http" "io" ) func main() { url := "https://api.pinnacle.sh/forms" payload := strings.NewReader("{\n \"fields\": [\n {\n \"type\": \"text\",\n \"key\": \"full_name\",\n \"label\": \"Full name\",\n \"max_length\": 60,\n \"min_length\": 2,\n \"placeholder\": \"Ada Lovelace\",\n \"required\": true\n }\n ]\n}") req, _ := http.NewRequest("POST", url, payload) req.Header.Add("PINNACLE-API-KEY", "") req.Header.Add("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(res) fmt.Println(string(body)) } ``` ```ruby Created Form require 'uri' require 'net/http' url = URI("https://api.pinnacle.sh/forms") http = Net::HTTP.new(url.host, url.port) http.use_ssl = true request = Net::HTTP::Post.new(url) request["PINNACLE-API-KEY"] = '' request["Content-Type"] = 'application/json' request.body = "{\n \"fields\": [\n {\n \"type\": \"text\",\n \"key\": \"full_name\",\n \"label\": \"Full name\",\n \"max_length\": 60,\n \"min_length\": 2,\n \"placeholder\": \"Ada Lovelace\",\n \"required\": true\n }\n ]\n}" response = http.request(request) puts response.read_body ``` ```java Created Form import com.mashape.unirest.http.HttpResponse; import com.mashape.unirest.http.Unirest; HttpResponse response = Unirest.post("https://api.pinnacle.sh/forms") .header("PINNACLE-API-KEY", "") .header("Content-Type", "application/json") .body("{\n \"fields\": [\n {\n \"type\": \"text\",\n \"key\": \"full_name\",\n \"label\": \"Full name\",\n \"max_length\": 60,\n \"min_length\": 2,\n \"placeholder\": \"Ada Lovelace\",\n \"required\": true\n }\n ]\n}") .asString(); ``` ```php Created Form request('POST', 'https://api.pinnacle.sh/forms', [ 'body' => '{ "fields": [ { "type": "text", "key": "full_name", "label": "Full name", "max_length": 60, "min_length": 2, "placeholder": "Ada Lovelace", "required": true } ] }', 'headers' => [ 'Content-Type' => 'application/json', 'PINNACLE-API-KEY' => '', ], ]); echo $response->getBody(); ``` ```csharp Created Form using RestSharp; var client = new RestClient("https://api.pinnacle.sh/forms"); var request = new RestRequest(Method.POST); request.AddHeader("PINNACLE-API-KEY", ""); request.AddHeader("Content-Type", "application/json"); request.AddParameter("application/json", "{\n \"fields\": [\n {\n \"type\": \"text\",\n \"key\": \"full_name\",\n \"label\": \"Full name\",\n \"max_length\": 60,\n \"min_length\": 2,\n \"placeholder\": \"Ada Lovelace\",\n \"required\": true\n }\n ]\n}", ParameterType.RequestBody); IRestResponse response = client.Execute(request); ``` ```swift Created Form import Foundation let headers = [ "PINNACLE-API-KEY": "", "Content-Type": "application/json" ] let parameters = ["fields": [ [ "type": "text", "key": "full_name", "label": "Full name", "max_length": 60, "min_length": 2, "placeholder": "Ada Lovelace", "required": true ] ]] as [String : Any] let postData = JSONSerialization.data(withJSONObject: parameters, options: []) let request = NSMutableURLRequest(url: NSURL(string: "https://api.pinnacle.sh/forms")! as URL, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10.0) request.httpMethod = "POST" request.allHTTPHeaderFields = headers request.httpBody = postData as Data let session = URLSession.shared let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in if (error != nil) { print(error as Any) } else { let httpResponse = response as? HTTPURLResponse print(httpResponse) } }) dataTask.resume() ``` > One API for RCS, iMessage, MMS, and SMS. Build, test, and scale every channel — send your first message in minutes, not weeks.