Skip to content
Pathbound DOCS

Forms

A form on your own site can post straight to Pathbound. Each submission creates or updates a contact (and a company, if you send one), is stored, and appears on the contact’s timeline. You can then have Pathbound email everyone who submits a given form, with a template from your own Resend account.

A form needs no registration. You choose an id, put it in the URL the form posts to, and the form exists from its first submission.

This page covers:

  • the public endpoint a form posts to, which takes no API key;
  • GET /v1/forms, which returns where to post and which forms have received submissions;
  • how to email everyone who submits a form.
POST /v1/public/form-submission/:tenant_id/:form_id

This endpoint takes no API key, because a browser calls it. Instead, the request must come from a page on one of your workspace’s verified domains. See Where the request may come from.

Your form’s own code calls this endpoint. The events snippet does not: its form_submit event only records that a form was submitted on a page, without the field values. See form_submit and form_submission.

ParameterTypeDescription
tenant_idstringYour workspace’s tenant ID. GET /v1/forms returns the full URL with it filled in. It is also user.tenant_id in GET /v1/auth/status.
form_idstringAn id you choose for the form: 1 to 64 letters, digits, _, ., : or -, starting with a letter or a digit. For example playbook or demo-request.

Use an id that fits that rule. The endpoint still accepts any other non-empty id and stores the contact, but such a submission writes no form_submission event, the form is not listed by GET /v1/forms, and it cannot have an email automation.

Send JSON (Content-Type: application/json).

FieldTypeRequiredDescription
contact_dataobjectone of the twoThe person. email is required when this object is sent. Every other key (firstname, lastname, jobtitle, phone, and so on) is stored as a contact property.
company_dataobjectone of the twoThe company. domain is required when this object is sent. Every other key is stored as a company property.
visitor_idstringnoThe value of the pathbound_visitor_id cookie that the events snippet sets. It ties the person’s earlier page views to the contact.

At least one of contact_data and company_data must be present.

lifecyclestage (in either object, if set) must be one of: lead, prospect, mql, sql, opportunity, customer, evangelist, subscriber, other.

An email automation emails the address in contact_data.email, so a submission without contact_data emails nobody. It reads contact_data.firstname for the template’s first_name variable.

The request’s Origin header must name a host on one of your workspace’s verified domains: the domain itself or any subdomain of it. When there is no Origin header, Referer is read instead. A browser sets these headers itself, so a form on your site needs nothing extra. You verify domains under Settings → Domains in the dashboard.

A request is refused with 403 when it comes from any other site, from a host listed under your excluded hostnames, or with neither header. A call from your own server usually has neither.

The endpoint answers CORS preflight requests, so a fetch() with a JSON body works from the browser.

const cookie = document.cookie.match(/(?:^|; )pathbound_visitor_id=([^;]*)/);
await fetch('https://api.pathbound.ai/v1/public/form-submission/YOUR_TENANT_ID/playbook', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contact_data: { email: '[email protected]', firstname: 'Ada' },
company_data: { domain: 'example.com' },
visitor_id: cookie ? decodeURIComponent(cookie[1]) : undefined,
}),
});
<form id="playbook-form">
<input type="email" name="email" placeholder="Work email" required />
<input type="text" name="firstname" placeholder="First name" />
<button type="submit">Send me the playbook</button>
</form>
<script>
document.getElementById('playbook-form').addEventListener('submit', async (event) => {
event.preventDefault();
const fields = event.target.elements;
const cookie = document.cookie.match(/(?:^|; )pathbound_visitor_id=([^;]*)/);
const response = await fetch(
'https://api.pathbound.ai/v1/public/form-submission/YOUR_TENANT_ID/playbook',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contact_data: {
email: fields.email.value,
firstname: fields.firstname.value || undefined,
},
visitor_id: cookie ? decodeURIComponent(cookie[1]) : undefined,
}),
},
);
if (response.ok) event.target.replaceWith('Thanks. Check your inbox.');
});
</script>

