Assistants
Manage forms
Define structured forms your assistant fills in during a conversation and delivers to email or SMS recipients.
A capture form is a structured set of questions attached to an assistant. Each form has a trigger describing when it applies, an introMessage the assistant uses to start the form, an ordered list of typed fields to collect, and one or more recipients (email and/or SMS) who receive the captured answers.
Typical use: a callback-request form ("caller wants a call back") that collects name, phone, and reason, then emails the details to your front desk.
| Method | Path | Purpose |
|---|---|---|
| GET | /capture-forms | List capture forms (optionally by assistant) |
| GET | /capture-forms/{id} | Get one capture form |
| POST | /capture-forms | Create a capture form |
| PUT | /capture-forms/{id} | Replace a capture form |
| PATCH | /capture-forms/{id} | Partially update a capture form |
| DELETE | /capture-forms/{id} | Delete a capture form |
Form structure#
| Field | Type | Required | Description |
|---|---|---|---|
assistantId | UUID | Yes | The assistant this form belongs to (ownership is validated) |
name | string | Yes | Form name |
trigger | string | Yes | When the assistant should run this form |
introMessage | string | Yes | What the assistant says when starting the form |
emailRecipients | string[] (emails) | See note | Who receives the results by email |
smsRecipients | string[] (phone numbers) | See note | Who receives the results by SMS |
emailSubjectTemplate | string | null | No | Your own subject line for the results email. Plain text. Unset means the standard subject; see Email and SMS templates |
emailBodyTemplate | string | null | No | Your own HTML body for the results email. Unset means the standard email |
smsBodyTemplate | string | null | No | Your own body for the results text message. Plain text. Unset means the standard message |
fields | field[] | Yes (min 1) | The questions to collect (see below) |
properties | object | No | Free-form metadata, defaults to {} |
isActive | boolean | No | Whether the form is active |
At least one recipient
On create (and full replace), emailRecipients and smsRecipients together must contain at least one entry. SMS recipients must be mobile numbers in international format, with + and the country code (for example +61412345678). Landlines, toll-free numbers and other numbers that can't receive texts are rejected. Accepted numbers are stored in E.164 form, so +61 412 345 678 is saved as +61412345678. On PATCH the rule is enforced whenever you touch either recipients field.
Fields#
Each entry in fields:
| Field | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Unique per form, lowercase snake_case (e.g. contact_email) |
question | string | Yes | What the assistant asks |
type | string | number | Yes | Expected answer type |
required | boolean | No | Whether the answer is mandatory |
action | string | No | Extra action hint for this field |
wait_for_response | boolean | No | Whether to pause for the answer |
prompt | string | No | Guidance injected into the system prompt to steer how the AI asks this field. Supports {question} (replaced with this field's question) and {{property}} client-parameter placeholders. |
Duplicate key values across fields are rejected with a validation error.
Email and SMS templates#
When a form is submitted, OnCore delivers the answers to the form's recipients using its standard email and text layouts. emailSubjectTemplate, emailBodyTemplate and smsBodyTemplate replace those layouts with wording and branding of your own.
The three are independent and each is optional. A form that sets only emailBodyTemplate keeps the standard subject line and the standard text message. A form that sets none of them keeps delivering exactly what it delivers today.
Send null to clear a template and go back to the standard layout. An empty string is rejected, because it would deliver an empty email or message. Remember that PUT replaces the whole form: a template left out of a PUT body is cleared, while PATCH only changes what you send.
emailSubjectTemplate and smsBodyTemplate are plain text. emailBodyTemplate is a complete HTML email document, not a fragment: it replaces the standard email outright, so write your own <html> and <body> and carry your branding in inline styles, the way email clients expect.
The sender address the email arrives from is not configurable through the API. Templates change what the message says, not who it comes from.
Placeholders are field keys#
A template is ordinary text with {{placeholder}} tokens in it, each replaced with a value from the submission before the email or text is sent.
A placeholder is a field's key, not its question. A field asking "What is your first name?" with key: "first_name" is written {{first_name}}. Every key in the form's fields is available in all three templates. Names are case-sensitive, and inner spaces are ignored, so {{ first_name }} and {{first_name}} are the same token.
{{property}} placeholders are a different thing
The {{...}} tokens you can write in trigger, introMessage, question, action and prompt are client parameters, filled from the form's properties before the assistant is prompted. (prompt also takes single-brace {question}, which belongs to that field alone.) The tokens in these three templates are filled from the answers after the form is submitted. The two read from different sources.
A skipped answer removes the value, not the words around it
An optional field the caller never answered renders as nothing, and the wording you wrote around it stays. "... would like a call back about: {{reason}}" becomes "... would like a call back about: " when reason was skipped. There is no conditional syntax, so keep optional fields where an empty value still reads as a sentence, or lean on {{details_block}} in emailBodyTemplate and {{details_text}} in smsBodyTemplate, which list only the answers that were actually collected.
Built-in placeholders#
Every field key on the form works in all three templates. The built-ins do not, because the three templates render differently. emailBodyTemplate is HTML, while emailSubjectTemplate and smsBodyTemplate are plain text, so each built-in is allowed only where it can render.
| Placeholder | emailBodyTemplate | emailSubjectTemplate | smsBodyTemplate | Contains |
|---|---|---|---|---|
{{form_name}} | Yes | Yes | Yes | The form's name |
{{assistant_name}} | Yes | Yes | Yes | The name of the assistant the form belongs to |
{{call_id}} | Yes | Yes | Yes | Identifier of the call or conversation the answers came from |
{{heading}} | Yes | Yes | No | The heading line the standard email uses |
{{details_block}} | Yes | No | No | HTML table of every collected answer, the one the standard email shows. Each row is labelled from the field's key, never from its question |
{{logo_img_html}} | Yes | No | No | HTML img tag for the platform logo, 96px and centred. The standard email shows a smaller copy in its top-left corner |
{{details_text}} | No | No | Yes | Plain-text list of every collected answer, the one the standard message shows. One key: value line per answer, using the field key exactly as you wrote it |
{{form_name}}, {{assistant_name}} and {{call_id}} are plain text and carry no layout, so every template takes them. {{heading}} is plain text too and reads fine as a subject line, but it is the email's heading and has no place in a text message. {{details_block}} and {{logo_img_html}} are pre-rendered HTML, and emailBodyTemplate is the only one of the three that is HTML. In a subject line or a text message the recipient would get the raw markup instead of the table or the logo, so neither template accepts them. {{details_text}} is the plain-text answer list the standard message uses, and stays with smsBodyTemplate. Using a built-in in a template that does not accept it is rejected when you save the form, exactly like an unknown placeholder.
{{logo_img_html}} renders the logo the standard email uses, which is OnCore's rather than your client's. To show your own, leave it out and write your own img tag in emailBodyTemplate pointing at a publicly reachable image.
Images must already be hosted#
emailBodyTemplate is HTML, so it can carry a logo, a header banner, a signature image, anything an <img> tag can show. What it cannot carry is the image itself. Every image has to already be hosted somewhere publicly reachable over HTTPS, and referenced by absolute URL, as in <img src="https://cdn.example.com/logo.png" alt="Example Motors" height="40" />.
There is nowhere to upload an image to OnCore, and the delivered email has no attachments: the HTML you write is the whole message. Host the file on your own site, CDN, or object storage, and keep it at a stable URL for as long as a template points at it. A relative path, or a URL only reachable from inside your network, arrives as a broken image.
Write the template so it still reads correctly with every image missing. Mail clients commonly hold remote images back until the recipient chooses to display them, so the facts a recipient needs belong in the text rather than in a picture, and every <img> is worth an alt attribute for the version of the email that arrives before the images do.
Answers are escaped in the HTML email#
emailBodyTemplate is HTML and its values come from what a caller said, so every value except {{details_block}} and {{logo_img_html}} is escaped as it is substituted. A caller who says "Smith & Sons" reaches the inbox as that text rather than as markup. Those two are the exceptions: OnCore builds them as HTML and inserts them unchanged.
Escaping applies to the values, never to the template. The HTML you write in emailBodyTemplate is used exactly as you wrote it.
An unknown placeholder is rejected on save#
On create, replace and update, every token in the three templates is checked against the form's field keys and the built-ins that template accepts. A token that is on neither list returns 400 VALIDATION_ERROR naming the offending token (see Errors), so {{fist_name}} fails at the API call rather than arriving as a gap in an email. The message also lists the names that would have worked, so the fix is usually visible in the response.
A template and its fields therefore move together. Renaming a field's key while a template still refers to the old one is rejected, so send both changes in the same request.
The check only sees tokens that match the placeholder grammar. {{first_name}} is a token; {{What is your name}} is not, because of the inner spaces, so it is neither substituted nor rejected and the braces are delivered as written. A placeholder that survives a save but arrives verbatim in the inbox is almost always this.
On a PATCH the check runs against the form as it will be once the patch lands. A template sent on its own is checked against the form's stored field keys, and fields sent on its own is re-checked against the stored templates, so a rename cannot quietly orphan a placeholder. A patch that touches neither leaves both alone.
Each template is capped at 100,000 bytes. That is a safety limit, not a target. On SMS it is far above anything you should send: a text message is delivered as written, {{details_text}} grows with every field on the form, and long messages are split into several billed segments. Keep smsBodyTemplate to the few facts the recipient needs.
Example#
The client's callback form already collects first_name and reason. This adds branded delivery to it without touching the questions:
curl -X PATCH https://core-api.heysadie.ai/capture-forms/CAPTURE_FORM_ID \
-H "Authorization: ApiKey YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"assistantId": "ASSISTANT_ID",
"emailSubjectTemplate": "Callback for {{first_name}}",
"emailBodyTemplate": "<!doctype html><html><body style=\"font-family:Arial,sans-serif;color:#111\"><img src=\"https://example.com/logo.png\" alt=\"Example Motors\" height=\"40\" /><h2>{{form_name}}</h2><p><strong>{{first_name}}</strong> would like a call back about: {{reason}}</p>{{details_block}}</body></html>",
"smsBodyTemplate": "Callback: {{first_name}} ({{reason}}). Ref {{call_id}}."
}'Do not add fields to this call
Sending fields on a PATCH replaces the whole field list. A branding update should leave fields out, or the questions not repeated in the request are lost.
A caller who gives "Dana" and "a quote for a new boiler" produces the subject Callback for Dana and a body reading Dana would like a call back about: a quote for a new boiler, above the same answers table the standard email uses.
Create a capture form#
POST /capture-forms
curl -X POST https://core-api.heysadie.ai/capture-forms \
-H "Authorization: ApiKey YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"assistantId": "ASSISTANT_ID",
"name": "Callback request",
"trigger": "The caller asks for a call back from a human.",
"introMessage": "Sure, I just need a few details so the team can call you back.",
"emailRecipients": ["[email protected]"],
"fields": [
{
"key": "caller_name",
"question": "What is your full name?",
"type": "string",
"required": true
},
{
"key": "callback_number",
"question": "What is the best number to reach you on?",
"type": "string",
"required": true
},
{
"key": "reason",
"question": "What is the call back regarding?",
"type": "string"
}
],
"isActive": true
}'List capture forms#
GET /capture-forms
| Query param | Type | Required | Description |
|---|---|---|---|
limit | number | No | Page size. Default 50, maximum 500. |
offset | number | No | Items to skip. Default 0. |
assistantId | UUID | No | Only forms for this assistant |
curl "https://core-api.heysadie.ai/capture-forms?assistantId=ASSISTANT_ID" \
-H "Authorization: ApiKey YOUR_API_KEY"Update a capture form#
Two options:
PUT /capture-forms/{id}: full replace; the body is the same as create (all required fields again, at least one recipient).PATCH /capture-forms/{id}: partial update;assistantIdis still required in the body, everything else is optional. Sendingfieldsreplaces the whole field list (min 1 entry).
curl -X PATCH https://core-api.heysadie.ai/capture-forms/CAPTURE_FORM_ID \
-H "Authorization: ApiKey YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "assistantId": "ASSISTANT_ID", "isActive": false }'Delete a capture form#
curl -X DELETE https://core-api.heysadie.ai/capture-forms/CAPTURE_FORM_ID \
-H "Authorization: ApiKey YOUR_API_KEY"Build it into your own product#
Most forms are not created from a terminal. They are created from whatever screen your own application gives a customer, and the endpoints on this page are what sits behind that screen.
The tenant pattern is the same one the rest of the API uses: one organization key held server-side, the customer selected per request with X-Tenant-Id. Reseller quickstart has the client helper and the isolation rules. Every route here is scoped through the assistant the form belongs to, so nothing belonging to another customer is reachable. Handle two refusals rather than one. Reading or deleting a form that belongs to someone else returns 404 RESOURCE_NOT_FOUND, while creating, replacing or updating one with another customer's assistantId returns 403 FORBIDDEN. Code that branches on 404 alone will mishandle the write paths. A form hangs off an assistant, so the screen you build for it belongs wherever you already show that customer's assistant, and the form ID is one more OnCore ID to store beside your own record.
If you are building the editor yourself, three properties of the API do most of the work:
- The placeholder list is derivable, so you never hardcode it. The valid tokens for a slot are that form's own
fields[].keyvalues plus the built-ins the slot accepts, and the per-slot rules are in Built-in placeholders. Read the form, read that table, and you can offer the exact set. - Validation errors are written to be shown to a person. A rejected placeholder returns
400 VALIDATION_ERRORnaming the token that failed and listing the names that would have worked. Put that text in front of whoever is editing rather than a generic save failure, and the typo is usually fixed without leaving the screen. - A key rename and the templates that use it travel together. Renaming a field's
keywhile a template still refers to the old one is rejected, so send both changes in one request.
The dashboard editor is the alternative for people who sign in to OnCore themselves. If your customers only ever see your product, the API is the whole surface.
Next steps#
- Assistants: the assistant a form belongs to.
- Custom instructions: lighter-weight behavior tweaks that don't collect data.
New to this? Capture forms explains what it does and when to use it, without the API detail.