Skip to main content
The forms API is the public subscribe endpoint behind embedded forms and the WordPress plugin. The bundled script calls it for you, and you can call it directly from a browser or from your own server.
The script is served from the domain root, not from /api/v1. The subscribe endpoint is the only one of the two that takes credentials.

Subscribe

Authenticated with the X-Sender-Tenant and X-API-Key header pair. X-Sender-Tenant accepts your workspace slug or its UUID. The endpoint is CORS enabled for any origin, so a browser can post to it directly. A preflight OPTIONS returns 204. The allowed request headers are Content-Type, X-Sender-Tenant and X-API-Key.

Identity

A submission must carry an email address, a phone number, or both. Neither field is required on its own.
  • The email is trimmed and lowercased.
  • The phone is normalised to E.164. Israeli local formats are accepted (0501234567 and 501234567 both become +972501234567), spaces, dashes, dots and brackets are stripped, and a leading 00 is read as +. A number that cannot be normalised is rejected rather than stored raw.

Body

string
Subscriber email address. Max 320 characters. Required only if no phone is sent.
string
Subscriber phone number. Max 40 characters. Required only if no email is sent.
boolean
default:"true"
Whether this submission is an opt in. Omit it or send true to subscribe the channels the submission carries. Send false to record the contact with no consent change on either channel. Must be a real boolean, not the string "true".
string
default:"embedded_form"
Which surface collected the submission. One of embedded_form or wordpress_form. See How source changes behaviour.
string
Your own label for which form this came from. Max 120 characters. Stored on the contact as the custom property embeddedFormId. It is client supplied and is not a security boundary.
object
Extra field values, keyed by contact property. See How fields map.
string
The page URL the form was submitted from. Max 2048 characters. Accepted and validated, and not written to the contact record today.
Unknown top level properties are rejected with 400. Send only the fields above.

Response

200 OK with the standard envelope.
boolean
Always true on a 200.
string
UUID of the contact that was created or matched.
boolean
true when this submission created the contact, false when it matched an existing one.
boolean
true when the contact is opted in on at least one channel after this submission. A subscribe: false submission that creates a contact returns false. A subscribe: false submission against a contact who was already opted in still returns true, because it describes the contact, not the change.

Phone only

A phone number with no email address is a complete submission. The contact is created with SMS consent and no email address.

Capture without opting in

A contact form or a support form collects an address without asking for marketing permission. Send subscribe: false and the contact is stored with both channels left at never_subscribed, no consent log entry, and no list_joined enrolment.
Consent is recorded per channel, and only for the channels the submission actually carries. Every consent change writes a consent log entry carrying the channel, the source, the caller IP address and the User-Agent of the request. That log is the evidence record for the opt in, which is why the endpoint reads the address and agent from the request itself rather than from the body. On an existing contact, a channel already at subscribed is left alone and produces no duplicate log entry. A subscribing submission also lifts any suppression entry for that address or number on that channel, so a re-subscribe through a form restores sending.

Flow enrolment

Two flow triggers fire after a successful submission, both best effort. A failure to enrol is logged and never fails the request.
  • contact_created fires when the submission created a new contact.
  • list_joined fires when the submission actually changed consent on at least one channel. It does not fire for a subscribe: false submission, and it does not fire when the contact was already opted in on every channel the submission carried.

How source changes behaviour

source sets three things at once. wordpress_form skips the allowlist because the WordPress plugin posts from your server, where there is no browser Origin header to check. A browser embed should stay on embedded_form so the allowlist still applies.

Origin allowlist

When source is embedded_form, the request Origin header is checked against the workspace allowlist configured in Settings → Embedded Forms.
  • An empty allowlist accepts any origin. This is the default, so existing embeds keep working until you decide to restrict them.
  • With entries configured, a request whose Origin does not match, and a request with no Origin header at all, are rejected with 403.
  • An entry written as a bare host (acme.com) matches that host on either http or https. An entry written as a full origin (https://acme.com) matches that scheme and host exactly.
  • Entries are matched case insensitively. Up to 20 entries are accepted.
The allowlist limits which page can post with your key. It does not replace keeping the key out of a public repository, and it does nothing for a server-side caller, which controls its own Origin header.

How fields map

Every key in fields is resolved against the contact property catalog, by property key, display label, or a known alias. firstName, first name and fname all reach the same property.
firstName, lastName, phone, timezone and language (stored as the contact locale) are written to the contact record itself.
Any other catalog property, for example city, zip, address1, country or dateOfBirth, is stored in the contact’s custom properties under its catalog key. formId is stored the same way, as embeddedFormId.
A key that matches no catalog property is dropped. Computed properties, the ones Senderz derives itself, are also ignored. An empty string value is skipped for every key.

Matching an existing contact

The endpoint matches on the submitted email address, then on the submitted phone number. When it finds an existing contact it fills gaps rather than overwriting.
  • An existing email address is never overwritten. A submitted email is written only when the contact has none, and an email sent inside fields is ignored entirely for an existing contact.
  • An existing phone number is never overwritten. A submitted phone is written only when the contact has none.
  • The other contact columns fields can reach, firstName, lastName, timezone and language, are overwritten by the submitted value. Send them only when the form really asked for them.
  • Custom properties from fields are merged into the existing ones, and a key present in the submission replaces its previous value.
To change an address or a name on an existing contact, use Profiles instead.

Errors

Checks run in this order: body validation, credentials, feature access, identity, then the origin allowlist. The first failure is returned, so a malformed phone number is reported before an origin problem. A coded error returns the machine readable code alongside the message, so you can branch on it without parsing English:
A missing email is no longer a validation failure. If you previously matched on the message email must be an email to detect an empty submission, switch to the contact_identifier_required code.
See Errors and rate limits for the shared envelope.

The form script

Public, no credentials, and identical for every workspace. It responds with Content-Type: application/javascript; charset=utf-8 and Cache-Control: public, max-age=300, so a change reaches a browser within five minutes.
The script binds every form[data-senderz-form] on the page, including forms added later, and posts each submission to the subscribe endpoint.
  • name="email" becomes email. name="phone" or name="sms" becomes phone. Every other named input goes into fields.
  • data-form-id becomes formId, and the current page URL becomes sourceUrl.
  • data-success-message and data-error-message override the text shown after a submission.
  • A successful submission fires a bubbling senderz:subscribed event on the form.
The script derives the API host from its own src, so it must be loaded from https://api.senderz.app. It also requires an email address before it will submit, even though the API itself accepts a phone number on its own. For a phone only form, post to the endpoint from your own code.