After wiring a form, submit it once from the live page and open Forms in the dashboard, or call GET /v1/forms: the form id appears with submissions_last_30_days of at least 1. If it does not, the page’s domain is probably not verified.

{
"status": "success",
"message": "Thank you for your submission"
}

The endpoint never returns the contact or any id, so an anonymous caller learns nothing about your workspace.

StatusMeaning
400The body is not usable: neither contact_data nor company_data, a contact_data without a valid email, a company_data without domain, or an unknown lifecyclestage.
403The Origin or Referer is missing, is not on a verified domain of this workspace, or is an excluded hostname.
429A rate limit was reached.
500The submission could not be processed.

An error body carries "status": "error" and a short, general text (message, or error for a 429). It does not say which field was wrong.

30 submissions a minute per address a request arrives from, and 100 a minute per workspace. A request over either limit is answered with 429.

One accepted submission does four things:

  1. A company, when company_data is sent. It is found by domain or created, and the other keys are merged into its properties.
  2. A contact, when contact_data is sent. It is found by email or created, and the other keys are merged into its properties. The contact is linked to the company from company_data. Without one, a contact that has no company yet is linked to an existing company whose domain matches its email domain (never for a free-mail address). When visitor_id is sent, that visitor is tied to the contact, so the page views recorded before the submission belong to the contact’s timeline.
  3. A stored submission: what was submitted, with the host of the page it came from, the Referer, the user agent and the address the request arrived from. This is what GET /v1/forms counts, and it is what triggers an email automation. The dashboard’s Forms page lists each form’s submissions with what was submitted and the host of the page; it never shows the Referer, the user agent or the address.
  4. A form_submission event on the contact’s timeline. It carries the form id and the ids of the records above, never the submitted values. It is delivered to HTTPS event streams like any other event.

The contact and company changes also reach your contact and company streams, with source: "form".

GET /v1/forms

Returns where a form on your site has to post, and the forms that have received submissions. Requires events:read.

{
"status": "success",
"endpoint": {
"method": "POST",
"url": "https://api.pathbound.ai/v1/public/form-submission/tenant_xyz/{form_id}",
"form_id": "Any id you choose for the form, in the URL: 1 to 64 letters, digits, …",
"content_type": "application/json",
"body": {
"contact_data": "Object. `email` is required; any other key …",
"company_data": "Optional object. `domain` is required when it is sent; …",
"visitor_id": "Optional. The Pathbound events snippet's visitor id, …"
},
"allowed_from": "A browser page on one of the workspace's verified domains …",
"response": "200 {\"status\":\"success\"} when stored. It never returns the contact or any id.",
"rate_limits": "30 submissions a minute per address a request arrives from and 100 a minute per workspace."
},
"verified_domains": ["example.com"],
"example": "fetch(\"https://api.pathbound.ai/v1/public/form-submission/tenant_xyz/playbook\", {\n method: \"POST\", …\n});",
"check": "After wiring the form, submit it once from the live page, then call this again: …",
"docs": "https://pathbound.ai/docs/api/forms",
"forms": [
{
"form_id": "playbook",
"submissions_last_30_days": 12,
"last_submitted_at": "2026-10-07T09:14:02.000Z",
"email_automation_id": "6703f1c2a4b5c6d7e8f90123"
}
],
"timestamp": "2026-10-07T12:00:00.000Z"
}
FieldDescription
endpoint.urlThe URL to post to, with your tenant ID filled in. Replace {form_id} with the form’s id.
endpoint.*The same rules as Submit a Form, written as text for an AI agent to read.
verified_domainsThe domains a form may post from.
exampleA fetch() call for a form with the id playbook.
formsForms that have received submissions. Only ids that fit the form id rule are listed: every form that has an email automation first, then up to 50 others, busiest first.
forms[].submissions_last_30_daysSubmissions stored in the last 30 days. A form that has an email automation and no recent submissions is listed with 0. On a workspace with a very large number of submissions the counts can be up to a minute old.
forms[].last_submitted_atWhen the most recent of those arrived.
forms[].email_automation_idThe stream id of the form’s email automation, when it has one.
forms[].event_automation_idThe stream id of the form’s Resend event automation, when it has one. A form has one or the other.

