Skip to content
Pathbound DOCS

Streaming webhooks

Streaming is Pathbound’s third way to get your data, alongside the REST API and the MCP server:

  • REST API — pull a snapshot when you ask for it.
  • MCP — let an AI agent read your data as tools.
  • Streaming — Pathbound pushes changes to your endpoint as they happen.

Use it to keep an external system in sync: notify your service when a high-intent visitor identifies, push form submissions to your warehouse, or mirror contacts and companies into your own database as integrations enrich them.

Streams are managed in the dashboard. Their trigger — conditions, filters, change type, on/off — can also be managed over the REST API and by an AI agent through the MCP server. Their destination cannot: see below.

A stream can also act instead of posting: add a tag, change Resend segment membership, or send a Resend template to the person who submitted a form, or one of your Resend events for them. Those are automations, email automations and Resend event automations.

Each subscription targets one data type:

Data typeChange typesFires when
eventa tracked event name, or *the events snippet, an integration or the public form endpoint records an event
contactcreated, updated, deleted, or *a unified contact is created, changed, or removed
companycreated, updated, deleted, or *a unified company is created, changed, or removed

Contact and company changes capture every source — manual edits, the REST API, form submissions, and (the common case) integration syncs that create or enrich records pulled from HubSpot, Apollo, and the rest.

In the dashboard, open Events → Streaming → New stream, then:

  1. Pick the data type (events, contacts, or companies).
  2. Pick the change type — for events, the event name or *; for contacts/companies, created / updated / deleted / *.
  3. Set the destination URL. Must be https:// and publicly reachable — internal IP ranges are rejected.
  4. Optionally add filters (see below).
  5. Optionally add custom headers (encrypted at rest) and a signing secret.
  6. Save.

Pathbound sends a POST with a JSON body and these headers:

Content-Type: application/json
User-Agent: Pathbound-Webhooks/1.0
X-Pathbound-Signature: sha256=<hex> (only if a signing secret is set)

Plus any custom headers you configured.

{
"id": "evt_xyz",
"event": "page_view",
"timestamp": "2026-06-08T12:00:00.000Z",
"data": { "title": "Pricing", "path": "/pricing" },
"url": "https://example.com/pricing",
"domain": "example.com",
"visitor_id": "vis_123",
"contact_id": "665f1c2ab3d4e5f601234567",
"platform": "web",
"os": "Mac OS X",
"device_type": "Desktop"
}
  • platform is web, ios or android: a browser, or a native app’s webview. Together with os and device_type (Desktop / Mobile / Tablet) it lets a receiver tell a tap in your mobile app from a click in a desktop browser without parsing data.userAgent. See Server-added fields for how each is derived.
  • contact_id is absent for a visitor who isn’t linked to a contact. platform, os and device_type are absent on an event that did not come from the snippet.
{
"id": "ec_abc123",
"type": "contact.updated",
"resource": "contact",
"action": "updated",
"timestamp": "2026-06-08T12:00:00.000Z",
"source": "hubspot",
"object": {
"contact_id": "665f1c2ab3d4e5f601234567",
"properties": { "email": "[email protected]", "jobtitle": "VP Engineering", "lifecyclestage": "mql" }
},
"changed_fields": ["jobtitle", "lifecyclestage"]
}
  • object is the full record, identical in shape to GET /v1/contacts/:id / GET /v1/companies/:id. For deleted it is the last-known state, or null.
  • source is the origin of the change: an integration name (hubspot, apollo, …), or manual / form / api / system.
  • changed_fields lists the fields that changed when available — it can be empty (creates, deletes, and updates where a per-field diff isn’t computed). Treat object as the source of truth and reconcile by updated_at.

Events support URL filters — AND/OR substring patterns matched against the event URL (e.g. only deliver page_view events whose URL contains /pricing).

Contacts and companies support entity filters:

  • Changed-field filter — when a per-field diff is available, only fire on updates that touch one of the named fields (e.g. lifecyclestage). When no diff is available the filter fails open (the update is still delivered). Creates and deletes always fire.
  • Source filter — only fire when the change came from one of the named origins (e.g. hubspot).

Compute an HMAC-SHA256 of the raw request body (not the parsed JSON) with your signing secret, and compare it to X-Pathbound-Signature (after stripping sha256=). Use a constant-time comparison.

Node
import crypto from 'node:crypto';
function verify(rawBody, header, secret) {
if (!header?.startsWith('sha256=')) return false;
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const provided = header.slice('sha256='.length);
if (provided.length !== expected.length) return false;
return crypto.timingSafeEqual(Buffer.from(provided), Buffer.from(expected));
}
Python
import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
if not header.startswith('sha256='):
return False
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header[len('sha256='):])

If your framework already parsed the body, capture the raw bytes before parsing — re-stringifying won’t match byte-for-byte.

  • Timeout. Respond 2xx within 30 seconds.
  • Retries. A non-2xx, timeout, or network error is retried up to 3 times with backoff (1s, 10s, 60s). A 4xx is treated as a permanent client error and not retried.
  • Circuit breaker. After a long run of consecutive failures a subscription auto-pauses; re-enable it from the dashboard once your endpoint is healthy.
  • At-least-once. Deliveries can repeat — dedupe on id.
  • Latest-wins, not strictly ordered. Retries mean two changes to the same record can arrive out of order. Treat object as the latest state and reconcile by updated_at rather than relying on delivery order.
  • Coalescing. Many rapid changes to the same record within a short window (e.g. an integration sync touching it repeatedly) collapse into a single delivery carrying the net change.

