Skip to main content
A profile is a contact. Every read and write is confined to the workspace that owns the API key, and every consent change is written to the consent log. All nine endpoints authenticate with the X-Sender-Tenant and X-API-Key headers. The base URL is https://api.senderz.app/api/v1.

Identity rules

A profile is identified by email, phone, or contact id. Email and phone are each unique per workspace among non-deleted contacts.
  • A contact needs at least one identifier. Create and upsert reject a body with neither an email nor a phone, with 400 and code contact_identifier_required.
  • Phone-only profiles are fully supported. Email is only validated when you actually send a non-empty value.
  • Emails are lowercased and trimmed on every write and on lookup, so Dana@Example.com and dana@example.com are the same contact.
  • Phones are normalized to E.164. A leading 00 becomes +, and Israeli local forms (050-123-4567, 0501234567, 501234567) resolve to +972501234567. Spaces, hyphens, dots and parentheses are stripped.
  • A phone that cannot be normalized is rejected, not silently dropped: 400 with code contact_phone_invalid.

The profile object

Every endpoint on this page returns this object, wrapped in the standard { "success": true, "data": ... } envelope.
string
Contact UUID.
string | null
Lowercased email address. Null on a phone-only profile.
string | null
E.164 phone number, for example +972501234567.
string | null
string | null
string | null
IANA timezone name, for example Asia/Jerusalem.
string | null
Language tag, for example he or en.
One of subscribed, unsubscribed, never_subscribed, cleaned. Defaults to never_subscribed.
Same four values. Defaults to never_subscribed.
Same four values. Defaults to never_subscribed.
Same four values. Defaults to never_subscribed.
ISO 8601 timestamp of the last email consent change.
string
One of valid, invalid, risky, unknown. Defaults to unknown. Read-only: it is set by list validation during import, never by these endpoints.
object | null
Free-form key and value store. See Custom properties.
string[] | null
string | null
Read-only. Assigned by RFM analysis, null until the contact has been scored.
string | null
boolean
Read-only. True when the contact’s email address is on the workspace suppression list (hard bounce, spam complaint, blacklist, or an unsubscribe). A suppressed address is skipped at send time regardless of emailConsent.
string | null
Read-only. ISO 8601 timestamp set by the recipient from the preference center when they choose to pause rather than unsubscribe. While it is in the future, marketing sends to this contact are skipped even when emailConsent is subscribed. There is deliberately no API to lift a pause early: the recipient clears it by opting in again, or it expires on its own.
string
ISO 8601 timestamp.
string
ISO 8601 timestamp.
Example profile
Four channels carry independent consent: emailConsent, smsConsent, pushConsent and whatsappConsent. Each accepts the same four states. Setting a consent value also writes a consent-log entry and stamps the matching *ConsentTimestamp. On create and upsert you can attribute the change with emailConsentSource, smsConsentSource, pushConsentSource and whatsappConsentSource (each a string of at most 64 characters). When you omit the source it is recorded as manual. Use api for programmatic writes so the consent record reflects where the opt-in came from.
Writing any consent value as unsubscribed triggers a contact-wide opt-out. The contact is unsubscribed on every channel and their identifiers are added to the suppression list. Sending smsConsent: "unsubscribed" will therefore also stop their email. To withdraw a single channel, use the blacklist endpoint with an explicit channel instead.
Omitting a consent field leaves it unchanged. On update, a value identical to the stored one is a no-op and does not restamp the timestamp.

Custom properties

customProperties is a JSON object of arbitrary keys.
  • On create, the object is stored as given.
  • On update, the object is shallow merged into the stored one. Keys you send win, keys you omit are preserved. There is no delete operation for a single key.
  • A key that matches a custom field defined in your workspace is coerced to that field’s type (number, date, boolean, URL, list, or a select option). A value that fails coercion returns 400 with a code naming the problem and a field naming the key.
  • Keys that match no definition are stored exactly as sent.
tags, by contrast, replaces the stored array. Send the full set you want.

List profiles

integer
default:"25"
Page size. Minimum 1, maximum 200. A larger value returns 400.
integer
default:"0"
Records to skip. Minimum 0. Takes precedence over page when both are sent.
integer
default:"1"
1-based page number, an alternative to offset. Minimum 1.
Case-insensitive substring match against email, phone, first name, last name, and the two names joined. At most 255 characters.
string
Restrict to members of a list or segment, by UUID. Works for both static and dynamic segments. An id that does not exist in your workspace returns an empty page rather than a 404.
One of subscribed, unsubscribed, never_subscribed, cleaned.
string
email or sms. This only chooses which consent column consent is matched against. It defaults to email and does nothing on its own, so send it together with consent.
string
default:"-createdAt"
Sort key. Legal columns are createdAt, updatedAt, email, firstName, lastName. Prefix with - for descending (-updatedAt), or use column:asc and column:desc. A bare timestamp column defaults to descending, a bare text column to ascending. An unknown column falls back to -createdAt rather than erroring.
Returns a paginated envelope. next_cursor is the next offset as a string, so feed it back as offset, not page.
200 OK

Look up a profile

Resolves one profile by email or contact id. Send exactly one of them.
string
Email address. Required unless contactId is sent. Trimmed and lowercased before matching. At most 320 characters.
string
Contact UUID. When present, email is ignored.
No other query parameter is accepted. Sending one, phone for example, returns 400. To resolve a contact by phone, use the upsert endpoint, which matches on phone.
Returns the profile object, or 404 with message Profile not found when nothing matches.