The MCP tool get_form_setup returns the same thing to an AI agent.

An email automation sends one email to each person who submits one form, through your own Resend account, a few seconds after the submission. The email is a template you write and publish in Resend. Pathbound does not hold the email and has no editor for it: an automation only says which template a form sends. You set it up once, over the API or by asking an AI agent connected through the MCP server. Nobody approves each email, so read what is emailed, and what is not before you turn one on.

End to end:

  1. Wire the form. Point it at the public endpoint as described above, with at least contact_data.email. Submit it once and check that GET /v1/forms lists it.

  2. Prepare Resend. Connect Resend with an API key that has full access, verify a sending domain there, and have a workspace admin turn on Send email for Resend under MCP Tools. To set the automation up through an AI agent, also turn on Manage streams.

  3. Prepare the template in Resend. See below.

  4. Create the automation. It is a stream with destination_type: "resend_send_email" attached to the form id and the template. The API key needs both streams:write and actions:write.

    Terminal window
    curl -X POST https://api.pathbound.ai/v1/streams \
    -H "Authorization: Bearer $PATHBOUND_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
    "destination_type": "resend_send_email",
    "name": "Playbook email",
    "destination_config": {
    "form_id": "playbook",
    "template_id": "playbook-email"
    }
    }'

    template_id is the template’s id in Resend, or its alias. The response shows the template’s sender and subject as Resend holds them (stream.template), so you can check that it is the email you mean. Add "validate_only": true to see that without creating anything.

  5. Send yourself the test email. POST /v1/streams/:id/test-email sends the template once to the address of your own Pathbound account. It is the only way to see the email from Pathbound.

  1. Create a template in Resend, under Templates, and write the email there.
  2. Give it a subject and a from address. The from address must be on a domain you have verified for sending in that Resend account. Pathbound sends the template with no sender and no subject of its own, so the template has to carry both.
  3. Give every variable a fallback value. Pathbound fills at most one variable (the next step), so every other one is sent as its fallback.
  4. To greet people by name, add a variable with the key first_name, of type string, with a fallback such as there. In Resend’s editor it is written {{{first_name}}}. Pathbound fills it with the person’s first name, and only when that name is a single word of letters, hyphens and apostrophes, at most 25 characters. Anything else is sent as the fallback.
  5. Publish it. A template that is not published cannot be used.

If one of these is missing, creating the automation is refused with 422 STREAM_EMAIL_TEMPLATE_NOT_USABLE and a message that says what to change in Resend.

To change the email later, edit the template in Resend. Emails sent from then on are the template as Resend holds it, and Pathbound notifies nobody about that change. If the template is unpublished or deleted, or a variable is added without a fallback, the automation’s emails wait for up to a day, the workspace’s admins are told, and the emails are sent once the template is fixed. See When the template changes in Resend.

  • Each submission to that form is emailed once. The same address is emailed at most once per 24 hours per form, even if the person submits again. You can change the interval.
  • The sender, subject, reply-to and content are the template’s. None of them can be set through Pathbound, and Pathbound adds no cc and no bcc.
  • An automation sends at most 50 emails a day unless you set another cap, up to 200. A submission that arrives after a limit is reached is not emailed later.
  • Nothing submitted before the automation was created, or while it was paused, is emailed.
  • Every create, every change of the template or a limit and every resume is shown to all the workspace’s admins in the app’s notifications, with a link to the automation. No email is sent to them.
  • Sending does not count against your monthly meter. A workspace without an active plan sends nothing.

Email automations are being turned on one workspace at a time. Until yours has them, creating one is answered with 412 STREAM_SEND_NOT_AVAILABLE. If you get that answer and want them, email [email protected].

The full reference, with every field, limit and error code, is Email automations on the Streaming page.