/v1/streams exposes stream configuration to scripts and AI agents. The same tools are available through the MCP server (list_streams, get_stream, create_stream, update_stream, delete_stream, preview_stream_matches, get_stream_options, send_stream_test_email; see the tools reference).

MethodPathScope
GET/v1/streamsstreams:read
GET/v1/streams/optionsstreams:read
GET/v1/streams/:idstreams:read
GET/v1/streams/:id/deliveriesstreams:read
POST/v1/streams/previewstreams:write
POST/v1/streamsstreams:write
PATCH/v1/streams/:idstreams:write
DELETE/v1/streams/:idstreams:write
POST/v1/streams/:id/test-emailstreams:write (email automations only)

streams:write is opt-in: tick it when you create the API key, and grant it explicitly when you connect an MCP client. Both require a workspace admin. The MCP tools additionally need Manage streams enabled under Pathbound → Streaming in your MCP Tools settings. That is a workspace-level switch, independent of any integration; whether a Resend segment destination is creatable is reported separately by GET /v1/streams/options.

An email automation asks for more than any other stream, because it sends email: the key or connection needs actions:write as well as streams:write, and Resend’s Send email toggle must be on.

It cannot set a destination URL. A stream that POSTs to an endpoint is a standing export of your customer data, so choosing that endpoint stays a human action in the dashboard. Over the API you can create automations — streams whose destination is an action on the matching record: add or remove tags, or add to or remove from a Resend segment in your own connected account. You can also create an email automation, which sends a template of your Resend account to the person who submitted a form. destination_url, custom headers and signing secrets are never settable and never returned.

What it can change depends on where the stream delivers:

AutomationsEmail automationsHTTPS streams
Createyesyesno — dashboard only
name, activeyesyesyes
conditions, event_type, filters, resource_typeyesno: it has noneno
destination_configimmutable (the tags, the segment) — recreatetemplate_id, daily_cap, min_interval_hours; never form_id—
destination_url, headers, secretnevernevernever

An HTTPS stream’s destination is invisible to the API, so the API also refuses to change what gets sent there. Widening one — clearing its conditions, or switching it from event to contact — would export customer records to an address the caller cannot read. Those edits return 403 STREAM_FIELD_FORBIDDEN; make them in the dashboard.

Three more deliberate omissions: POST /v1/streams/preview returns counts only (no contact records) and requires streams:write because a contains predicate over live data is a substring oracle; GET /v1/streams/:id/deliveries omits request and response bodies; and for HTTPS streams the delivery error is reduced to a category (timeout, destination_client_error, …) rather than the raw message, which can name the host. Use the dashboard to inspect an actual delivered payload.

An automation is a stream whose destination is an action. Four act on a contact or company that matches conditions, and are described here. A fifth, resend_send_email, sends a Resend template to the person who submitted a form; it follows different rules and has its own section. A sixth, resend_send_event, sends one of your Resend events for that person instead, and has its own section too. GET /v1/streams/options reports which are creatable for your workspace right now and lists the valid values for each one’s config fields (option_sources).

destination_typeActs onConfigMeters?
pathbound_tag_addcontacts, companies{ "tags": ["vip", "customer"] } (1–10)no
pathbound_tag_removecontacts, companies{ "tags": ["lead"] } (1–10)no
resend_segment_addcontacts{ "segment_id": "seg_…" }yes
resend_segment_removecontacts{ "segment_id": "seg_…" }yes
resend_send_emailform submissionsa form and a Resend template: see Email automationsno
resend_send_eventform submissionsa form and a Resend event: see Resend event automationsno

(resend_segment is still accepted as an alias of resend_segment_add.)

Terminal window
curl -X POST https://api.pathbound.ai/v1/streams \
-H "Authorization: Bearer $PATHBOUND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Customers → onboarding segment",
"destination_type": "resend_segment_add",
"destination_config": { "segment_id": "seg_abc123" },
"event_type": "*",
"conditions": [
{ "field": "properties.lifecyclestage", "op": "eq", "value": "customer" },
{ "field": "properties.company_size", "op": "in", "value": ["201-500", "501-1000"] }
]
}'
Terminal window
# Tag every enterprise company, and drop the "lead" tag from contacts that became customers.
curl -X POST https://api.pathbound.ai/v1/streams \
-H "Authorization: Bearer $PATHBOUND_API_KEY" -H "Content-Type: application/json" \
-d '{ "resource_type": "company", "destination_type": "pathbound_tag_add",
"destination_config": { "tags": ["enterprise"] }, "event_type": "*",
"conditions": [{ "field": "properties.numberofemployees", "op": "exists" }] }'
curl -X POST https://api.pathbound.ai/v1/streams \
-H "Authorization: Bearer $PATHBOUND_API_KEY" -H "Content-Type: application/json" \
-d '{ "destination_type": "pathbound_tag_remove",
"destination_config": { "tags": ["lead"] }, "event_type": "updated",
"conditions": [{ "field": "properties.lifecyclestage", "op": "eq", "value": "customer" }] }'