Get a profile by ID

string
required
Contact UUID. A value that is not a valid UUID returns 400.
Returns the profile object, or 404 with code contact_not_found.

Create a profile

Creates a new contact. Use upsert when the contact may already exist.
string
At most 320 characters. Trimmed and lowercased. Required unless phone is sent.
string
At most 32 characters before normalization. Required unless email is sent.
string
At most 128 characters.
string
At most 128 characters.
string
At most 64 characters.
string
At most 10 characters.
subscribed, unsubscribed, never_subscribed or cleaned.
Same four values.
Same four values.
Same four values.
At most 64 characters. Defaults to manual.
At most 64 characters. Defaults to manual.
At most 64 characters. Defaults to manual.
At most 64 characters. Defaults to manual.
string[]
Array of strings.
object
Free-form object.
string | null
At most 4000 characters. Send null to clear.

Address-style fields

These nine convenience fields are accepted on create, upsert and update-by-email. They are not native columns: each is folded into customProperties under the same key before the contact is written, so they come back inside customProperties, not at the top level.
string
At most 255 characters.
string
At most 255 characters.
string
At most 255 characters.
string
At most 64 characters.
string
At most 500 characters.
string
At most 500 characters.
string
At most 32 characters.
string
At most 64 characters.
string
At most 128 characters.
These nine fields are not accepted by update-by-id. Sending one there returns 400, because the API rejects unknown properties. Write them through customProperties on that route instead.
Returns 201 with the profile object.
201 Created
A successful create fires the contact.created outbound webhook and enrolls the contact in any automation triggered by a new contact.

Create errors

Upsert a profile

Create-or-update in one call, matching an existing contact by email first, then by phone. This is the endpoint to use for a sync, because it is safe to repeat. The body is identical to create, including the nine address-style fields.
At least one of email or phone is required. A body with neither returns 400 with the message email or phone is required.
When a match is found the contact is updated, which means customProperties is merged, tags is replaced, and an omitted field is left alone. When nothing matches, a new contact is created.
If the email you send resolves to one contact and the phone you send already belongs to a different contact, the update fails with 409 and code contact_phone_taken. Upsert never merges two existing contacts.
Returns 200 with the profile object, whether it was created or updated.

Update a profile by email

Updates an existing contact addressed by email. Returns 404 when the email does not resolve.
string
required
The lookup key, not a value to write. At most 320 characters. It is never rewritten unless you also send newEmail.
string
Change the contact’s email address. At most 320 characters.
string
At most 32 characters before normalization.
string
At most 128 characters.
string
At most 128 characters.
string
At most 64 characters.
string
At most 10 characters.
subscribed, unsubscribed, never_subscribed or cleaned.
Same four values.
Same four values.
Same four values.
string[]
Replaces the stored array.
object
Shallow merged into the stored object.
string | null
At most 4000 characters. Send null to clear.
The nine address-style fields are also accepted here and merge into customProperties.
The four *ConsentSource fields are not accepted on this route. A consent change made here is recorded with source manual. Use create or upsert when you need to attribute the opt-in.
Returns 200 with the profile object.

Update a profile by ID

string
required
Contact UUID. A value that is not a valid UUID returns 400.
This route takes a different body from the routes above.
string
Replaces the contact’s email. At most 320 characters.
string
At most 32 characters before normalization.
string
At most 128 characters.
string
At most 128 characters.
string
At most 64 characters.
string
At most 10 characters.
subscribed, unsubscribed, never_subscribed or cleaned.
Same four values.
Same four values.
Same four values.
At most 64 characters.
At most 64 characters.
At most 64 characters.
At most 64 characters.
string[]
Replaces the stored array.
object
Shallow merged into the stored object.
string | null
At most 4000 characters. Send null to clear.
Two differences from the other write routes, both of which return 400 if you get them wrong. The nine address-style fields (city, country, stateProvince, zip, address1, address2, dateOfBirth, gender, jobTitle) are not accepted here. There is no newEmail: send email to change the address.
Returns 200 with the profile object.

Update errors

Both update routes share these: A successful update fires the contact.updated outbound webhook.

Blacklist

POST opts the contact out on the chosen channel and adds their identifier to the suppression list. DELETE lifts it and restores the consent state captured at the moment they were blacklisted, rather than blindly re-subscribing them. Both take the same body.
string
Contact UUID. When present, email is ignored.
string
Email address. Required unless contactId is sent. Trimmed and lowercased.
string
default:"email"
email, sms or both.
channel: "sms" on a contact with no phone number returns 400 with the message Contact has no phone for sms channel. channel: "both" on the same contact applies the email half and skips SMS without erroring.
POST returns 201. DELETE returns 200 with the profile object, not 204.
201 Created
Blacklisting fires the contact.unsubscribed webhook when the email channel was affected, and enrolls the contact in any unsubscribe automation. Lifting a blacklist fires contact.updated.

Errors

Every failure returns the standard envelope, with a machine-readable code where one exists:
Codes used by this resource: contact_identifier_required, contact_phone_invalid, contact_not_found, contact_email_taken, contact_phone_taken. A custom-property coercion failure returns a code naming the problem (invalid_number, invalid_date, invalid_boolean, invalid_url, invalid_list, invalid_option, too_long, too_many_items) together with a field naming the offending key. See Errors and rate limits for the shared envelope and limits, and Lists for adding a profile to a list.