The response carries estimated_matches — how many records match right now — so you can see the blast radius. Pass "validate_only": true to get that estimate without creating anything. enrolled_count on a stream is the number of records the action has been applied to.

Rules worth knowing before you script this

Section titled “Rules worth knowing before you script this”
  • Conditions are level-triggered. They describe current state (“lifecyclestage is customer”), never a transition (“became a customer”). A record matches for as long as the state holds.
  • Conditions are AND-ed. For “A or B” on a single field, use the in operator. Across fields, create two automations — tag automations may share the same tags.
  • Each tag or segment automation acts on each record at most once, and never undoes it. A contact is added to (or removed from) a segment once; a tag is added (or removed) once. When the record stops matching nothing is reversed, and a human who undoes the action afterwards is not fought: a tag you remove is not re-added, a contact you re-add is not re-removed. A no-op — the tag was already there, the contact was not in the segment — also counts as done. An email automation is different: it acts once per form submission, so the same person can be emailed again by a later submission, after the automation’s interval.
  • Tag actions re-enter the stream. Adding or removing a tag is a change like any other, so the record is captured again a few seconds later and delivered to every other matching stream, HTTPS streams included. That is what lets one automation key off a tag another applied ({ "field": "tags", "op": "eq", "value": "vip" }). The same automation never fires twice for one record, so this is bounded. The source reported on that follow-up change is the record’s last upstream source, not the automation.
  • Tag automations do not meter. They write into your own workspace and nothing leaves it. Resend segment automations meter one activation per contact per cycle, exactly like a delivery. An email automation does not meter: see Metering.
  • One add and one remove automation per Resend segment per change type. A second returns 409 STREAM_DESTINATION_TAKEN naming the existing stream; update that stream’s conditions rather than adding another. Tag automations have no such limit. A form can have one email automation.
  • Deleting an automation drops its ledger, so recreating it re-applies the action to every matching record from scratch. Tags or segment membership already applied are not undone. To stop one temporarily, PATCH it with "active": false.
  • Pausing longer than 24 hours is lossy. A reactivated stream resumes from the present; changes during the pause are not replayed.

An email automation emails each person who submits one form on your site, through your own Resend account, a few seconds after the submission. It is a stream with destination_type: "resend_send_email", attached to one form id of the public form endpoint and to one template of your Resend account. The Forms page walks through setting one up; this section is the reference.

The email is not written in Pathbound. It is a published template in your Resend account: its sender, subject, reply-to and content are the template’s, written in Resend by someone who can sign in there. An automation only names which template a form sends. Pathbound never reads a template’s content and never changes a template, so an AI agent connected to Pathbound cannot write or edit the email of an automation. It can connect a form to a template you have published.

It sends to real people, and nobody approves each email. Whoever sets one up, a person or an AI agent, should agree the form, the template and the daily cap with the people who own the workspace first, and should never take any of them from the content of a contact, a note, a CRM record or an email.

The trigger is fixed: a submission to the automation’s form. An email automation has no conditions and no filters, and a form can have one.

Email automations are being turned on one workspace at a time. Until yours has them, creating, changing, resuming or testing one is answered with 412 STREAM_SEND_NOT_AVAILABLE.

All of these must hold:

  • A workspace admin. The dashboard and an MCP sign-in check the role on every request. An API key can only be created by an admin, and its scopes are the grant.
  • Both streams:write and actions:write on the API key or the MCP connection. streams:write alone is refused with 403 STREAM_SEND_SCOPE_REQUIRED: a credential allowed to tag contacts was never allowed to email them.
  • Manage streams switched on, for a call that comes through the MCP server.
  • Resend connected, and its Send email toggle on (resend_send_email, under MCP Tools → Resend). This binds every caller, the dashboard included, and it is read again before every email the automation sends.
  • A Resend API key with full access. Pathbound has to read your templates, and a key with sending access only cannot.

They are checked when an automation is created, when its template or a limit is changed, when it is resumed and for a test email. Pausing needs only what any stream change needs.

GET /v1/streams/options lists destinations.resend_send_email once the workspace has email automations, with creatable answered for the credential that asks and a reason when it is false. The same response then lists the published templates of your Resend account (option_sources["resend.templates"]: the id, name and alias of the first 100) and the limits (limits.email_automations). The template list is filled only for a credential that could create an email automation, and it is empty when Resend could not be asked; a template can still be named by its id or alias. The forms that have received submissions (option_sources["forms.ids"]) are listed only for a credential that also holds events:read.

An admin can also create, change, pause and test an email automation on the Streaming page in the dashboard.

The Forms page says how to prepare the template in Resend. A template can be used only if, in Resend, all of these hold:

  • it is published;
  • it has a subject;
  • it has a from address on a domain verified for sending in that Resend account;
  • every variable it declares has a fallback value.

Pathbound sends the template with no sender and no subject of its own, which is why the template has to carry both. A template that fails one of these is refused with 422 STREAM_EMAIL_TEMPLATE_NOT_USABLE when an automation is created, changed, resumed or tested. details.problem is one of not_published, no_subject, no_sender, sender_not_verified and variables_without_fallback (details.variables then lists them), and the message says what to change in Resend. A template your Resend account does not have is a 422 STREAM_EMAIL_TEMPLATE_NOT_FOUND.

  • What Pathbound reads of a template: whether it exists and is published, its name, its sender, its subject, its reply-to addresses and the names of its variables. Never its HTML or its text. Pathbound cannot show you the email; the test email can.
  • The one variable Pathbound fills is first_name: this exact key, declared as a string in the template. In Resend’s editor it is written {{{first_name}}}. It is filled with the contact’s first name when the contact’s own email address is the one being emailed, and otherwise with the firstname sent in that submission. It can be text a stranger typed into your form, and it goes into an email sent from your domain, so it is used only when it is a single word of letters, hyphens and apostrophes, at most 25 characters. Otherwise Pathbound gives the variable no value, and the fallback value you set in Resend is sent. No other variable is ever filled, which is why each one needs a fallback.
  • No part of the email can be set through Pathbound. from, reply_to, subject, body, text, html, cc, bcc or variables in destination_config is refused with 400 STREAM_CONFIG_FIELD_UNKNOWN and a message that says it is set on the template in Resend. It is never silently ignored.

Pathbound reads the template again before every email, and remembers the answer for one minute.

  • Its content, subject or sender is changed in Resend. Emails sent from then on are the template as Resend holds it. Pathbound notifies nobody about a change made in Resend.
  • It is unpublished or deleted, a variable is added without a fallback, or its sender’s domain is no longer verified. Emails wait: their runs stay pending with the reason template_not_usable, for up to a day after each submission. The workspace’s admins are told, and the emails are sent once the template is fixed in Resend. Nothing has to be changed in Pathbound. Meanwhile GET /v1/streams/:id returns template.usable: false with a problem that says what to change.
  • A form should send a different template. Change the automation’s template_id: see Change.

POST /v1/streams with destination_type: "resend_send_email". Send no event_type, no resource_type and no conditions.

{
"destination_type": "resend_send_email",
"name": "Playbook email",
"active": true,
"validate_only": false,
"destination_config": {
"form_id": "playbook",
"template_id": "playbook-email",
"daily_cap": 50,
"min_interval_hours": 24
}
}
FieldRequiredDescription
destination_config.form_idyesThe form’s id: the last part of the URL the form posts to. It cannot be changed afterwards. The form does not have to have received a submission yet.
destination_config.template_idyesThe id or the alias of a published template in your Resend account: letters, digits, _ and -, up to 128 characters. See The template.
destination_config.daily_capnoThe most emails this automation sends per UTC day. 1 to 200, default 50.
destination_config.min_interval_hoursnoHours before the same address is emailed again from this form. 1 to 720, default 24.
namenoA label, up to 100 characters.
activenoWhether it starts running. Default true.
validate_onlynoRun every check and create nothing.

conditions, entity_filters or url_filters are refused with 400 STREAM_FIELD_UNSUPPORTED. They are never silently dropped: an automation saved without the condition you asked for would email everyone. A resource_type other than event, or an event_type other than form_submission, is a 400 STREAM_DESTINATION_UNSUPPORTED.

Response — 201 Created

{
"status": "success",
"stream": { "_id": "6703f1c2a4b5c6d7e8f90123", "destination_type": "resend_send_email", "…": "…" },
"form": { "form_id": "playbook", "submissions_last_30_days": 12 }
}

stream is the stream object, with template: the sender and the subject the automation will send, as Resend holds them. form.submissions_last_30_days tells you whether the form id is one that receives submissions: 0 usually means the id is misspelled or the form is not wired yet.

A second automation for the same form is refused with 409 STREAM_DESTINATION_TAKEN; details.existing_stream_id names the first. Change that one instead.

With validate_only: true — 200 OK

{
"status": "success",
"validate_only": true,
"would_create": true,
"stream": {
"name": "Playbook email",
"resource_type": "event",
"event_type": "form_submission",
"destination_type": "resend_send_email",
"destination_label": "Form playbook",
"destination_config": {
"form_id": "playbook",
"template_id": "34a080c9-b17d-4187-ad80-5af20266e535",
"template_name": "Playbook email",
"daily_cap": 50,
"min_interval_hours": 24
},
"template": {
"id": "34a080c9-b17d-4187-ad80-5af20266e535",
"name": "Playbook email",
"from": "Acme Team <[email protected]>",
"subject": "Your playbook",
"reply_to": [],
"variables": ["first_name"],
"fills": ["first_name"],
"usable": true
},
"active": true
},
"form": { "form_id": "playbook", "submissions_last_30_days": 12 }
}

Every check of a real create has run, the read of the template included. stream has no _id. Nothing is written and nobody is notified. Use it to see which sender and subject an automation would send before you create it.

{
"_id": "6703f1c2a4b5c6d7e8f90123",
"tenant_id": "tenant_xyz",
"name": "Playbook email",
"resource_type": "event",
"event_type": "form_submission",
"destination_type": "resend_send_email",
"destination_label": "Form playbook",
"destination_config": {
"form_id": "playbook",
"template_id": "34a080c9-b17d-4187-ad80-5af20266e535",
"template_name": "Playbook email",
"daily_cap": 50,
"min_interval_hours": 24
},
"template": {
"id": "34a080c9-b17d-4187-ad80-5af20266e535",
"name": "Playbook email",
"from": "Acme Team <[email protected]>",
"subject": "Your playbook",
"reply_to": [],
"variables": ["first_name"],
"fills": ["first_name"],
"usable": true
},
"active": true,
"active_since": "2026-10-07T09:00:00.000Z",
"created_by": "abc123",
"createdAt": "2026-10-07T09:00:00.000Z",
"updatedAt": "2026-10-07T09:00:00.000Z",
"has_headers": false,
"has_secret": false,
"sent_count": 3,
"sent_today": 1,
"failed_count": 0,
"skipped_count": 2,
"unconfirmed_count": 0,
"waiting_count": 0,
"last_sent_at": "2026-10-07T09:14:05.000Z"
}
  • destination_config.template_id is always Resend’s id of the template, also when you named the template by its alias. template_name is the template’s name in Resend when the automation was last saved. The server sets it; sending it is refused.
  • template is the template as Resend holds it when you ask. It is not stored. GET /v1/streams/:id, a create, a validate_only and a change of the config or a resume return it. GET /v1/streams (the list) does not.
    • variables are the variables the template declares. fills are those Pathbound gives a value to: first_name, or none.
    • When the template can no longer be sent, usable is false and problem is a sentence that says what to change in Resend. For a template that was deleted, only id, name, usable and problem are returned.
    • When Resend could not be asked, it is { "id": "…", "name": "…", "unavailable": true }.
  • active_since is set when the automation is created and again at every resume. Nothing submitted before it is emailed.
  • blocked_since is present only while Resend refuses the key or the sender. See What is emailed, and what is not.
  • The counters count runs by status, and sent_today counts from 00:00 UTC. GET /v1/streams and GET /v1/streams/:id return them. There is no enrolled_count for this type.

PATCH /v1/streams/:id. For this type the body may hold only name, active and destination_config.

{
"destination_config": {
"template_id": "playbook-email-v2",
"daily_cap": 100
}
}
  • destination_config may hold any of template_id, daily_cap and min_interval_hours. What you send is merged over the saved config, and the template is read from Resend again and checked as it is at that moment, whichever field changes.
  • The form cannot change. Create another automation for another form. Over this API, destination_config.form_id, any key inside destination_config that is not in the list above (template_name and every part of an email included), and any other top-level field (conditions, event_type, resource_type, filters) are refused with 403 STREAM_FIELD_FORBIDDEN. validate_only is refused with 400 STREAM_FIELD_UNSUPPORTED.
  • name is one line of up to 100 characters (400 STREAM_NAME_INVALID otherwise).
  • A credential without actions:write that sends destination_config is refused with 403 STREAM_SEND_SCOPE_REQUIRED before the template it named is looked up.
  • Pause with { "active": false }. Emails that have not started are cancelled. Pausing is never limited, and it needs only what any stream change needs.
  • Resume with { "active": true }. It goes through the same checks as a create, the check of the template included. Nothing submitted while the automation was paused is emailed, however short the pause.
  • A new template_id applies to every email that has not been attempted yet, including one that is waiting. An email that was already attempted and is being retried is sent with the template it was first prepared with.

The response is { "status": "success", "stream": { … } }. stream.template is present when the call changed the config or resumed the automation.

DELETE /v1/streams/:id removes the automation and the emails it had queued. It does not reset who was emailed: an automation created again for the same form does not email an address before its interval has passed.

POST /v1/streams/:id/test-email, with no body.

It sends the automation’s template once, to the address of the Pathbound account behind the credential: the account that created the API key, or the person signed in through MCP. There is no recipient parameter, so a test can never be pointed at someone else. Any field in the body is refused with 422 STREAM_TEST_TAKES_NO_INPUT.

{
"status": "success",
"sent_to": "[email protected]",
"sent_at": "2026-10-07T09:05:00.000Z",
"email_id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
"template_id": "34a080c9-b17d-4187-ad80-5af20266e535",
"template_name": "Playbook email",
"subject": "Your playbook"
}

subject is the template’s subject as Resend holds it, with its variables not filled in. first_name is filled from that account’s first name when the template declares it. The checks are the same as for a create, the check of the template included. A workspace can send 5 test emails a minute. A test counts against the workspace’s 500 emails a day, and not against the automation’s daily_cap.

The test email is the only way to see the email from Pathbound, which never reads a template’s content.

If Resend does not answer, the call ends with 502 SEND_UNCONFIRMED: the email may have been sent, so look in the inbox before you send another.

  • One email per submission, a few seconds after it arrives.
  • At most one email per address per min_interval_hours per form (24 hours unless you change it), even if the person submits again, and even if the automation is deleted and created again. [email protected] counts as [email protected], and for Gmail addresses the dots are ignored.
  • Only the submitted address is emailed, and only when it is one plain email address. Pathbound adds no other recipient.
  • An address that Resend lists as unsubscribed is not emailed.
  • Nothing submitted before the automation was created, or while it was paused, is emailed. Resuming does not send what arrived during the pause.
  • A submission that arrives after a limit is reached is not emailed later, not the next day either. See Limits.
  • A submission older than 24 hours is never emailed. An email that cannot be sent is tried again after 30 seconds, 2 minutes, 10 minutes, 30 minutes, 1 hour, 2 hours, 4 hours and 8 hours, for up to 24 hours after the submission, and then given up on.
  • At most once. When Resend does not answer, nobody knows whether the email went out. It is tried again in a way that Resend recognizes as the same email, and if no attempt is confirmed, the run ends unconfirmed and is not sent again.
  • If Resend refuses your API key or the sender (a 401 or a 403), the automation is blocked. Its emails wait, for up to a day, instead of failing; the admins are told; Resend is asked again at least every 30 minutes; and the automation carries on by itself once the key or the domain is fixed.
  • Emails also wait, for up to a day, while Resend is disconnected or connected again with Send email not yet set, and while the template cannot be sent as it is: see When the template changes in Resend. Both are checked again before every email.
  • If an admin switches Send email off, nothing is sent while it is off, and emails that were waiting are skipped for good.
  • A workspace without an active plan sends nothing.

Each email that is sent is recorded on the contact’s timeline and appears among the emails sent to that contact.

LimitValue
Emails per automation per UTC day (daily_cap)50 by default, 200 at most
Emails per workspace per UTC day, all email automations together, test emails included500
Emails per UTC day to one recipient domain that is not a free-mail provider such as gmail.com10
Hours before one address is emailed again from one form (min_interval_hours)24 by default, 1 to 720
Email automations per workspace20
Creates, changes of the template or a limit and resumes per workspace per day, over the API and through MCP together20
Test emails per workspace5 a minute, 20 per UTC day

The per-domain limit is there because a company’s mail server may accept any invented address at its domain.

The limit of 20 changes a day does not count a refused request or a validate_only call. Pausing is never counted, and changes made in the dashboard are outside it, so an admin can always fix and resume an automation. Past the limit, the call is refused with 429 STREAM_AGENT_CHANGE_LIMIT.

A submission that is over a daily limit gets a run with status skipped. It is not queued for the next day.

GET /v1/streams/:id/deliveries?page=1&limit=20 returns an email automation’s runs, newest first. A run is one submission and what became of its email.

{
"status": "success",
"deliveries": [
{
"id": "6703f2a1a4b5c6d7e8f90aaa",
"status": "sent",
"contact_id": "6703f1c2a4b5c6d7e8f90456",
"submission_id": "6703f29fa4b5c6d7e8f90999",
"submitted_at": "2026-10-07T09:14:02.000Z",
"attempts": 1,
"sent_at": "2026-10-07T09:14:05.000Z",
"finished_at": "2026-10-07T09:14:05.000Z"
},
{
"id": "6703f2b0a4b5c6d7e8f90bbb",
"status": "skipped",
"reason": "cooldown",
"contact_id": "6703f1c2a4b5c6d7e8f90456",
"submission_id": "6703f2aea4b5c6d7e8f909aa",
"submitted_at": "2026-10-07T09:20:41.000Z",
"attempts": 0,
"finished_at": "2026-10-07T09:20:44.000Z"
}
],
"pagination": { "page": 1, "limit": 20, "total_items": 2, "total_pages": 1 }
}

next_attempt_at is present while a run is pending or sending. A run never returns the address or anything Resend said, and over the API it does not return Resend’s id of the email either: the dashboard shows that. Skipped runs are kept for 35 days.

StatusMeaning
pendingWaiting to be sent, to be retried, or for something to be fixed. reason says which.
sendingBeing sent now.
sentResend accepted the email.
skippedIt was decided not to send. Nothing was sent.
failedResend refused the email, or 24 hours passed. Nothing was sent.
unconfirmedIt may have been sent. It is never sent again.
ReasonStatusMeaning
pausedskippedThe automation was paused, or the submission arrived before it was created or last resumed.
deletedskippedThe automation was deleted.
disabledskippedAn admin switched Resend’s Send email off.
no_planskippedThe workspace has no active plan.
staleskippedThe submission was more than 24 hours old, or is gone.
invalid_recipientskippedThe submitted address is not one plain email address, or the contact no longer exists.
unsubscribedskippedResend lists the address as unsubscribed.
cooldownskippedThe address was emailed from this form within min_interval_hours.
daily_capskippedThe automation’s daily_cap or the workspace’s daily total was reached.
source_limitskippedThe recipient’s domain reached its daily limit.
provider_rejectedfailedResend refused this email.
window_elapsedfailedIt could not be sent within 24 hours.
outcome_unknownunconfirmedResend did not answer, and no later attempt confirmed the send.
idempotency_conflictunconfirmedResend holds this send for a different email.
key_changedunconfirmedThe workspace’s Resend API key changed after an attempt that got no answer.
retryingpendingResend gave an answer that allows another attempt.
resend_not_connectedpendingResend is not connected, or its key no longer works.
resend_refusedpendingResend refuses the key or the sender. The automation is blocked until that is fixed.
template_not_usablependingThe template was unpublished or deleted, or can no longer be sent as it is. See When the template changes in Resend.
send_toggle_unsetpendingResend was connected again and Send email has not been set since.
not_availablependingEmail automations are not turned on for the workspace at the moment.

Every create, every change of the template or a limit and every resume is shown to all verified admins of the workspace in the app’s notifications. Pathbound sends them no email about it. The notice says who made the change and has a link that opens the automation, where the form and the template are shown and it can be paused. Nobody approves an automation before it runs, so this notice is the review. If the notice cannot be recorded, nothing is changed and the call is answered with 503 STREAM_NOTICE_FAILED.

Admins are also told when an email could not be sent, when the automation is blocked, when Resend is not connected, when the template cannot be sent as it is, when a daily limit is reached (the automation’s, the workspace’s, or one recipient domain’s) and when the workspace has no plan. Each of these is shown at most once per automation per day, in the app’s notifications and not by email, so a problem is seen when an admin opens the app. They are not told when the content of a template is changed in Resend.

The emails of an email automation do not count against your monthly meter. Anyone on the internet can post to a public form, so metering them would let a stranger spend your monthly cap. In return, a workspace without an active plan sends none.

Every error below has the shape { "status": "error", "error": "…", "code": "…", "details": { … } }. details is present only for the codes that name it. A request that fails basic shape checks first (a malformed stream id, an active that is not a boolean) is a 400 with "error": "Validation failed" and no code.

HTTPcodeMeaning
403STREAM_ADMIN_REQUIREDThe caller is not a workspace admin.
403STREAM_SCOPE_REQUIREDThe API key has no streams:write.
403STREAM_SEND_SCOPE_REQUIREDThe credential has no actions:write (required_scopes, also under details).
412STREAM_TOOLS_DISABLEDThe call came through MCP and Manage streams is off.
412STREAM_SEND_NOT_AVAILABLEThe workspace does not have email automations yet.
412STREAM_SEND_RESEND_NOT_CONNECTEDResend is not connected, or its key no longer works.
412STREAM_SEND_DISABLEDResend’s Send email toggle is off or was never set.
400STREAM_FIELD_UNSUPPORTEDconditions, entity_filters or url_filters on create, or validate_only on PATCH (details.fields).
400STREAM_DESTINATION_UNSUPPORTEDA resource_type other than event, or an event_type other than form_submission.
400STREAM_CONFIG_FIELD_UNKNOWNAn unknown key in destination_config on create (details.fields). from, reply_to, subject, body, text, html, cc, bcc and variables get a message that says they are set on the template in Resend.
400STREAM_FORM_ID_INVALIDform_id is missing or does not fit the form id rule.
400STREAM_EMAIL_TEMPLATE_REQUIREDtemplate_id is missing, or is not one token of letters, digits, _ and - of up to 128 characters.
422STREAM_EMAIL_TEMPLATE_NOT_FOUNDYour Resend account has no template with this id or alias (details.field).
422STREAM_EMAIL_TEMPLATE_NOT_USABLEThe template cannot be sent as it is. details.problem is not_published, no_subject, no_sender, sender_not_verified or variables_without_fallback (then details.variables lists them). The message says what to change in Resend.
503STREAM_EMAIL_TEMPLATE_UNREADABLEResend could not be asked about the template just now. Nothing was changed; try again.
412STREAM_EMAIL_TEMPLATE_UNREADABLEResend refuses to show templates to the connected API key. The key must have full access.
422STREAM_EMAIL_LIMIT_INVALIDdaily_cap or min_interval_hours is outside its range (details.field).
400STREAM_NAME_INVALIDname is not one line of at most 100 characters, or holds a control character.
400STREAM_ACTIVE_INVALIDactive is not true or false.
403STREAM_FIELD_FORBIDDENA PATCH with a field, or a key in destination_config, that may not be changed over the API. This includes form_id and every part of an email.
400STREAM_DESTINATION_IMMUTABLEA change of form_id or destination_type made in the dashboard. Over the API the same attempt is a 403 STREAM_FIELD_FORBIDDEN.
409STREAM_DESTINATION_TAKENThe form already has an email automation or a Resend event automation (details.existing_stream_id).
429QUOTA_EXCEEDEDThe workspace has 20 email and Resend event automations together, or has reached its stream limit.
429STREAM_AGENT_CHANGE_LIMITThe 20 changes a day over the API and MCP are used.
503STREAM_NOTICE_FAILEDThe admins could not be notified. Nothing was changed; try again.
422STREAM_TEST_TAKES_NO_INPUTTest email: the request had a body.
422STREAM_TEST_RECIPIENT_INVALIDTest email: the credential’s account has no usable address.
412STREAM_SEND_RESEND_REFUSEDTest email: Resend refused the key or the template’s sender. Nothing was sent.
422STREAM_SEND_REJECTEDTest email: Resend refused this email (details.provider_message). Nothing was sent.
429STREAM_SEND_DAILY_LIMITTest email: the workspace’s 500 emails for the day are used.
429STREAM_TEST_DAILY_LIMITTest email: the workspace’s 20 test emails for the day are used.
429STREAM_SEND_THROTTLEDTest email: Resend is limiting the account. Nothing was sent; try again in a minute.
502SEND_UNCONFIRMEDTest email: no answer from Resend. It may have been sent.

A Resend event automation sends one of your own Resend events for each person who submits one form on your site, a few seconds after the submission. The event starts the automations you built on it in Resend: a welcome series, a segment change, whatever you set up there. It is a stream with destination_type: "resend_send_event", attached to one form id and one event name.

It is an email automation with an event in the template’s place. The trigger, the daily limits, the runs, the notice to admins, pausing and metering are the same, and so is who may set one up, with one difference: the toggle is Resend’s Send events (resend_send_event), not Send email. This section covers only what differs.

Every submission sends the event. There is no wait per address: when the same person submits the form again, the event is sent again, and what a repeat should do is decided by your automation in Resend. The daily limits still apply.

Pathbound cannot see what an event sets off. One event starts every active automation built on it, and an automation may send several emails over days. A run that reads sent means Resend accepted the event, not that an email was sent. daily_cap limits events, not the emails your automations then send.

A form has one automation that sends: an email automation or a Resend event automation, not both. A second one of either kind is a 409 STREAM_DESTINATION_TAKEN.

An admin sees a Resend event automation on the Streaming page in the dashboard, with its runs, and can pause, resume or delete it there. It is created and changed over the API or by an agent.

POST /v1/streams with destination_type: "resend_send_event". Send no event_type, no resource_type and no conditions.

{
"destination_type": "resend_send_event",
"name": "Demo requests",
"destination_config": {
"form_id": "demo",
"event": "demo.requested",
"daily_cap": 50
}
}
FieldRequiredMeaning
destination_config.form_idyesThe form whose submissions trigger it. It cannot be changed afterwards. A form needs no setup: see Forms.
destination_config.eventyesThe exact name of an event your Resend account defines, capitals included. GET /v1/streams/options lists them under option_sources["resend.events"]. Events are created in Resend, and Resend’s own events (resend:…) cannot be named.
destination_config.daily_capnoThe most events this automation sends per UTC day, 1 to 200. Default 50. A submission that arrives after it is reached gets no event later.

destination_config takes nothing else. There is no recipient, no payload, no template and no min_interval_hours to set: a key such as email, payload or template_id is a 400 STREAM_CONFIG_FIELD_UNKNOWN.

The workspace’s total of 500 a day and the limit of 10 a day per recipient domain (free mail excepted) apply as for an email automation.

"validate_only": true checks the request, including the event’s name against your Resend account, without creating anything or notifying anyone.

  • To whom. The address the person submitted, as one plain address. Resend creates a contact for an address it does not know. A person Resend lists as unsubscribed is sent no event.
  • The payload. One field, first_name, and only when the event declares first_name as a string in Resend and the person gave a first name of one word. Nothing else typed into the form travels with the event.
  • Once per submission. One event for each submission, and another for each further submission by the same person.

An event is never sent twice for one submission

Section titled “An event is never sent twice for one submission”

Resend does not deduplicate events: a second request starts every automation again. So an event Resend did not clearly answer (a timeout, a server error) is not retried, unlike an email. Its run ends unconfirmed with reason outcome_unknown, and the admins are told. Check the automation’s runs in Resend to see whether it started.

An event Resend itself refused was not accepted. A rate limit is retried for up to a day, a refusal of your API key makes runs wait until it is fixed, and any other refusal ends that one run as failed.

The event is looked up in your Resend account before every send, by exact name. If it was deleted or renamed, runs wait with reason event_not_found for up to a day after each submission, the admins are told, and they go out once an event of that name exists again or the automation is changed to another event.

PATCH /v1/streams/:id. destination_config may hold event and daily_cap; what you send is merged over the saved config, and the event is looked up in Resend again. name and active work as for an email automation. form_id cannot change.

There is no test send: POST /v1/streams/:id/test-email answers 400 STREAM_DESTINATION_UNSUPPORTED, because a test event would start your real automations. Submit the form once to try it.

The error codes of an email automation apply, with STREAM_SEND_DISABLED meaning Resend’s Send events toggle. A Resend event automation adds these:

HTTPcodeMeaning
400STREAM_EVENT_REQUIREDevent is missing, is not a usable name, or is one of Resend’s own events.
422STREAM_EVENT_NOT_FOUNDYour Resend account defines no event of this exact name (details.field).
503STREAM_EVENT_LIST_UNREADABLEResend could not be asked for its events just now. Nothing was changed; try again.
412STREAM_EVENT_LIST_UNREADABLEResend refuses to list events for the connected API key. The key must have full access.
  • Treat the destination URL as public. The signature is your only authenticator — verify it on every request.
  • Acknowledge fast, process async. Return 200 OK, then do the work in a background job.
  • Be idempotent. Use id as a dedup key.
  • Inspect deliveries in the dashboard — response code, latency, body, and attempt count per subscription.