SplashSend API
The SplashSend REST API lets your own systems work with your people, lists, custom events, orders, products and data sets.
Authentication: every request needs a key or a token as a Bearer token: Authorization: Bearer sk_live_…. An app for your own account (a private app) uses its key (sk_live_…). Make one under Apps, then Apps you've made, or in Setup → API keys. An app for every SplashSend account (a public app) uses the access token (ssa_…) each account gives it when it connects with OAuth 2.1 and PKCE: see Connecting a public app. A key or a token belongs to one account and carries scopes. Each endpoint below names the scope it needs, and a request without it gets a 403 with the code forbidden. A key made under Apps is refused when it's sent from a web page (a request with an Origin header), with the code key_in_web_page: call SplashSend from a server.
Test mode: a new private app starts in test mode, and so does a public app on its maker's own account. Its requests work, but nothing it does starts a drip, sends an email or sends a webhook until it's switched to live on the app's page in SplashSend.
People access: a few fields show who someone is, and are sent only to a key with contacts:read. They are an order's email, an event's email, a list member's email and name, and data-set columns holding email addresses. Without that scope those fields are left out, and filtering events by email gets a 403. A write's response still shows an address the request itself sent.
Idempotency: POST /contacts, POST /contacts/import, POST /events, POST /orders, POST /lists, POST /data and POST /data/{key}/rows accept an Idempotency-Key header. Send a new key with each new request, and the same key when you retry it. A retry with the same key and the same request, body byte for byte, gets the first response back, and nothing is done twice. The same key on a different request gets a 422 (idempotency_key_reused). A retry sent while the first request is still running gets a 409 (idempotency_in_progress) with Retry-After: 5. A request that fails doesn't keep its key, so you can retry it with the same one. Keys are kept for 7 days.
Rate limit: 100 requests a minute per key, and per public app on each account. Over that you get a 429 (rate_limited) with Retry-After: 60. A request refused for a missing scope still counts.
Body size: a request body can be up to 256KB, or 1MB for POST /contacts/import and 4MB for POST /data/{key}/rows. The limit applies to the bytes received, with or without a Content-Length header. A larger body gets a 413 (payload_too_large).
Errors: every error has the body { "error": { "code": "…", "message": "…" } }. The message says what went wrong in plain words, and the codes are listed below.
Webhooks: SplashSend can also send events to your own systems as they happen. See Webhooks at the end of this page, and add endpoints in Setup → Webhooks.
AI assistants: the same API keys connect an AI assistant to your account over MCP, at https://splashsend.com/api/mcp. Its calls count towards the key's rate limit. See Connecting an AI assistant.
Building with an AI tool: this reference is also written for AI tools to read, at https://splashsend.com/developers/llms.txt, and as an MCP server your editor can search, with no key, at https://splashsend.com/api/mcp/docs.
| Status | Code | Meaning |
|---|---|---|
400 |
validation_error |
A field is missing, or has a value the endpoint can't take. |
400 |
invalid_request |
The body isn't valid JSON, or isn't the shape the endpoint expects. |
400 |
read_only |
The data set is built in or read from a connected source, so it can't be changed this way. |
400 |
limit_reached |
A cap was reached, such as 5,000 rows in a data set, 100 data sets, 500 lists in an account or 50 subscriptions for an app. |
400 |
sender_not_verified |
A campaign can't be sent or scheduled until the account's sender email is verified in Setup. |
400 |
design_not_ready |
The design can't be sent as it is, such as one without an unsubscribe link. The message says what to fix. |
400 |
send_refused |
The campaign couldn't be sent, for the reason the message gives. |
401 |
unauthorized |
The key or token is missing, wrong, expired or revoked, or the app was removed. |
403 |
forbidden |
The key or token doesn't have the scope this request needs. |
403 |
key_in_web_page |
A key made under Apps was sent from a web page, where anyone can copy it. Call SplashSend from a server, and make a new key for the app. |
403 |
plan_limit_reached |
Your plan's people limit is reached, so a new person can't be added, or its sends for the month are used up. |
403 |
not_open_yet |
The account isn't open for sending yet. |
404 |
not_found |
The person, list, data set or row isn't in your account. |
409 |
stopped_address |
The address is stopped because it bounced or was reported as spam, so it can't be subscribed. |
409 |
sent_recently |
This design went, or is scheduled to go, to the same people within a day, so it wasn't sent again. |
409 |
idempotency_in_progress |
A request with this Idempotency-Key is still running. Wait for the Retry-After header (5 seconds), then retry. |
413 |
payload_too_large |
The body is over the size limit. |
422 |
idempotency_key_reused |
This Idempotency-Key was already used for a different request. Send a new key. |
429 |
rate_limited |
The key, or the public app on this account, made more than 100 requests in a minute. Wait for the Retry-After header (60 seconds), then retry. |
500 |
internal_error |
Something went wrong on our side. |
Base URL https://splashsend.com · Download the OpenAPI spec to use in Postman, Insomnia or your own client.
Account
Which account a key or token is for, and what it may do.
/api/v1/meWho this key or token is for
The account's name, the app the key or token belongs to, whether that app is in test mode, and what it may do. Any working key or token can ask, whatever it may do. account is the brand's or sender's name, and null for an account with neither.
- 200 Who the key or token is for.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
object | |
data.account |
string or null | The account's name, as people see it on its emails. |
data.app |
string or null | The app the key or token belongs to. |
data.test_mode |
boolean | True while the app is in test mode: nothing it does starts a drip, sends an email or sends a webhook. |
data.scopes |
array of string | What it may do. |
curl -X GET "https://splashsend.com/api/v1/me" \
-H "Authorization: Bearer sk_live_…"
People
The people on your list. Their details, such as a birthday, are called properties here, and are named the way they are in SplashSend.
/api/v1/contactsList people
Needs the contacts:read scope.
Newest first.
| Name | Type | |
|---|---|---|
page |
integer · query | The page to read, from 1. default: 1 minimum: 1 |
limit |
integer · query | How many to read on a page, up to 100. default: 25 minimum: 1 maximum: 100 |
status |
"subscribed" | "unsubscribed" · query |
Only people with this status. |
email |
string · query | Only the person at this address, to look someone up before changing them. format: email |
- 200 A page of people.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 429 The key made more than 100 requests in a minute (
rate_limited).
curl -X GET "https://splashsend.com/api/v1/contacts" \
-H "Authorization: Bearer sk_live_…"
/api/v1/contactsAdd or update a person
Needs the contacts:write scope.
Adds a person by email address, subscribed, or updates the person already at that address.
Someone already here who never subscribed, for example someone first seen through an event or an order, is subscribed, and this request is recorded as their consent. Someone who unsubscribed stays unsubscribed unless you send "resubscribe": true, so only send it when they've asked to hear from you again. A stopped address, one that bounced or was reported as spam, is never subscribed.
When this request subscribes someone, their welcome drips start and contact.subscribed is sent, unless you send "startAutomations": false. source is set only for someone new. To add many people at once, use POST /contacts/import.
preferences switches on the email topics you name. For someone new, the topics you don't name are switched off. For someone already here, nothing is switched off.
meta says what happened. warnings explains a kept opt-out, a stopped address and anything that was skipped.
Accepts an Idempotency-Key header. Send a new key with each new request and the same key when you retry it: a retry gets the first response back instead of acting twice.
| Field | Type | |
|---|---|---|
emailrequired |
string | Their email address. format: email |
firstName |
string | Their first name. |
lastName |
string | Their last name. |
name |
string | Their full name, split into first and last. Ignored when firstName or lastName is sent. |
source |
string | Where they came from, such as website. Set only for someone new. |
properties |
PropertyValues | Details keyed by name, as they're called in SplashSend (any case). |
preferences |
array of string | The names of the email topics they chose. |
resubscribe |
boolean | Subscribe someone who unsubscribed. Send it only when they've asked to hear from you again. default: false |
startAutomations |
boolean | Start their welcome drips and send contact.subscribed when this request subscribes them, and start drips that watch the details you set. Send false for someone from your records rather than a new sign-up. default: true |
- 200 They were already here, and are updated.
- 201 They're new, and have been added.
- 400 The request isn't valid.
error.codeisvalidation_errororinvalid_request, anderror.messagesays what to change. - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The key doesn't have
contacts:write(forbidden), or adding them would go over your plan's people limit (plan_limit_reached). - 409 A request with this Idempotency-Key is still running (
idempotency_in_progress). Retry after theRetry-Afterheader's 5 seconds. - 413 The body is over the size limit (
payload_too_large). - 422 This Idempotency-Key was used for a different request (
idempotency_key_reused). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
Person | A person, as every People endpoint returns them. |
meta |
object | |
meta.created |
boolean | True when they're new. |
meta.subscribed |
boolean | True when this request subscribed them. |
meta.suppressed |
boolean | True when the address is stopped, so it wasn't subscribed. |
meta.keptOptOut |
boolean | True when they had unsubscribed and still are, because resubscribe wasn't true. |
warnings |
array of string | Notes for you, such as a kept opt-out or details that were skipped. The field is left out when there are none. |
curl -X POST "https://splashsend.com/api/v1/contacts" \
-H "Authorization: Bearer sk_live_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"email": "sam@example.com",
"firstName": "Sam",
"lastName": "Lee",
"source": "website",
"properties": {
"Birthday": "1990-05-15"
},
"preferences": [
"Newsletter"
]
}'
/api/v1/contacts/importImport people
Needs the contacts:write scope.
Adds or updates up to 500 people in one request, for a migration or a backfill.
Someone new is added subscribed, with consent recorded, unless their row says "subscriptionStatus": "unsubscribed", for example someone who had opted out of your old email service. Someone already here keeps their subscription status and source, so an import never brings back someone who unsubscribed. Their name and details are updated from the row.
Automations are off unless you send "startAutomations": true. Without it nobody starts a welcome drip or a drip that watches a detail or a list, and contact.subscribed isn't sent.
Each row is handled on its own. A row that fails is listed in meta.errors and the rest carry on. If your plan's people limit is reached, the import stops: meta.planLimitReached is true, and meta.stoppedAtIndex is the row it stopped at. That row and the rows after it weren't added. To add everyone to a list, send listId, which needs lists:write as well.
Accepts an Idempotency-Key header. Send a new key with each new request and the same key when you retry it: a retry gets the first response back instead of acting twice.
| Field | Type | |
|---|---|---|
contactsrequired |
array of ImportContactInput | The people, up to 500. max items: 500 |
startAutomations |
boolean | Start welcome drips, drips that watch a detail or a list, and contact.subscribed for these people. Leave it out for people from your records. default: false |
listId |
string | Add everyone imported to this list. Needs lists:write. |
source |
string | The source for someone new whose row has none. Defaults to api_import. |
- 200 The import ran. Rows that failed are listed in
meta.errors. - 400 The request isn't valid.
error.codeisvalidation_errororinvalid_request, anderror.messagesays what to change. - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The key doesn't have
contacts:write, or you sentlistIdand it doesn't havelists:write. - 404 The list in
listIdisn't in your account. - 409 A request with this Idempotency-Key is still running (
idempotency_in_progress). Retry after theRetry-Afterheader's 5 seconds. - 413 The body is over the size limit (
payload_too_large). - 422 This Idempotency-Key was used for a different request (
idempotency_key_reused). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
object | |
data.created |
integer | People added. |
data.updated |
integer | People already here, updated. |
data.failed |
integer | Rows that weren't saved. |
data.addedToList |
integer | People added to the list. The field is there only when you sent listId. |
meta |
object | |
meta.startAutomations |
boolean | Whether automations ran. |
meta.errors |
array of object | Up to 50 rows that failed. |
meta.errors[].index |
integer | The row's position in contacts, from 0. |
meta.errors[].email |
string | The row's email address, when it had one. |
meta.errors[].message |
string | Why it failed. |
meta.unknownProperties |
array of string | Detail names that aren't in your account, up to 50. |
meta.planLimitReached |
boolean | True when the import stopped at your plan's people limit. |
meta.stoppedAtIndex |
integer | The row the import stopped at. It and the rows after it weren't added. |
warnings |
array of string | Notes about details that were skipped, or a list that couldn't be updated. The field is left out when there are none. |
curl -X POST "https://splashsend.com/api/v1/contacts/import" \
-H "Authorization: Bearer sk_live_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"contacts": [
{
"email": "sam@example.com",
"firstName": "Sam",
"lastName": "Lee"
},
{
"email": "alex@example.com",
"name": "Alex Morgan",
"subscriptionStatus": "unsubscribed"
}
],
"source": "old_platform"
}'
/api/v1/contacts/{id}Get a person
Needs the contacts:read scope.
Includes their details in properties, keyed by name. A detail with no value is left out.
| Name | Type | |
|---|---|---|
idrequired |
string · path | The person's id. |
- 200 The person.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
PersonWithProperties | A person with their details. |
curl -X GET "https://splashsend.com/api/v1/contacts/cm4u2x8yq0003l208f1k9d7vz" \
-H "Authorization: Bearer sk_live_…"
/api/v1/contacts/{id}Update a person
Needs the contacts:write scope.
Changes only the fields you send.
Setting subscriptionStatus to subscribed subscribes them, records their consent and sends contact.subscribed. It doesn't start welcome drips, so use POST /contacts for a new sign-up. A stopped address can't be subscribed and gets a 409 (stopped_address). Setting it to unsubscribed unsubscribes them and sends contact.unsubscribed.
contact.updated is sent only when their name, source or details changed.
| Name | Type | |
|---|---|---|
idrequired |
string · path | The person's id. |
| Field | Type | |
|---|---|---|
firstName |
string | Their first name. |
lastName |
string | Their last name. |
name |
string | Their full name, split into first and last. Ignored when firstName or lastName is sent. A blank value never clears a stored name. |
source |
string | Where they came from. |
subscriptionStatus |
"subscribed" | "unsubscribed" |
|
properties |
PropertyValues | Details keyed by name, as they're called in SplashSend (any case). |
- 200 The person, with their details.
- 400 The request isn't valid.
error.codeisvalidation_errororinvalid_request, anderror.messagesays what to change. - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 409 The address is stopped, so it can't be subscribed (
stopped_address). - 413 The body is over the size limit (
payload_too_large). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
PersonWithProperties | A person with their details. |
warnings |
array of string | Notes about anything that was skipped. The field is left out when there are none. |
curl -X PATCH "https://splashsend.com/api/v1/contacts/cm4u2x8yq0003l208f1k9d7vz" \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Samantha",
"properties": {
"Birthday": "1990-05-15"
}
}'
/api/v1/contacts/{id}Unsubscribe a person
Needs the contacts:write scope.
Unsubscribes the person and sends contact.unsubscribed. Nothing is deleted: they stay in your account with their history. Someone already unsubscribed is left as they are.
| Name | Type | |
|---|---|---|
idrequired |
string · path | The person's id. |
- 200 The person, now unsubscribed.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
Person | A person, as every People endpoint returns them. |
curl -X DELETE "https://splashsend.com/api/v1/contacts/cm4u2x8yq0003l208f1k9d7vz" \
-H "Authorization: Bearer sk_live_…"
/api/v1/contacts/{id}/propertiesList a person's details
Needs the contacts:read scope.
Every detail in your account, with this person's value. A detail they have no value for has "".
| Name | Type | |
|---|---|---|
idrequired |
string · path | The person's id. |
- 200 Their details.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
array of PropertyValue |
curl -X GET "https://splashsend.com/api/v1/contacts/cm4u2x8yq0003l208f1k9d7vz/properties" \
-H "Authorization: Bearer sk_live_…"
/api/v1/contacts/{id}/propertiesSet a person's details
Needs the contacts:write scope.
Sets the details you send and leaves the others as they are. A name that isn't a detail in your account is skipped with a warning, and if none of the names are details, the request gets a 400. contact.updated is sent when a value changed, and drips that watch a detail start when its value changes.
| Name | Type | |
|---|---|---|
idrequired |
string · path | The person's id. |
| Field | Type | |
|---|---|---|
propertiesrequired |
PropertyValues | Details keyed by name, as they're called in SplashSend (any case). |
- 200 Their details.
- 400 The request isn't valid.
error.codeisvalidation_errororinvalid_request, anderror.messagesays what to change. - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 413 The body is over the size limit (
payload_too_large). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
array of PropertyValue | |
warnings |
array of string | Notes about anything that was skipped. The field is left out when there are none. |
curl -X PUT "https://splashsend.com/api/v1/contacts/cm4u2x8yq0003l208f1k9d7vz/properties" \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"properties": {
"Birthday": "1990-05-15"
}
}'
/api/v1/contacts/{id}/profileGet a person's profile
Needs the contacts:read scope.
The person with their details, their consent for email and text messages, how their emails did, what happened to them recently, their orders and the data-set rows matched to them, in one request.
Their emails are counted the way Reports counts them: an email sent again after a failed try counts once, and a click made by link-checking software isn't counted.
What happened to them also needs events:read, their orders orders:read, their data-set rows data:read, and the names of lists that stop mail reaching them lists:read. What a key can't see is left out, and meta.left_out names it.
| Name | Type | |
|---|---|---|
idrequired |
string · path | The person's id. |
- 200 Their profile.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
Profile | A person with everything SplashSend knows about them. |
meta |
object | |
meta.left_out |
array of string | The parts of the profile, and the events, left out because the key can't see them. The field is left out when there are none. |
curl -X GET "https://splashsend.com/api/v1/contacts/cm4u2x8yq0003l208f1k9d7vz/profile" \
-H "Authorization: Bearer sk_live_…"
Lists
Lists you add people to yourself. A list set to never be emailed (blockSending) keeps everyone on it out of every send, without changing their subscriptions.
/api/v1/listsList your lists
Needs the lists:read scope.
Newest first.
| Name | Type | |
|---|---|---|
page |
integer · query | The page to read, from 1. default: 1 minimum: 1 |
limit |
integer · query | How many to read on a page, up to 100. default: 25 minimum: 1 maximum: 100 |
search |
string · query | Only lists whose name or description contains this. |
- 200 A page of lists.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 429 The key made more than 100 requests in a minute (
rate_limited).
curl -X GET "https://splashsend.com/api/v1/lists" \
-H "Authorization: Bearer sk_live_…"
/api/v1/listsCreate a list
Needs the lists:write scope.
Creates a list you add people to yourself. Send "blockSending": true to make it a "Never email these people" list from the start. An account can have up to 500 lists.
Accepts an Idempotency-Key header. Send a new key with each new request and the same key when you retry it: a retry gets the first response back instead of acting twice.
| Field | Type | |
|---|---|---|
namerequired |
string | max length: 120 |
description |
string or null | Send null to clear it. max length: 500 |
blockSending |
boolean | Makes it a "Never email these people" list. Take someone off the list and they can be emailed again as before. |
- 201 Created.
- 400 The request isn't valid, or the account already has 500 lists (
limit_reached). - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 409 A request with this Idempotency-Key is still running (
idempotency_in_progress). Retry after theRetry-Afterheader's 5 seconds. - 413 The body is over the size limit (
payload_too_large). - 422 This Idempotency-Key was used for a different request (
idempotency_key_reused). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
List | A list you add people to yourself. |
curl -X POST "https://splashsend.com/api/v1/lists" \
-H "Authorization: Bearer sk_live_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"name": "Workshop regulars",
"description": "People who have been to three or more workshops."
}'
/api/v1/lists/{id}Get a list
Needs the lists:read scope.
Includes how many people are on it.
| Name | Type | |
|---|---|---|
idrequired |
string · path | The list's id. |
- 200 The list.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
List | A list you add people to yourself. |
curl -X GET "https://splashsend.com/api/v1/lists/cm4u3b1zt0007l2083n6pq2xa" \
-H "Authorization: Bearer sk_live_…"
/api/v1/lists/{id}Update a list
Needs the lists:write scope.
Changes only the fields you send.
| Name | Type | |
|---|---|---|
idrequired |
string · path | The list's id. |
| Field | Type | |
|---|---|---|
name |
string | max length: 120 |
description |
string or null | Send null to clear it. max length: 500 |
blockSending |
boolean | Makes it a "Never email these people" list. Take someone off the list and they can be emailed again as before. |
- 200 The list, updated.
- 400 The request isn't valid.
error.codeisvalidation_errororinvalid_request, anderror.messagesays what to change. - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 413 The body is over the size limit (
payload_too_large). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
List | A list you add people to yourself. |
curl -X PATCH "https://splashsend.com/api/v1/lists/cm4u3b1zt0007l2083n6pq2xa" \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Workshop regulars 2026"
}'
/api/v1/lists/{id}Delete a list
Needs the lists:write scope.
Deletes the list. The people on it stay in your account, and their subscriptions don't change.
| Name | Type | |
|---|---|---|
idrequired |
string · path | The list's id. |
- 200 Deleted.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
object | |
data.deleted |
true |
curl -X DELETE "https://splashsend.com/api/v1/lists/cm4u3b1zt0007l2083n6pq2xa" \
-H "Authorization: Bearer sk_live_…"
/api/v1/lists/{id}/membersList a list's members
Needs the lists:read scope.
Newest additions first. A member's email and name are included only for a key with contacts:read.
| Name | Type | |
|---|---|---|
idrequired |
string · path | The list's id. |
page |
integer · query | The page to read, from 1. default: 1 minimum: 1 |
limit |
integer · query | How many to read on a page, up to 100. default: 25 minimum: 1 maximum: 100 |
- 200 A page of members.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
array of ListMember | |
meta |
PageMeta | Paging details for a list. |
curl -X GET "https://splashsend.com/api/v1/lists/cm4u3b1zt0007l2083n6pq2xa/members" \
-H "Authorization: Bearer sk_live_…"
/api/v1/lists/{id}/membersAdd people to a list
Needs the lists:write scope.
Adds people already in your account, by id or email address, up to 500 in one request. Nobody is created here: to add new people and put them on a list in one go, use POST /contacts/import with listId. Ids and addresses that aren't in your account come back in meta. Drips that watch this list start only if you send "startAutomations": true.
| Name | Type | |
|---|---|---|
idrequired |
string · path | The list's id. |
| Field | Type | |
|---|---|---|
contactIds |
array of string | |
emails |
array of string | |
startAutomations |
boolean | Start drips that watch this list for the people added. default: false |
- 200 Added.
- 400 The request isn't valid.
error.codeisvalidation_errororinvalid_request, anderror.messagesays what to change. - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 413 The body is over the size limit (
payload_too_large). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
object | |
data.added |
integer | People added to the list. |
data.alreadyMembers |
integer | People who were on it already. |
meta |
object | |
meta.startAutomations |
boolean | Whether drips that watch this list were started. |
meta.unknownIds |
array of string | Ids that aren't in your account, up to 50. |
meta.unknownEmails |
array of string | Addresses that aren't in your account, up to 50. |
curl -X POST "https://splashsend.com/api/v1/lists/cm4u3b1zt0007l2083n6pq2xa/members" \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"emails": [
"sam@example.com"
],
"contactIds": [
"cm4u2x8yq0004l208t6r3b1nh"
]
}'
/api/v1/lists/{id}/membersRemove people from a list
Needs the lists:write scope.
Takes people off the list, by id or email address. The people and their subscriptions don't change.
| Name | Type | |
|---|---|---|
idrequired |
string · path | The list's id. |
| Field | Type | |
|---|---|---|
contactIds |
array of string | |
emails |
array of string |
- 200 Removed.
- 400 The request isn't valid.
error.codeisvalidation_errororinvalid_request, anderror.messagesays what to change. - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 413 The body is over the size limit (
payload_too_large). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
object | |
data.removed |
integer | People taken off the list. |
meta |
object | |
meta.unknownIds |
array of string | Ids that aren't in your account, up to 50. |
meta.unknownEmails |
array of string | Addresses that aren't in your account, up to 50. |
curl -X DELETE "https://splashsend.com/api/v1/lists/cm4u3b1zt0007l2083n6pq2xa/members" \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"emails": [
"sam@example.com"
]
}'
Events
Things people do, such as a booking or a sign-up, recorded so they can start drips.
/api/v1/eventsList events
Needs the events:read scope.
Newest first. An event's email is included only for a key with contacts:read.
| Name | Type | |
|---|---|---|
page |
integer · query | The page to read, from 1. default: 1 minimum: 1 |
limit |
integer · query | How many to read on a page, up to 100. default: 25 minimum: 1 maximum: 100 |
name |
string · query | Only events with this name. |
email |
string · query | Only this person's events. Needs contacts:read, and without it the request gets a 403. format: email |
- 200 A page of events.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The key doesn't have
events:read, or it filtered byemailwithoutcontacts:read. - 429 The key made more than 100 requests in a minute (
rate_limited).
curl -X GET "https://splashsend.com/api/v1/events" \
-H "Authorization: Bearer sk_live_…"
/api/v1/eventsRecord an event
Needs the events:write scope.
Records something a person did, such as booking a class, and starts any drip whose trigger is an event with this name.
The event is attached to the person at email. Someone new is added unsubscribed, because an event isn't consent to marketing email, and they count towards your plan's people limit.
Drips start for an event from the last 3 days, or one dated ahead. An older event is recorded without starting any, unless you send "startAutomations": true. Send false to record any event without starting drips.
Send external_id so a retried event is recorded once. A second event with the same external_id records nothing and starts nothing, and the response is the first event with meta.duplicate set to true.
Accepts an Idempotency-Key header. Send a new key with each new request and the same key when you retry it: a retry gets the first response back instead of acting twice.
| Field | Type | |
|---|---|---|
emailrequired |
string | The person who did it. format: email |
namerequired |
string | The event's name, such as class_booked. max length: 120 |
properties |
object | Anything else about the event, up to 16KB as JSON. |
occurred_at |
string | When it happened. Defaults to now. format: date-time |
external_id |
string or number | Your id for this event, so a retry is recorded once. |
startAutomations |
boolean | true starts drips for an event of any age. false starts none. |
- 201 Recorded, or found already recorded.
- 400 The request isn't valid.
error.codeisvalidation_errororinvalid_request, anderror.messagesays what to change. - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The key doesn't have
events:write(forbidden), or adding the person would go over your plan's people limit (plan_limit_reached). - 409 A request with this Idempotency-Key is still running (
idempotency_in_progress). Retry after theRetry-Afterheader's 5 seconds. - 413 The body is over the size limit (
payload_too_large). - 422 This Idempotency-Key was used for a different request (
idempotency_key_reused). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
object | |
data.id |
string | The event's id. |
data.name |
string | |
data.email |
string | The person it's attached to. For a duplicate that belongs to a different address, it's left out unless the key has contacts:read. format: email |
data.occurred_at |
string | format: date-time |
data.external_id |
string or null | |
meta |
object | |
meta.contact_created |
boolean | True when this event added the person. |
meta.duplicate |
boolean | True when an event with this external_id was already recorded. Nothing new was stored. |
meta.automations_started |
boolean | True when drips with this event as their trigger were started, if there are any. |
curl -X POST "https://splashsend.com/api/v1/events" \
-H "Authorization: Bearer sk_live_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"email": "sam@example.com",
"name": "class_booked",
"properties": {
"class": "Beginners wheel",
"seats": 2
},
"external_id": "booking-10482"
}'
Orders
Orders from a shop that isn't connected through Shopify.
/api/v1/ordersList orders
Needs the orders:read scope.
Orders sent in with POST /orders, newest order first. email is included only for a key with contacts:read.
| Name | Type | |
|---|---|---|
page |
integer · query | The page to read, from 1. default: 1 minimum: 1 |
limit |
integer · query | How many to read on a page, up to 100. default: 25 minimum: 1 maximum: 100 |
- 200 A page of orders.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 429 The key made more than 100 requests in a minute (
rate_limited).
curl -X GET "https://splashsend.com/api/v1/orders" \
-H "Authorization: Bearer sk_live_…"
/api/v1/ordersAdd or update an order
Needs the orders:write scope.
Records an order from your shop by order_id. To update it, send the same order_id again with only what changed.
A buyer new to your account is added as a customer who can't be emailed, unless accepts_marketing is true because they agreed to marketing email at checkout. A buyer already here keeps their subscription status. If your plan's people limit is reached, the order is saved without a person and meta.warning says so.
When this order subscribes a new buyer, their welcome drips start and contact.subscribed is sent if the order is from the last 3 days. Send "startAutomations": true to start them for an older order, or false to never start them.
Drips for an order placed, a first order or a repeat order run once, when a new order from the last 3 days arrives, and order.placed is sent then. "Order shipped" drips run, and order.fulfilled is sent, when the order moves into a shipped status (fulfilled, completed, complete, shipped or delivered) and is from the last 30 days.
Accepts an Idempotency-Key header. Send a new key with each new request and the same key when you retry it: a retry gets the first response back instead of acting twice.
| Field | Type | |
|---|---|---|
order_idrequired |
string or number | Your order number or id. |
email |
string | The buyer's email address. Needed for a new order. format: email |
total |
number | The order's total. Needed for a new order. minimum: 0 |
currency |
string | A three-letter currency code, such as GBP. Defaults to USD. |
items |
array of object | What was bought. Each item's name, or title, names it in review emails. |
status |
string | The order's status in your shop. fulfilled, completed, complete, shipped and delivered count as shipped. |
order_date |
string | When it was placed. Defaults to now. format: date-time |
source |
string | Where it came from, such as your shop's name. Defaults to woocommerce. |
accepts_marketing |
boolean | True when the buyer agreed to marketing email at checkout. Used only when this order adds them. default: false |
startAutomations |
boolean | For a buyer this order subscribes: true starts their welcome drips for an order of any age, and false never starts them. |
- 201 Saved.
- 400 The request isn't valid.
error.codeisvalidation_errororinvalid_request, anderror.messagesays what to change. - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 409 A request with this Idempotency-Key is still running (
idempotency_in_progress). Retry after theRetry-Afterheader's 5 seconds. - 413 The body is over the size limit (
payload_too_large). - 422 This Idempotency-Key was used for a different request (
idempotency_key_reused). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
object | |
data.id |
string | The order's id in SplashSend. |
data.order_id |
string | Your order number or id. |
data.email |
string | Sent when the request included it, or the key has contacts:read. format: email |
data.total |
number | |
data.currency |
string | |
data.status |
string or null | |
data.order_date |
string | format: date-time |
data.contact_id |
string or null | The buyer's person id. Null when the order has no person. |
meta |
object | |
meta.contact_created |
boolean | True when this order added the buyer. |
meta.warning |
string | Present when your plan's people limit kept the order from being linked to a person. |
curl -X POST "https://splashsend.com/api/v1/orders" \
-H "Authorization: Bearer sk_live_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"order_id": "1042",
"email": "sam@example.com",
"total": 42.5,
"currency": "GBP",
"items": [
{
"name": "Speckled mug",
"quantity": 2,
"price": 18
}
],
"status": "processing",
"order_date": "2026-10-09T10:30:00Z"
}'
Activity
Everything that happened in your account, read in one place: SplashSend's own events and the events your systems send. SplashSend's own use the same names as the webhooks.
The events SplashSend records:
- email.sent (Email sent): An email to a person, counted once however many tries it took. Its status says how it went: delivered, bounced, failed when no try got through, or sent while there's no word back yet. Needs contacts:read.
- email.delivered (Email delivered): An email that neither failed nor bounced. The full list shows it as email.sent with the status delivered; ask for email.delivered by name to list just these. Needs contacts:read.
- email.failed (Email couldn't be sent): An email no try got through. The full list shows it as email.sent with the status failed; ask for email.failed by name to list just these. Needs contacts:read.
- email.bounced (Email bounced): An email the receiving server turned away. The full list shows it as email.sent with the status bounced; ask for email.bounced by name to list just these. Needs contacts:read.
- email.opened (Email opened): Each open as recorded, so an email opened twice has two. Reports counts an email as opened once. Needs contacts:read.
- email.clicked (Link clicked): Each click a person made. Link-checking software opens every link the moment an email arrives, and those clicks aren't counted. Needs contacts:read.
- email.complained (Marked as spam): A spam report from the person's email provider. Needs contacts:read.
- contact.subscribed (Person subscribed): Subscribing, or subscribing again, with how they did it. Reports leaves out imports and customers brought in from a shop's history, since those record people arriving in bulk. Needs contacts:read.
- contact.unsubscribed (Person unsubscribed): Leaving by any route, from the unsubscribe link to a spam report. Needs contacts:read.
- contact.suppressed (Emails stopped): A person's emails stopped by hand or after a bounce. Needs contacts:read.
- order.placed (Order placed): An order from a connected Shopify store or one sent through POST /orders. One refunded in full, voided, cancelled or whose payment failed is marked reversed and isn't revenue. Needs orders:read.
- checkout.abandoned (Checkout left unfinished): A Shopify checkout with an email address that wasn't finished, and whether they bought later. Needs orders:read.
- viewed_product (Viewed a product): Recorded by the website script for someone who came from one of your emails.
- added_to_cart (Added to basket): Recorded by the website script for someone who came from one of your emails.
- searched (Searched the site): Recorded by the website script. A search that looks like an address or a long number isn't kept.
- viewed_collection (Viewed a collection): Recorded by the website script for someone who came from one of your emails.
- form.submitted (Form submitted): An answer to one of your forms. Answers marked as spam aren't counted. Webhooks send a Collect answer as submission.created. Needs contacts:read.
- drip.enrolled (Started a drip): A person starting a drip. Someone who goes through a drip again is shown at their latest start. Needs contacts:read.
- back_in_stock.requested (Asked to hear when it's back): A request made through a form to hear when a product is back in stock. Needs contacts:read.
/api/v1/activityList what happened
Needs the events:read scope.
Everything that happened, newest first, or what happened to one person with email. Each event says whether it's one of SplashSend's own (kind: built_in) or one your systems sent (kind: custom), so an event you named order.placed yourself is never mistaken for an order.
An email is listed once, as email.sent with its status, however many tries it took. Ask for email.delivered, email.bounced or email.failed by name to list the emails with that outcome under that name. A click made by link-checking software isn't listed.
Events about a person (emails, joining and leaving, form answers, drips started and back in stock requests) need contacts:read, and orders and unfinished checkouts need orders:read. What a key can't see is left out, and meta.left_out names it. An event's email is sent only to a key with contacts:read.
To page, send the meta.next_cursor of one page as cursor to get the next. It's null on the last page.
| Name | Type | |
|---|---|---|
limit |
integer · query | How many to read on a page, up to 100. default: 25 minimum: 1 maximum: 100 |
cursor |
string · query | Where the last page ended: its meta.next_cursor. |
name |
string · query | Only this event: one from GET /activity/catalogue, such as email.clicked, or one your systems send. |
kind |
"built_in" | "custom" · query |
built_in for SplashSend's own events, custom for the ones your systems send. |
email |
string · query | Only this person's events. Needs contacts:read, and without it the request gets a 403. format: email |
since |
string · query | From this time: a date such as 2026-10-01 or a time such as 2026-10-01T09:00:00Z. |
until |
string · query | Up to this time, not including it. |
- 200 A page of what happened.
- 400 The request isn't valid.
error.codeisvalidation_errororinvalid_request, anderror.messagesays what to change. - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The key doesn't have
events:read, or it filtered byemailwithoutcontacts:read. - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
array of ActivityEvent | |
meta |
object | |
meta.limit |
integer | How many fit on a page. |
meta.next_cursor |
string or null | Send as cursor for the next page. Null on the last page. |
meta.left_out |
array of string | Events left out because the key can't see them. The field is left out when there are none. |
curl -X GET "https://splashsend.com/api/v1/activity" \
-H "Authorization: Bearer sk_live_…"
/api/v1/activity/countCount an event
Needs the events:read scope.
How many times an event happened, for the whole account or one person, over a stretch of time, counted the way GET /activity lists it: an email sent again after a failed try counts once, and a click made by link-checking software isn't counted. For a name that is both one of SplashSend's events and one your systems send, kind=custom counts yours.
| Name | Type | |
|---|---|---|
namerequired |
string · query | The event to count: one from GET /activity/catalogue, or one your systems send. |
kind |
"built_in" | "custom" · query |
built_in for SplashSend's own events, custom for the ones your systems send. |
email |
string · query | Only this person's events. Needs contacts:read, and without it the request gets a 403. format: email |
since |
string · query | From this time: a date such as 2026-10-01 or a time such as 2026-10-01T09:00:00Z. |
until |
string · query | Up to this time, not including it. |
- 200 The count.
- 400 The request isn't valid.
error.codeisvalidation_errororinvalid_request, anderror.messagesays what to change. - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The key doesn't have
events:read, it counted an event about a person or filtered byemailwithoutcontacts:read, or it counted orders or unfinished checkouts withoutorders:read. - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
object | |
data.name |
string | |
data.kind |
"built_in" | "custom" |
Which events were counted. |
data.count |
integer | |
data.since |
string or null | format: date-time |
data.until |
string or null | format: date-time |
data.email |
string | The person counted for, when you asked for one. format: email |
curl -X GET "https://splashsend.com/api/v1/activity/count" \
-H "Authorization: Bearer sk_live_…"
/api/v1/activity/catalogueThe events SplashSend records
Needs the events:read scope.
Every kind of event SplashSend records: its name, what it carries in properties, how it's counted, and any scope needed beyond events:read to see it.
- 200 The catalogue.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
array of CatalogueEvent |
curl -X GET "https://splashsend.com/api/v1/activity/catalogue" \
-H "Authorization: Bearer sk_live_…"
Data sets
Facts your business keeps that aren't about one person, such as your catalogue, form answers or a timetable. A set you keep yourself takes rows over the API.
/api/v1/dataList data sets
Needs the data:read scope.
Every data set that's switched on, with its fields. Use it to find a set's key.
- 200 The data sets.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data_sets |
array of DataSetSummary |
curl -X GET "https://splashsend.com/api/v1/data" \
-H "Authorization: Bearer sk_live_…"
/api/v1/dataCreate a data set
Needs the data:write scope.
Creates a data set you keep yourself and send rows to. An account can have up to 100 data sets.
fields lists its columns, each with a label and a type. Each field gets a key made from its label, which is what rows and email tags use. A set sent with no fields gets one, called "Name".
audienceMode says who a row is for. everyone, the default, gives everyone the same row. matched gives each person the row that matches one of their details: send matchOn with the person's detail and the set's field. A matched set made without matchOn gives nobody a row until you choose them in the app. internal keeps the set out of emails, for use in groups. A set that picks people by rules is set up in the app.
joins links the set to up to 4 others. Each join pairs one of this set's fields with a field of another set. When a person has a row of this set, the other set's tags read its row with the same value in that field, so a set of shops joined to the weather on the town gives each person the forecast for their own shop. Values match when they're the same apart from capital letters and spaces at either end.
The response is the new set's key.
Accepts an Idempotency-Key header. Send a new key with each new request and the same key when you retry it: a retry gets the first response back instead of acting twice.
| Field | Type | |
|---|---|---|
namerequired |
string | The set's name. Its key is made from it. max length: 80 |
description |
string | max length: 500 |
category |
string | Where it's filed in the app. Defaults to "Kept by you". |
fields |
array of DataFieldInput | Its columns, up to 40. max items: 40 |
audienceMode |
"everyone" | "matched" | "internal" |
Who a row is for. default: "everyone" |
matchOn |
MatchOn | What a matched set matches on. |
joins |
array of DataJoin | Other sets it joins, up to 4. max items: 4 |
- 201 Created.
- 400 The request isn't valid (
invalid_request), or the account already has 100 data sets (limit_reached). - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 409 A request with this Idempotency-Key is still running (
idempotency_in_progress). Retry after theRetry-Afterheader's 5 seconds. - 413 The body is over the size limit (
payload_too_large). - 422 This Idempotency-Key was used for a different request (
idempotency_key_reused). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
key |
string | The new set's key. |
curl -X POST "https://splashsend.com/api/v1/data" \
-H "Authorization: Bearer sk_live_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"name": "Workshops",
"description": "Upcoming workshops and how many places are left.",
"fields": [
{
"label": "Title",
"type": "words"
},
{
"label": "Date",
"type": "date"
},
{
"label": "Price",
"type": "money"
},
{
"label": "Places left",
"type": "number"
}
]
}'
/api/v1/data/{key}Get a data set
Needs the data:read scope.
Its fields, and how many rows it has.
| Name | Type | |
|---|---|---|
keyrequired |
string · path | The data set's key, as GET /data lists it, such as shopify_products. It's also on the set's Source tab in the app. |
- 200 The data set.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
key |
string | Its identifier, used in the API and in email tags. |
name |
string | |
description |
string or null | |
source |
"live" | "manual" |
manual for a set you keep yourself, live for one read from a connected source. |
writable |
boolean | True when it takes rows over the API. |
row_count |
integer | |
updated_at |
string or null | When its rows last changed, when that's known. format: date-time |
fields |
array of DataField | |
joins |
array of DataJoin | The other sets it joins. |
curl -X GET "https://splashsend.com/api/v1/data/workshops" \
-H "Authorization: Bearer sk_live_…"
/api/v1/data/{key}Update a data set
Needs the data:write scope.
Changes the fields you send. name, description, category and matchOn work on any set. fields works only on a set you keep yourself.
fields is the whole new list of columns. A field sent with its key keeps its values, and keeps any label, type or example you leave out. A field you leave out of the list is removed. A field that is removed and later added back starts empty. The field a matched set matches people on, and a field a join uses, can't be removed.
matchOn makes it a matched set and says what it matches on, as on create.
joins works on any set and replaces its joins, as on create. Send an empty list to remove them. A join to a set that has since been switched off can stay, and starts working again when that set is back on.
| Name | Type | |
|---|---|---|
keyrequired |
string · path | The data set's key, as GET /data lists it, such as shopify_products. It's also on the set's Source tab in the app. |
| Field | Type | |
|---|---|---|
name |
string | max length: 80 |
description |
string | max length: 500 |
category |
string | |
fields |
array of DataFieldInput | The whole new list of columns. max items: 40 |
matchOn |
MatchOn | What a matched set matches on. |
joins |
array of DataJoin | The whole new list of joins. max items: 4 |
- 200 The data set, updated.
- 400 The request isn't valid (
invalid_request), or it changes the fields of a set read from a connected source (read_only). - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 413 The body is over the size limit (
payload_too_large). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
key |
string | |
name |
string | |
description |
string or null | |
fields |
array of DataField | |
joins |
array of DataJoin |
curl -X PATCH "https://splashsend.com/api/v1/data/workshops" \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Workshops and classes",
"description": "Upcoming workshops and classes, with places left."
}'
/api/v1/data/{key}Delete a data set
Needs the data:write scope.
Deletes a set you keep yourself, or one added through "Connect an app", with all its rows. Ideas it put on your calendar are removed too. A built-in set can't be deleted: switch it off in the app instead.
| Name | Type | |
|---|---|---|
keyrequired |
string · path | The data set's key, as GET /data lists it, such as shopify_products. It's also on the set's Source tab in the app. |
- 200 Deleted.
- 400 The set is built in (
read_only). - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
deleted |
true |
curl -X DELETE "https://splashsend.com/api/v1/data/workshops" \
-H "Authorization: Bearer sk_live_…"
/api/v1/data/{key}/rowsList a data set's rows
Needs the data:read scope.
Reads the rows of any data set that's switched on. A built-in set is read when you ask, so its rows are current.
Each row's values are keyed by field key. A row in a set you keep yourself also has its id, which you send to update or delete it. A column labelled ID is keyed id_2, so it never hides the row's id.
Some sets leave out rows whose date has passed. Send include_past=true to list those too.
Without contacts:read, columns holding someone's email are left out, and form answers are limited to the ones shown on your site, with first names only.
| Name | Type | |
|---|---|---|
keyrequired |
string · path | The data set's key, as GET /data lists it, such as shopify_products. It's also on the set's Source tab in the app. |
limit |
integer · query | How many rows to read. default: 50 minimum: 1 maximum: 200 |
offset |
integer · query | How many rows to skip. default: 0 minimum: 0 |
include_past |
boolean · query | Include rows whose date has passed. default: false |
- 200 A page of rows.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data_set |
string | The set's key. |
fields |
array of DataField | Its fields. Columns holding email addresses are left out without contacts:read. |
rows |
array of DataRow | |
total |
integer | How many rows there are to read, for paging. |
limit |
integer | |
offset |
integer |
curl -X GET "https://splashsend.com/api/v1/data/workshops/rows" \
-H "Authorization: Bearer sk_live_…"
/api/v1/data/{key}/rowsAdd rows
Needs the data:write scope.
Adds rows to a set you keep yourself, up to 5,000 in one request (4MB). A set holds up to 5,000 rows.
Each row is an object of values. A key can be a field's key, in any case, or its label. Keys that don't match a field are skipped and listed in warnings. A row where no key matches a field is refused with a 400, and so is a request whose rows are all empty.
"mode": "replace" swaps every row in the set for the rows you send. A row that comes back unchanged keeps its id and doesn't start drips waiting for a new row.
A date column is stored as YYYY-MM-DD. A value such as 14/10/2026 or 14 Oct 2026 is read as a date too, day first unless your account's country is the US or Canada.
| Name | Type | |
|---|---|---|
keyrequired |
string · path | The data set's key, as GET /data lists it, such as shopify_products. It's also on the set's Source tab in the app. |
Accepts an Idempotency-Key header. Send a new key with each new request and the same key when you retry it: a retry gets the first response back instead of acting twice.
| Field | Type | |
|---|---|---|
rowsrequired |
array of RowValues | The rows, each an object of values. max items: 5000 |
mode |
"append" | "replace" |
append adds the rows after the ones already there. replace swaps every row in the set for these. default: "append" |
- 201 Saved.
- 400 The request isn't valid (
invalid_request), the set is read from a connected source (read_only), or it would hold more than 5,000 rows (limit_reached). - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 409 A request with this Idempotency-Key is still running (
idempotency_in_progress). Retry after theRetry-Afterheader's 5 seconds. - 413 The body is over the size limit (
payload_too_large). - 422 This Idempotency-Key was used for a different request (
idempotency_key_reused). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
added |
integer | How many rows were saved. |
total |
integer | How many rows the set has now. |
mode |
"append" | "replace" |
|
warnings |
array of string | Keys that didn't match a field. The field is left out when there are none. |
curl -X POST "https://splashsend.com/api/v1/data/workshops/rows" \
-H "Authorization: Bearer sk_live_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"rows": [
{
"title": "Wreath making",
"date": "2026-12-05",
"price": 45,
"places_left": 8
},
{
"title": "Beginners wheel",
"date": "2026-12-12",
"price": 60,
"places_left": 4
}
]
}'
/api/v1/data/{key}/rowsUpdate rows
Needs the data:write scope.
Changes up to 500 rows in one request, each found by its id. Send only the values that change, and the rest of the row stays as it is. Keys are matched the same way as when adding rows. An id that isn't in the set is skipped, and updated is how many were found.
| Name | Type | |
|---|---|---|
keyrequired |
string · path | The data set's key, as GET /data lists it, such as shopify_products. It's also on the set's Source tab in the app. |
| Field | Type | |
|---|---|---|
updatesrequired |
array of object | Up to 500 changes. max items: 500 |
updates[].idrequired |
string | The row's id. |
updates[].valuesrequired |
RowValues | A row's values, keyed by a field's key in any case, or by its label. |
- 200 Updated.
- 400 The request isn't valid, sends more than 500 updates, or the set is read from a connected source (
read_only). - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 413 The body is over the size limit (
payload_too_large). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
updated |
integer | How many of the ids were found. |
warnings |
array of string | Keys that didn't match a field. The field is left out when there are none. |
curl -X PATCH "https://splashsend.com/api/v1/data/workshops/rows" \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"updates": [
{
"id": "cm4u5d0re000bl208h4w1s6kc",
"values": {
"places_left": 6
}
}
]
}'
/api/v1/data/{key}/rowsDelete rows
Needs the data:write scope.
Deletes up to 1,000 rows by id. Ids that aren't in the set are ignored. To clear every row, send { "all": true } instead of ids.
| Name | Type | |
|---|---|---|
keyrequired |
string · path | The data set's key, as GET /data lists it, such as shopify_products. It's also on the set's Source tab in the app. |
| Field | Type | |
|---|---|---|
ids |
array of string | The ids of the rows to delete. max items: 1000 |
all |
boolean | Delete every row in the set. Send it only when you mean to empty the set. |
- 200 Deleted.
- 400 The request sends no ids or more than 1,000, or the set is read from a connected source (
read_only). - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 413 The body is over the size limit (
payload_too_large). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
removed |
integer | How many rows were deleted. |
curl -X DELETE "https://splashsend.com/api/v1/data/workshops/rows" \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"ids": [
"cm4u5d0re000bl208h4w1s6kc",
"cm4u5d0re000cl208x9j2v3mn"
]
}'
/api/v1/data/{key}/rows/{rowId}Update a row
Needs the data:write scope.
Changes one row. Send the values that change, inside values or on their own. Keys are matched the same way as when adding rows.
| Name | Type | |
|---|---|---|
keyrequired |
string · path | The data set's key, as GET /data lists it, such as shopify_products. It's also on the set's Source tab in the app. |
rowIdrequired |
string · path | The row's id, from GET /data/{key}/rows. |
| Field | Type | |
|---|---|---|
values |
RowValues | A row's values, keyed by a field's key in any case, or by its label. |
- 200 Updated.
- 400 The request isn't valid.
error.codeisvalidation_errororinvalid_request, anderror.messagesays what to change. - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 413 The body is over the size limit (
payload_too_large). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
updated |
1 |
|
warnings |
array of string | Keys that didn't match a field. The field is left out when there are none. |
curl -X PATCH "https://splashsend.com/api/v1/data/workshops/rows/cm4u5d0re000bl208h4w1s6kc" \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"values": {
"places_left": 6
}
}'
/api/v1/data/{key}/rows/{rowId}Delete a row
Needs the data:write scope.
Deletes one row from a set you keep yourself.
| Name | Type | |
|---|---|---|
keyrequired |
string · path | The data set's key, as GET /data lists it, such as shopify_products. It's also on the set's Source tab in the app. |
rowIdrequired |
string · path | The row's id, from GET /data/{key}/rows. |
- 200 Deleted.
- 400 The set is read from a connected source (
read_only). - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
deleted |
true |
curl -X DELETE "https://splashsend.com/api/v1/data/workshops/rows/cm4u5d0re000bl208h4w1s6kc" \
-H "Authorization: Bearer sk_live_…"
Products
Your product catalogue, for product blocks in emails.
/api/v1/productsList products
Needs the products:read scope.
Products from your Shopify store and products sent with POST /products, newest first.
| Name | Type | |
|---|---|---|
page |
integer · query | The page to read, from 1. default: 1 minimum: 1 |
limit |
integer · query | How many to read on a page, up to 100. default: 25 minimum: 1 maximum: 100 |
- 200 A page of products.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 429 The key made more than 100 requests in a minute (
rate_limited).
curl -X GET "https://splashsend.com/api/v1/products" \
-H "Authorization: Bearer sk_live_…"
/api/v1/productsAdd or update products
Needs the products:write scope.
Adds products to your catalogue, or updates them, by product_id, so they can appear in product blocks in emails. Send one product, or { "products": [ … ] } with up to 100.
An update changes only the fields you send. A field sent as null is cleared, apart from title and handle, which can't be. title is needed only for a new product.
status is active, draft or archived, and only active products appear in emails. Other platforms' words are understood: WooCommerce's publish is active, private and pending are drafts, and trash is archived. A status that isn't recognised is an error for that product.
A product that can't be saved is listed in meta.errors, and the others are saved. If none can be saved, the request gets a 400.
| Field | Type | |
|---|---|---|
product_idrequired |
string or number | Your id for the product. Sending it again updates the product. |
title |
string | Its name. Needed for a new product. |
price |
number or string | Its price, such as 18 or "18.00". |
currency |
string | A currency code, such as GBP. |
image_url |
string | Its picture, as an http(s) address. format: uri |
url |
string | Its page in your shop. Product blocks link here. format: uri |
handle |
string | Its slug in your shop's addresses. Defaults to product_id. |
description |
string | max length: 300 |
product_type |
string | Its type, such as Mugs. |
vendor |
string | Who makes it. |
status |
string | active, draft or archived, in any case. Defaults to active for a new product. |
tags |
string or array of string | Its tags, as an array or as one string. |
inventory |
integer | How many you have in stock. When a product goes from 0 to more, the people who asked to hear when it was back are told (a drip that starts with "They asked to hear when it’s back"). minimum: 0 |
in_stock |
boolean | For a shop that only knows whether it is in stock: true or false. Ignored when inventory is sent. |
| Field | Type | |
|---|---|---|
productsrequired |
array of ProductInput | Up to 100 products. max items: 100 |
- 200 Saved.
- 400 The request isn't valid, or none of the products could be saved. Then
meta.errorslists each reason. - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 413 The body is over the size limit (
payload_too_large). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
object | |
data.upserted |
integer | How many products were saved. |
meta |
object | |
meta.errors |
array of string | Products that couldn't be saved, each with the reason. |
curl -X POST "https://splashsend.com/api/v1/products" \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"product_id": "mug-speckled",
"title": "Speckled mug",
"price": 18,
"currency": "GBP",
"image_url": "https://shop.example.com/images/speckled-mug.jpg",
"url": "https://shop.example.com/products/speckled-mug",
"status": "active"
}'curl -X POST "https://splashsend.com/api/v1/products" \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"products": [
{
"product_id": "mug-speckled",
"title": "Speckled mug",
"price": 18,
"currency": "GBP"
},
{
"product_id": "bowl-ash",
"title": "Ash glaze bowl",
"price": "24.00",
"currency": "GBP",
"status": "draft"
}
]
}'
Groups
Groups pick people by rules and keep themselves up to date. A campaign can go to one.
/api/v1/groupsList groups
Needs the contacts:read scope.
Your groups, pinned ones first. The groups of addresses that bounced or reported spam aren't listed, because nobody in them is emailed. built_in marks the two SplashSend keeps for you, Everyone and Subscribed.
- 200 The groups.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
array of object | |
data[].id |
string | |
data[].name |
string | |
data[].description |
string or null | |
data[].built_in |
boolean | |
data[].created_at |
string | format: date-time |
data[].updated_at |
string | format: date-time |
curl -X GET "https://splashsend.com/api/v1/groups" \
-H "Authorization: Bearer sk_live_…"
Campaigns
Draft a campaign for someone on your team to finish, or send one of your designs to a group, a list or everyone, now or at a time. A send goes through the same checks as the Send button in SplashSend.
/api/v1/designsList designs
Needs the campaigns:write scope.
The designs in your Designs library, most recently changed first, for choosing one to send. subject is the subject line saved with a design, if it has one.
| Name | Type | |
|---|---|---|
page |
integer · query | The page to read, from 1. default: 1 minimum: 1 |
limit |
integer · query | How many to read on a page, up to 100. default: 25 minimum: 1 maximum: 100 |
- 200 A page of designs.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
array of object | |
data[].id |
string | |
data[].name |
string | |
data[].subject |
string or null | |
data[].folder |
string or null | |
data[].created_at |
string | format: date-time |
data[].updated_at |
string | format: date-time |
meta |
PageMeta | Paging details for a list. |
curl -X GET "https://splashsend.com/api/v1/designs" \
-H "Authorization: Bearer sk_live_…"
/api/v1/campaignsDraft or send a campaign
Needs the campaigns:write or campaigns:send scope.
Without send, makes a draft for someone on your team to finish and send in SplashSend. It needs campaigns:write.
With send, sends one of your designs to a group, a list or everyone ("now"), or schedules it (a time). It needs campaigns:send, and design_id, who it goes to and a subject line. It goes through the same checks as the Send button: a verified sender, your plan's sends, the unsubscribe link, and only subscribed people, leaving out stopped addresses, lists set to never be emailed, people who opted out of the campaign's topic and anyone their own email frequency would skip. A send that's refused makes nothing, and the error says why.
The same design can't go to the same group, list or everyone twice within a day, whoever sent it first: that's refused with sent_recently. An app in test mode gets a draft and sends nothing.
Accepts an Idempotency-Key header. Send a new key with each new request and the same key when you retry it: a retry gets the first response back instead of acting twice.
| Field | Type | |
|---|---|---|
name |
string | What the campaign is called in SplashSend. Defaults to the design's name. max length: 200 |
subject |
string | The subject line. Defaults to the one saved with the design. max length: 300 |
preview_text |
string | The line shown after the subject in the inbox. Defaults to the design's own. max length: 150 |
design_id |
string | A design from GET /designs. Needed to send. |
group_id |
string | Send it to a group from GET /groups. |
list_id |
string | Send it to a list from GET /lists. |
everyone |
boolean | Send it to everyone subscribed. |
send |
string | now, or a time to schedule it for, such as 2026-10-24T09:00:00Z. Leave it out for a draft. Needs campaigns:send. |
- 201 Drafted, sent or scheduled.
- 400 The request isn't valid.
error.codeisvalidation_errororinvalid_request, anderror.messagesays what to change. - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 A permission is missing, or your plan's sends for the month are used up (
plan_limit_reached). - 404 The person, list, data set or row isn't in your account (
not_found). - 409 This design went, or is scheduled to go, to the same people within a day (
sent_recently), or the same Idempotency-Key is still being handled. - 413 The body is over the size limit (
payload_too_large). - 422 This Idempotency-Key was used for a different request (
idempotency_key_reused). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
object | |
data.id |
string | |
data.name |
string | |
data.subject |
string or null | |
data.status |
"draft" | "scheduled" | "sending" | "sent" |
|
data.scheduled_at |
string or null | When a scheduled campaign goes. format: date-time |
data.edit_url |
string | Where someone on your team opens it. format: uri |
meta |
object | |
meta.sent |
boolean | True when it's on its way. |
meta.scheduled |
boolean | |
meta.test_mode |
boolean | Present when the app is in test mode and nothing was sent. |
meta.note |
string |
curl -X POST "https://splashsend.com/api/v1/campaigns" \
-H "Authorization: Bearer sk_live_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"design_id": "cm4u2xi5l0012l208u7i9o1pa",
"group_id": "cm4u2xh4k0011l208y6u8i0op",
"subject": "Three new glazes are in",
"send": "now"
}'
Webhook subscriptions
An app's own subscriptions to the account's events, so it hears of them as they happen (REST hooks, as Zapier uses). Messages are the same as Setup → Webhooks sends, signed with the subscription's own secret, which is shown once, when it's made. An app may subscribe to an event it could read through GET /activity, so most events need contacts:read. A 410 Gone from the address ends the subscription, and removing the app ends all of its subscriptions. An app sees and ends only its own, and the account sees each one in Setup → Webhooks, marked as added by an app.
/api/v1/webhooksList this app's subscriptions
The subscriptions this app holds on this account, newest first. active is false while one is paused, after a day of failed deliveries.
- 200 The subscriptions.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
array of Subscription |
curl -X GET "https://splashsend.com/api/v1/webhooks" \
-H "Authorization: Bearer sk_live_…"
/api/v1/webhooksSubscribe to events
Sends each of these events to url as it happens. event_name narrows event.recorded to one of your own events by name, such as Class booked. step_note narrows drip.step to the Notify your systems steps with that note, matched without regard to capital letters. Each narrows only its own event. The answer holds the signing secret, the only time it's shown. An app may hold 50 subscriptions on an account.
Accepts an Idempotency-Key header. Send a new key with each new request and the same key when you retry it: a retry gets the first response back instead of acting twice.
| Field | Type | |
|---|---|---|
urlrequired |
string | An https address. Events can't be sent to internal or private addresses. format: uri |
eventsrequired |
array of "contact.subscribed" | "contact.unsubscribed" | "contact.updated" | "campaign.sent" | "email.delivered" | "email.opened" | "email.clicked" | "email.bounced" | "email.complained" | "order.placed" | "order.fulfilled" | "submission.created" | "drip.enrolled" | "drip.step" | "form.submitted" | "event.recorded" |
The events to send, as Webhooks lists them. |
event_name |
string | Only this one of your own events, with event.recorded. |
step_note |
string | Only Notify your systems steps with this note, with drip.step. max length: 500 |
- 201 Subscribed.
- 400 The request isn't valid.
error.codeisvalidation_errororinvalid_request, anderror.messagesays what to change. - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 409 A request with this Idempotency-Key is still running (
idempotency_in_progress). Retry after theRetry-Afterheader's 5 seconds. - 413 The body is over the size limit (
payload_too_large). - 422 This Idempotency-Key was used for a different request (
idempotency_key_reused). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
Subscription |
curl -X POST "https://splashsend.com/api/v1/webhooks" \
-H "Authorization: Bearer sk_live_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/splashsend/123",
"events": [
"event.recorded"
],
"event_name": "Class booked"
}'
/api/v1/webhooks/{id}Read a subscription
| Name | Type | |
|---|---|---|
idrequired |
string · path | The subscription's id. |
- 200 The subscription.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
Subscription | One of an app's webhook subscriptions. |
curl -X GET "https://splashsend.com/api/v1/webhooks/ID" \
-H "Authorization: Bearer sk_live_…"
/api/v1/webhooks/{id}End a subscription
Nothing more is sent to it, and its delivery log goes with it.
| Name | Type | |
|---|---|---|
idrequired |
string · path | The subscription's id. |
- 200 Ended.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
object | |
data.id |
string | |
data.deleted |
true |
curl -X DELETE "https://splashsend.com/api/v1/webhooks/ID" \
-H "Authorization: Bearer sk_live_…"
Report cards
Cards an app adds to Reports from the events it sends. A card counts one event, or adds up one of its numbers, over the dates a dashboard shows, split by day, by week or by one of the event's details. The figures are worked out when the account looks at them, and the app never sees them.
Reports lists an app's cards under From your apps, and the account chooses which dashboards they go on. Only the app can change its cards, and they go when the app is removed. An app may keep 20 cards on an account.
/api/v1/report-cardsList this app's report cards
Needs the events:write scope.
- 200 The cards, oldest first.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
array of ReportCard |
curl -X GET "https://splashsend.com/api/v1/report-cards" \
-H "Authorization: Bearer sk_live_…"
/api/v1/report-cards/{key}Read a report card
Needs the events:write scope.
| Name | Type | |
|---|---|---|
keyrequired |
string · path | The app's own key for the card. |
- 200 The card.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
ReportCard | One of this app's report cards. |
curl -X GET "https://splashsend.com/api/v1/report-cards/classes-by-week" \
-H "Authorization: Bearer sk_live_…"
/api/v1/report-cards/{key}Make or replace a report card
Needs the events:write scope.
Makes the card, or replaces this app's card with this key. Sending the same card again changes nothing, so it's safe to send each time the app starts.
event is the name of one of your events. A card counts them, or with sum adds up that number from each one (as money with money). split is none, day, week (the default) or property, which splits by the event detail named in property. chart is number for a card with no split, line for one split by day or week, or bars or table for any split.
| Name | Type | |
|---|---|---|
keyrequired |
string · path | The app's own key for the card. |
| Field | Type | |
|---|---|---|
namerequired |
string | Its title in Reports. max length: 80 |
description |
string | A line under the title saying what it shows. max length: 200 |
eventrequired |
string | The name of the event it reads, as you send it to POST /events. max length: 120 |
split |
"none" | "day" | "week" | "property" |
How its figure is split. default: "week" |
property |
string | With split: property: the event detail to split by, such as class. Events sent without it are under Not given. max length: 60 |
sum |
string | Add up this number from each event, such as price, instead of counting them. An event without a number there adds nothing. max length: 60 |
money |
boolean | With sum: show the total as money, in the account's currency. default: false |
chart |
"number" | "line" | "bars" | "table" |
How it's drawn. Defaults to number with no split, line by day or week, and bars by a detail. |
- 200 Replaced.
- 201 Made.
- 400 The card isn't valid (
invalid_request), or the app already keeps 20 cards on the account (limit_reached). - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 413 The body is over the size limit (
payload_too_large). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
ReportCard | One of this app's report cards. |
curl -X PUT "https://splashsend.com/api/v1/report-cards/classes-by-week" \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Classes booked, by class",
"description": "Bookings made through Hearth in these dates.",
"event": "Class booked",
"split": "property",
"property": "class",
"chart": "bars"
}'
/api/v1/report-cards/{key}Remove a report card
Needs the events:write scope.
It comes off every dashboard it's on.
| Name | Type | |
|---|---|---|
keyrequired |
string · path | The app's own key for the card. |
- 200 Removed.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
object | |
data.deleted |
true |
curl -X DELETE "https://splashsend.com/api/v1/report-cards/classes-by-week" \
-H "Authorization: Bearer sk_live_…"
Drip steps
Steps an app adds to the account's drip builder. Each is listed under From your apps when someone adds a step, with the app's name, and asks for the settings the app wants. When a person reaches it, the app hears a drip.step webhook about them, with the step's key and the settings, through its own subscription (POST /webhooks). Nothing else hears it.
Needs events:read and contacts:read, what hearing drip.step needs. Only the app can change its steps, and they go when the app is removed. An app may keep 20 steps on an account.
/api/v1/drip-stepsList this app's drip steps
Needs the events:read and contacts:read scopes.
- 200 The steps, oldest first.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
array of DripStep |
curl -X GET "https://splashsend.com/api/v1/drip-steps" \
-H "Authorization: Bearer sk_live_…"
/api/v1/drip-steps/{key}Read a drip step
Needs the events:read and contacts:read scopes.
| Name | Type | |
|---|---|---|
keyrequired |
string · path | The app's own key for the step. |
- 200 The step.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
DripStep | One of this app's drip steps. |
curl -X GET "https://splashsend.com/api/v1/drip-steps/offer-next-class" \
-H "Authorization: Bearer sk_live_…"
/api/v1/drip-steps/{key}Make or replace a drip step
Needs the events:read and contacts:read scopes.
Makes the step, or replaces this app's step with this key. Sending the same step again changes nothing, so it's safe to send each time the app starts.
fields are the settings the account fills in on the step, up to 6, each sent in values when someone reaches it. To hear the step, subscribe to drip.step with POST /webhooks.
| Name | Type | |
|---|---|---|
keyrequired |
string · path | The app's own key for the step. |
| Field | Type | |
|---|---|---|
namerequired |
string | Its name in the drip builder. max length: 60 |
description |
string | What it does, shown when it's chosen. max length: 200 |
fields |
array of StepField | max items: 6 |
- 200 Replaced.
- 201 Made.
- 400 The step isn't valid (
invalid_request), or the app already keeps 20 steps on the account (limit_reached). - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 413 The body is over the size limit (
payload_too_large). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
DripStep | One of this app's drip steps. |
curl -X PUT "https://splashsend.com/api/v1/drip-steps/offer-next-class" \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Offer the next class",
"description": "Hearth offers them a place on the next class of the kind you choose.",
"fields": [
{
"key": "class_type",
"label": "Kind of class",
"type": "choice",
"options": [
"Wheel throwing",
"Glazing",
"Hand building"
],
"required": true
},
{
"key": "discount",
"label": "Discount (%)",
"type": "number"
}
]
}'
/api/v1/drip-steps/{key}Remove a drip step
Needs the events:read and contacts:read scopes.
It leaves the builder's list. A drip that already uses it sends nothing at that step, and the person carries on.
| Name | Type | |
|---|---|---|
keyrequired |
string · path | The app's own key for the step. |
- 200 Removed.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
object | |
data.deleted |
true |
curl -X DELETE "https://splashsend.com/api/v1/drip-steps/offer-next-class" \
-H "Authorization: Bearer sk_live_…"
Email blocks
Blocks an app adds to the account's email designer, listed under From your apps. A block shows rows from a data set the app made, as cards with a title, a line, a picture and a button, filled in when the email sends: a set for everyone gives its first rows, and a set matched to people gives each person their own row.
Needs data:write. Only the app can change its blocks, and they go when the app is removed. An app may keep 20 blocks on an account.
/api/v1/email-blocksList this app's email blocks
Needs the data:write scope.
- 200 The blocks, oldest first.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
array of EmailBlock |
curl -X GET "https://splashsend.com/api/v1/email-blocks" \
-H "Authorization: Bearer sk_live_…"
/api/v1/email-blocks/{key}Read an email block
Needs the data:write scope.
| Name | Type | |
|---|---|---|
keyrequired |
string · path | The app's own key for the block. |
- 200 The block.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
EmailBlock | One of this app's email blocks. |
curl -X GET "https://splashsend.com/api/v1/email-blocks/upcoming-classes" \
-H "Authorization: Bearer sk_live_…"
/api/v1/email-blocks/{key}Make or replace an email block
Needs the data:write scope.
Makes the block, or replaces this app's block with this key. data_set is the key of a set this app made (POST /data), switched on and not kept out of emails. title, text, image and link name its fields, by key or label: each card shows the title, a line of text and a picture, and its button opens the link. count is how many cards at most, 1 to 6.
| Name | Type | |
|---|---|---|
keyrequired |
string · path | The app's own key for the block. |
| Field | Type | |
|---|---|---|
namerequired |
string | Its name in the designer. max length: 60 |
description |
string | max length: 200 |
data_setrequired |
string | The key of a data set this app made. |
titlerequired |
string | The field each card's title shows. |
text |
string | The field each card's line of text shows. |
image |
string | The field holding each card's picture address. |
link |
string | The field holding the address each card's button opens. |
button |
string | The words on each card's button. Needs link. Defaults to View. max length: 30 |
count |
integer | How many cards at most. default: 3 minimum: 1 maximum: 6 |
- 200 Replaced.
- 201 Made.
- 400 The block isn't valid (
invalid_request), or the app already keeps 20 blocks on the account (limit_reached). - 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 413 The body is over the size limit (
payload_too_large). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
EmailBlock | One of this app's email blocks. |
curl -X PUT "https://splashsend.com/api/v1/email-blocks/upcoming-classes" \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Upcoming classes",
"description": "The next classes with places left.",
"data_set": "classes",
"title": "Class",
"text": "When",
"image": "Photo",
"link": "Booking link",
"button": "Book a place",
"count": 3
}'
/api/v1/email-blocks/{key}Remove an email block
Needs the data:write scope.
It leaves the designer's list. A design that already uses it shows nothing there when it sends.
| Name | Type | |
|---|---|---|
keyrequired |
string · path | The app's own key for the block. |
- 200 Removed.
- 401 The API key is missing, wrong or revoked (
unauthorized). - 403 The API key doesn't have the scope this endpoint needs (
forbidden). - 404 The person, list, data set or row isn't in your account (
not_found). - 429 The key made more than 100 requests in a minute (
rate_limited).
| Field | Type | |
|---|---|---|
data |
object | |
data.deleted |
true |
curl -X DELETE "https://splashsend.com/api/v1/email-blocks/upcoming-classes" \
-H "Authorization: Bearer sk_live_…"
Webhooks
Events SplashSend sends to your own systems as they happen. Add endpoints in Setup → Webhooks, where each one gets its own signing secret and receives only the events you pick.
Message: a POST with a JSON body of { id, event, timestamp, data }. id (evt_…) stays the same through every retry and any later redelivery, so use it to skip a message you've already handled. timestamp is when this copy was sent, so a redelivery has a new one and a check on its age still passes. data holds ids and plain values, and never a secret.
Signature: X-SplashSend-Signature is the hex HMAC-SHA256 of the raw request body, keyed with the endpoint's signing secret. Compute it over the body exactly as it arrived, and compare the two in constant time. X-SplashSend-Event names the event.
Delivery: reply with any 2xx status to acknowledge a message. A message is tried up to three times: the second try comes 5 seconds after the first fails, and the third 15 seconds after the second. Each try waits up to 10 seconds for a reply. Redirects aren't followed, so a 3xx reply counts as a failure. Up to four messages are sent to an endpoint at once, so they can arrive out of order.
Pausing: an endpoint is paused when every try has failed for 20 messages in a row and the first of those failures was at least a day ago. The account's owner and admins are emailed when that happens. In the endpoint's Deliveries log you can Redeliver one message, or click Send failed ones again to resend everything from the last 30 days that didn't get through. Delivery logs are kept for 30 days.
Email events: every email.* event carries emailSendId, contactId and email, the address the email went to. campaignId is set for a campaign email, dripStepId for a drip email and scheduledEmailId for a Cruise email, and the other two are null.
Names: every event about a person also carries their firstName and lastName, null when they have none.
Apps: an app subscribes to these events with POST /webhooks rather than through Setup. See Webhook subscriptions.
contact.subscribedPerson subscribed
Someone joined your list: through a form, the API, an AI assistant, the app, a new Shopify customer or a checkout opt-in. An import or a store's first sync doesn't send it, unless the import was told to start automations. Some sources also send the person's name. When you need their details, read them with GET /contacts/{id}.
{
"id": "evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s",
"event": "contact.subscribed",
"timestamp": "2026-10-09T10:00:00.000Z",
"data": {
"contactId": "cm4u2x8yq0003l208f1k9d7vz",
"email": "sam@example.com",
"name": "Sam Lee"
}
}
contact.unsubscribedPerson unsubscribed
Someone unsubscribed, from an unsubscribe link, their preferences page, the app, the API or your Shopify store. It's also sent when a spam complaint unsubscribes them.
{
"id": "evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s",
"event": "contact.unsubscribed",
"timestamp": "2026-10-09T10:00:00.000Z",
"data": {
"contactId": "cm4u2x8yq0003l208f1k9d7vz",
"email": "sam@example.com"
}
}
contact.updatedA person's details changed
A person's name, email address, source or details changed. changed lists what changed: name, email, source or a detail's name. Nothing is sent when a save changes nothing.
{
"id": "evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s",
"event": "contact.updated",
"timestamp": "2026-10-09T10:00:00.000Z",
"data": {
"contactId": "cm4u2x8yq0003l208f1k9d7vz",
"email": "sam@example.com",
"changed": [
"name",
"Birthday"
]
}
}
campaign.sentCampaign sent
A campaign finished sending. totalSent is how many emails it sent.
{
"id": "evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s",
"event": "campaign.sent",
"timestamp": "2026-10-09T10:00:00.000Z",
"data": {
"campaignId": "cm4t9k2ab0001qz0870m3fh2c",
"name": "October newsletter",
"totalSent": 1250
}
}
email.deliveredEmail delivered
The receiving mail server accepted an email.
{
"id": "evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s",
"event": "email.delivered",
"timestamp": "2026-10-09T10:00:00.000Z",
"data": {
"emailSendId": "cm4u6p1sd0009l208e7c5x4wy",
"campaignId": "cm4t9k2ab0001qz0870m3fh2c",
"email": "sam@example.com",
"scheduledEmailId": null,
"dripStepId": null,
"contactId": "cm4u2x8yq0003l208f1k9d7vz"
}
}
email.openedEmail opened
Someone opened an email. An open is counted when the email's images load, so some mail apps report opens nobody made and others report none.
{
"id": "evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s",
"event": "email.opened",
"timestamp": "2026-10-09T10:00:00.000Z",
"data": {
"emailSendId": "cm4u6p1sd0009l208e7c5x4wy",
"campaignId": "cm4t9k2ab0001qz0870m3fh2c",
"email": "sam@example.com",
"scheduledEmailId": null,
"dripStepId": null,
"contactId": "cm4u2x8yq0003l208f1k9d7vz"
}
}
email.clickedLink clicked
Someone clicked a link in an email. url is where the link goes, without a form's personal-link token. linkId is the link's name, when you gave it one. Clicks made by link-checking software as the email arrives aren't sent.
{
"id": "evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s",
"event": "email.clicked",
"timestamp": "2026-10-09T10:00:00.000Z",
"data": {
"emailSendId": "cm4u6p1sd0009l208e7c5x4wy",
"campaignId": "cm4t9k2ab0001qz0870m3fh2c",
"email": "sam@example.com",
"scheduledEmailId": null,
"dripStepId": null,
"contactId": "cm4u2x8yq0003l208f1k9d7vz",
"url": "https://shop.example.com/sale",
"linkId": "shop-the-sale"
}
}
email.bouncedEmail bounced
An email bounced. bounceType is hard when the address can't receive email, which stops it at once, or soft for a problem that may pass. Three soft bounces stop an address.
{
"id": "evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s",
"event": "email.bounced",
"timestamp": "2026-10-09T10:00:00.000Z",
"data": {
"emailSendId": "cm4u6p1sd0009l208e7c5x4wy",
"campaignId": "cm4t9k2ab0001qz0870m3fh2c",
"email": "sam@example.com",
"scheduledEmailId": null,
"dripStepId": null,
"contactId": "cm4u2x8yq0003l208f1k9d7vz",
"bounceType": "hard"
}
}
email.complainedMarked as spam
Someone reported an email as spam. They're unsubscribed and their address is stopped. contact.unsubscribed is sent too, if they were subscribed.
{
"id": "evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s",
"event": "email.complained",
"timestamp": "2026-10-09T10:00:00.000Z",
"data": {
"emailSendId": "cm4u6p1sd0009l208e7c5x4wy",
"campaignId": "cm4t9k2ab0001qz0870m3fh2c",
"email": "sam@example.com",
"scheduledEmailId": null,
"dripStepId": null,
"contactId": "cm4u2x8yq0003l208f1k9d7vz"
}
}
order.placedOrder placed
A new order arrived from your Shopify store or POST /orders. It's sent once per order, and only for an order from the last 3 days, so a backfill or a store's first sync doesn't send one for every past order. contactId is null when the order isn't linked to a person.
{
"id": "evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s",
"event": "order.placed",
"timestamp": "2026-10-09T10:00:00.000Z",
"data": {
"orderId": "cm4u7r3ty000dl208k2p8v6nb",
"source": "shopify",
"email": "sam@example.com",
"total": 42.5,
"currency": "GBP",
"orderNumber": "1042",
"contactId": "cm4u2x8yq0003l208f1k9d7vz"
}
}
order.fulfilledOrder shipped
An order moved into a shipped status: fulfilled in Shopify, or a shipped status sent to POST /orders. It's sent for an order from the last 30 days, including a guest order, which has contactId null.
{
"id": "evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s",
"event": "order.fulfilled",
"timestamp": "2026-10-09T10:00:00.000Z",
"data": {
"orderId": "cm4u7r3ty000dl208k2p8v6nb",
"source": "shopify",
"contactId": "cm4u2x8yq0003l208f1k9d7vz",
"email": "sam@example.com"
}
}
submission.createdForm answer / review received
Someone's answers to a form were saved, such as a review or a survey. rating is the score they gave, when the form asks for one. subjectKey names what the answer is about, such as a Shopify product. contactId and email are null for an anonymous answer.
{
"id": "evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s",
"event": "submission.created",
"timestamp": "2026-10-09T10:00:00.000Z",
"data": {
"submissionId": "cm4u8s4uv000fl208q1d7m5xa",
"formId": "cm4s1f5gh0002rt09a6b3n8qk",
"contactId": "cm4u2x8yq0003l208f1k9d7vz",
"email": "sam@example.com",
"rating": 5,
"subjectKey": "gid://shopify/Product/7712345678901"
}
}
drip.enrolledSomeone started a drip
Someone started a drip. trigger is what started it, such as contact_subscribed or custom_event.
{
"id": "evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s",
"event": "drip.enrolled",
"timestamp": "2026-10-09T10:00:00.000Z",
"data": {
"dripId": "cm4r0d2ef0004wx07c9n1h6ps",
"dripName": "Welcome series",
"contactId": "cm4u2x8yq0003l208f1k9d7vz",
"trigger": "contact_subscribed"
}
}
drip.stepA drip reached a Notify your systems step
A drip reached a "Notify your systems" step for someone. note is the text typed on the step, when there is one. A step an app added (PUT /drip-steps/{key}) carries step, its key and name, and values, the settings the account filled in, and goes only to that app's own subscriptions.
{
"id": "evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s",
"event": "drip.step",
"timestamp": "2026-10-09T10:00:00.000Z",
"data": {
"dripId": "cm4r0d2ef0004wx07c9n1h6ps",
"dripName": "Welcome series",
"stepId": "cm4r0d2ef0006wx07z3k8j1tg",
"contactId": "cm4u2x8yq0003l208f1k9d7vz",
"email": "sam@example.com",
"note": "Add to the CRM"
}
}
form.submittedForm submitted
Someone submitted a form that has a "Send it to another tool" action. It's sent once per submission. answers holds only the form's own questions, keyed by each question's id.
{
"id": "evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s",
"event": "form.submitted",
"timestamp": "2026-10-09T10:00:00.000Z",
"data": {
"formId": "cm4s1f5gh0002rt09a6b3n8qk",
"formName": "Feedback",
"contactId": "cm4u2x8yq0003l208f1k9d7vz",
"email": "sam@example.com",
"name": "Sam Lee",
"rating": 5,
"subjectKey": null,
"answers": {
"rating": 5,
"review": "Loved the class"
}
}
}
event.recordedAn event your systems sent
One of your own events was recorded through POST /events or by an app, and it happened in the last three days: older history sent in later isn't sent. name is the event's name and properties what came with it. Your website's page views and form answers aren't sent here, since they have events of their own.
{
"id": "evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s",
"event": "event.recorded",
"timestamp": "2026-10-09T10:00:00.000Z",
"data": {
"eventId": "cm4u9t5vw000gl208r2e8n6yb",
"name": "Class booked",
"contactId": "cm4u2x8yq0003l208f1k9d7vz",
"email": "sam@example.com",
"properties": {
"class": "Wheel throwing for beginners",
"date": "2026-10-24"
},
"occurredAt": "2026-10-18T13:20:00.000Z",
"externalId": "hearth-1040",
"firstName": "Sam",
"lastName": "Lee"
}
}
webhook.testTest message
Sent by the Test button on an endpoint in Setup → Webhooks, whether or not the endpoint is switched on. It isn't an event you subscribe to.
{
"id": "evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s",
"event": "webhook.test",
"timestamp": "2026-10-09T10:00:00.000Z",
"data": {
"test": true,
"message": "This is a test event from SplashSend."
}
}
Objects
These are the objects the endpoints and webhooks send and receive. A type shown as a link in any table is described here.
ErrorThe body of every error response.
| Field | Type | |
|---|---|---|
error |
object | |
error.code |
"validation_error" | "invalid_request" | "read_only" | "limit_reached" | "sender_not_verified" | "design_not_ready" | "send_refused" | "unauthorized" | "forbidden" | "key_in_web_page" | "plan_limit_reached" | "not_open_yet" | "not_found" | "stopped_address" | "sent_recently" | "idempotency_in_progress" | "payload_too_large" | "idempotency_key_reused" | "rate_limited" | "internal_error" |
A fixed code your code can act on. |
error.message |
string | What went wrong, in words you can show a person. |
StepFieldA setting the account fills in on an app's drip step.
| Field | Type | |
|---|---|---|
keyrequired |
string | Its key in values. |
labelrequired |
string | The words the account sees. max length: 60 |
type |
"text" | "number" | "choice" |
default: "text" |
options |
array of string | With type: choice: 2 to 20 options. |
required |
boolean | The drip can't go live with it empty. default: false |
DripStepInput| Field | Type | |
|---|---|---|
namerequired |
string | Its name in the drip builder. max length: 60 |
description |
string | What it does, shown when it's chosen. max length: 200 |
fields |
array of StepField | max items: 6 |
DripStepOne of this app's drip steps.
| Field | Type | |
|---|---|---|
key |
string | |
name |
string | |
description |
string or null | |
fields |
array of StepField | |
created_at |
string | format: date-time |
updated_at |
string | format: date-time |
EmailBlockInput| Field | Type | |
|---|---|---|
namerequired |
string | Its name in the designer. max length: 60 |
description |
string | max length: 200 |
data_setrequired |
string | The key of a data set this app made. |
titlerequired |
string | The field each card's title shows. |
text |
string | The field each card's line of text shows. |
image |
string | The field holding each card's picture address. |
link |
string | The field holding the address each card's button opens. |
button |
string | The words on each card's button. Needs link. Defaults to View. max length: 30 |
count |
integer | How many cards at most. default: 3 minimum: 1 maximum: 6 |
EmailBlockOne of this app's email blocks.
| Field | Type | |
|---|---|---|
key |
string | |
name |
string | |
description |
string or null | |
data_set |
string | |
title |
string | |
text |
string or null | |
image |
string or null | |
link |
string or null | |
button |
string or null | |
count |
integer | |
created_at |
string | format: date-time |
updated_at |
string | format: date-time |
ReportCardInputA card for Reports, made from one of your events.
| Field | Type | |
|---|---|---|
namerequired |
string | Its title in Reports. max length: 80 |
description |
string | A line under the title saying what it shows. max length: 200 |
eventrequired |
string | The name of the event it reads, as you send it to POST /events. max length: 120 |
split |
"none" | "day" | "week" | "property" |
How its figure is split. default: "week" |
property |
string | With split: property: the event detail to split by, such as class. Events sent without it are under Not given. max length: 60 |
sum |
string | Add up this number from each event, such as price, instead of counting them. An event without a number there adds nothing. max length: 60 |
money |
boolean | With sum: show the total as money, in the account's currency. default: false |
chart |
"number" | "line" | "bars" | "table" |
How it's drawn. Defaults to number with no split, line by day or week, and bars by a detail. |
ReportCardOne of this app's report cards.
| Field | Type | |
|---|---|---|
key |
string | |
name |
string | |
description |
string or null | |
event |
string | |
split |
"none" | "day" | "week" | "property" |
|
property |
string or null | |
sum |
string or null | |
money |
boolean | |
chart |
"number" | "line" | "bars" | "table" |
|
created_at |
string | format: date-time |
updated_at |
string | format: date-time |
SubscriptionOne of an app's webhook subscriptions.
| Field | Type | |
|---|---|---|
id |
string | |
url |
string | format: uri |
events |
array of string | |
event_name |
string or null | The one of your own events it's narrowed to, with event.recorded. |
step_note |
string or null | The step note it's narrowed to, with drip.step. |
active |
boolean | False while it's paused after a day of failed deliveries, or switched off in Setup. |
paused_at |
string or null | format: date-time |
created_at |
string | format: date-time |
PageMetaPaging details for a list.
| Field | Type | |
|---|---|---|
page |
integer | This page's number, from 1. |
limit |
integer | How many fit on a page. |
total |
integer | How many there are in all. |
totalPages |
integer | How many pages there are. |
PersonA person, as every People endpoint returns them.
| Field | Type | |
|---|---|---|
id |
string | The person's id. |
email |
string | format: email |
name |
string | Their full name, or an empty string when it isn't known. |
firstName |
string or null | |
lastName |
string or null | |
source |
string | Where they first came from. |
subscriptionStatus |
"subscribed" | "unsubscribed" |
|
suppressed |
boolean | True when the address is stopped because it bounced or was reported as spam. A stopped address is never emailed, whatever subscriptionStatus says. |
createdAt |
string | format: date-time |
updatedAt |
string | format: date-time |
PersonWithPropertiesA person with their details.
| Field | Type | |
|---|---|---|
id |
string | The person's id. |
email |
string | format: email |
name |
string | Their full name, or an empty string when it isn't known. |
firstName |
string or null | |
lastName |
string or null | |
source |
string | Where they first came from. |
subscriptionStatus |
"subscribed" | "unsubscribed" |
|
suppressed |
boolean | True when the address is stopped because it bounced or was reported as spam. A stopped address is never emailed, whatever subscriptionStatus says. |
createdAt |
string | format: date-time |
updatedAt |
string | format: date-time |
properties |
map of string | Their details, keyed by name. Only details with a value are included. |
PersonResultWhat POST /contacts returns.
| Field | Type | |
|---|---|---|
data |
Person | A person, as every People endpoint returns them. |
meta |
object | |
meta.created |
boolean | True when they're new. |
meta.subscribed |
boolean | True when this request subscribed them. |
meta.suppressed |
boolean | True when the address is stopped, so it wasn't subscribed. |
meta.keptOptOut |
boolean | True when they had unsubscribed and still are, because resubscribe wasn't true. |
warnings |
array of string | Notes for you, such as a kept opt-out or details that were skipped. The field is left out when there are none. |
PropertyValuesDetails keyed by name, as they're called in SplashSend (any case). A value can be text, a number, or true or false. Send an array for a List or Multi-select detail, and null to clear a value. A date detail is stored as YYYY-MM-DD, and 15/01/1990 (day first) is read as 1990-01-15. The first 200 details are read, and each value is cut at 2,000 characters. A name that isn't a detail in your account is skipped with a warning. Shopify Tags, address details and details SplashSend works out for itself can't be set here, and are skipped with a warning too.
Type: map of string, number, boolean, array or null
PropertyValueOne of your details, with a person's value.
| Field | Type | |
|---|---|---|
id |
string | The detail's id. |
name |
string | The detail's name. |
type |
string | What kind of value it holds, such as text, date or list. |
value |
string | Their value, as text. Empty when they have none. |
ProfileA person with everything SplashSend knows about them.
| Field | Type | |
|---|---|---|
id |
string | The person's id. |
email |
string | format: email |
name |
string | Their full name, or an empty string when it isn't known. |
firstName |
string or null | |
lastName |
string or null | |
source |
string | Where they first came from. |
subscriptionStatus |
"subscribed" | "unsubscribed" |
|
suppressed |
boolean | True when the address is stopped because it bounced or was reported as spam. A stopped address is never emailed, whatever subscriptionStatus says. |
createdAt |
string | format: date-time |
updatedAt |
string | format: date-time |
consent |
object | |
consent.email |
object | |
consent.email.status |
"subscribed" | "unsubscribed" |
|
consent.email.mailable |
boolean | True when a send would reach them. A send doesn't reach someone who unsubscribed, whose address is stopped or who is on a list marked never to be emailed. |
consent.email.consentedAt |
string or null | When they agreed to hear from you. format: date-time |
consent.email.consentMethod |
string or null | How they agreed, such as signup_form, import, shopify_sync or api. |
consent.email.unsubscribedAt |
string or null | format: date-time |
consent.email.unsubscribeReason |
string or null | The reason they gave for leaving, when they gave one. |
consent.email.suppressedAt |
string or null | When their address was stopped. format: date-time |
consent.email.suppressionReason |
string or null | Why it was stopped. One of hard_bounce, soft_bounce_limit and complaint. |
consent.email.blockedByLists |
array of object | The lists marked never to be emailed that they're on. Sent only to a key with lists:read. |
consent.email.blockedByLists[].id |
string | |
consent.email.blockedByLists[].name |
string | |
consent.sms |
object | |
consent.sms.phone |
string or null | Their mobile number in international format, such as +447700900123. |
consent.sms.status |
"not_subscribed" | "subscribed" | "unsubscribed" |
|
consent.sms.consentedAt |
string or null | When they agreed to texts. format: date-time |
consent.sms.consentMethod |
string or null | How they agreed, such as signup_form, shopify_sync or api. |
consent.sms.unsubscribedAt |
string or null | format: date-time |
consent.sms.suppressedAt |
string or null | When texts to their number were stopped. format: date-time |
consent.sms.suppressionReason |
string or null | Why they were stopped. One of invalid_number, carrier_rejected and stop. |
consent.topics |
array of object | The topics on your preferences page that they've chosen to be in or out of. |
consent.topics[].name |
string | |
consent.topics[].subscribed |
boolean | |
details |
array of object | Their details that have a value. |
details[].name |
string | |
details[].type |
string | What kind of value it holds, such as text, date or list. |
details[].value |
string | |
details[].source |
"manual" | "auto" | "guessed" |
How the value got there, in the words their page in SplashSend uses. manual is a value you entered, auto one kept up to date automatically, and guessed one filled in as they arrived. |
emails |
object | The emails sent to them, counted the way Reports counts them. |
emails.sent |
integer | Emails sent to them. An email sent again after a failed try counts once. |
emails.delivered |
integer | |
emails.bounced |
integer | |
emails.opened |
integer | Emails they opened. |
emails.clicked |
integer | Emails they clicked a link in. A click made by link-checking software isn't counted. |
emails.lastSentAt |
string or null | format: date-time |
recentActivity |
array of ActivityEvent | The 25 most recent things that happened to them, newest first. GET /activity with their email lists the rest. Sent only to a key with events:read. |
orders |
object | Their orders, from a Shopify store or sent with POST /orders. Sent only to a key with orders:read. |
orders.count |
integer | Their sales. An order refunded in full, voided, cancelled or failed isn't one. |
orders.revenue |
number | What their sales came to. |
orders.currency |
string or null | The currency of their latest sale. |
orders.recent |
array of object | Their 10 most recent orders, newest first, refunded and cancelled ones included. |
orders.recent[].id |
string | The order's id in SplashSend. |
orders.recent[].order_id |
string or null | The shop's order number. |
orders.recent[].total |
number | |
orders.recent[].currency |
string | |
orders.recent[].status |
string or null | The order's status, as the shop sent it. |
orders.recent[].order_date |
string | format: date-time |
orders.recent[].source |
"shopify" | "api" |
shopify for a Shopify store's order, api for one sent with POST /orders. |
orders.recent[].is_sale |
boolean | False when it was refunded in full, voided, cancelled or failed. |
orders.recent[].credited_to_email |
boolean | True when it's credited to an email they were sent. |
data |
array of object | The data-set rows matched to them: in each data set matched on a detail, the row whose column equals theirs. Then the rows those join to, such as the class a booking is for, where an email would read the same row. Sent only to a key with data:read. |
data[].set |
string | The data set's key. |
data[].name |
string | The data set's name. |
data[].values |
map of string | The row's values keyed by column name, written the way an email prints them. |
data[].through |
string | For a joined row, the key of the set whose row joins to it. Left out for their own rows. |
ImportContactInputOne person in an import.
| Field | Type | |
|---|---|---|
emailrequired |
string | Their email address. format: email |
firstName |
string | Their first name. |
lastName |
string | Their last name. |
name |
string | Their full name, split into first and last. Ignored when firstName or lastName is sent. |
source |
string | Where they came from. Used only for someone new. |
subscriptionStatus |
"subscribed" | "unsubscribed" |
How someone new arrives. Defaults to subscribed. Someone already here keeps their status. |
properties |
PropertyValues | Details keyed by name, as they're called in SplashSend (any case). |
ImportResultWhat POST /contacts/import returns.
| Field | Type | |
|---|---|---|
data |
object | |
data.created |
integer | People added. |
data.updated |
integer | People already here, updated. |
data.failed |
integer | Rows that weren't saved. |
data.addedToList |
integer | People added to the list. The field is there only when you sent listId. |
meta |
object | |
meta.startAutomations |
boolean | Whether automations ran. |
meta.errors |
array of object | Up to 50 rows that failed. |
meta.errors[].index |
integer | The row's position in contacts, from 0. |
meta.errors[].email |
string | The row's email address, when it had one. |
meta.errors[].message |
string | Why it failed. |
meta.unknownProperties |
array of string | Detail names that aren't in your account, up to 50. |
meta.planLimitReached |
boolean | True when the import stopped at your plan's people limit. |
meta.stoppedAtIndex |
integer | The row the import stopped at. It and the rows after it weren't added. |
warnings |
array of string | Notes about details that were skipped, or a list that couldn't be updated. The field is left out when there are none. |
EventA recorded event.
| Field | Type | |
|---|---|---|
id |
string | The event's id. |
name |
string | |
email |
string | The person's email address, sent only to a key with contacts:read. format: email |
properties |
object or null | What was sent with it. |
occurred_at |
string | format: date-time |
external_id |
string or null | Your id for it, when you sent one. |
ActivityEventSomething that happened.
| Field | Type | |
|---|---|---|
id |
string | The id of what it records: the email, the order, the answer and so on. |
name |
string | The event, such as email.opened or one your systems send. |
kind |
"built_in" | "custom" |
built_in for SplashSend's own events, custom for the ones your systems send. |
occurred_at |
string | format: date-time |
email |
string or null | Whose it was, sent only to a key with contacts:read. Null for an answer given without an email address. format: email |
first_name |
string or null | Their first name, when SplashSend has it. Sent only to a key with contacts:read. |
last_name |
string or null | Their last name, when SplashSend has it. Sent only to a key with contacts:read. |
properties |
object | What it carries. GET /activity/catalogue lists each built-in event's; a custom event's are what your systems sent with it. |
CatalogueEventA kind of event SplashSend records.
| Field | Type | |
|---|---|---|
name |
string | Its name. Webhooks use the same one. |
label |
string | Its name in plain words. |
group |
string | Email, People, Shop, Website, Forms or Drips. |
properties |
array of object | What it carries in properties. |
properties[].key |
string | |
properties[].label |
string | |
properties[].type |
string | |
counting |
string | How it's counted. |
webhook |
boolean | Webhooks send it under this name. |
needs |
string or null | The scope needed beyond events:read to see it: contacts:read or orders:read. |
OrderAn order sent in with POST /orders.
| Field | Type | |
|---|---|---|
id |
string | The order's id in SplashSend. |
order_id |
string | Your order number or id. |
email |
string | The buyer's email address, sent only to a key with contacts:read. format: email |
total |
number | |
currency |
string | |
status |
string or null | |
order_date |
string | format: date-time |
contact_id |
string or null | The buyer's person id. Null when the order has no person. |
source |
string |
ListA list you add people to yourself.
| Field | Type | |
|---|---|---|
id |
string | |
name |
string | |
description |
string or null | |
blockSending |
boolean | When true, nobody on the list is sent a campaign, a drip email or a Cruise email, even when another group they're in is picked. Their subscriptions don't change. |
memberCount |
integer | How many people are on it. |
createdAt |
string | format: date-time |
updatedAt |
string | format: date-time |
ListEnvelopeA list, as the single-list endpoints return it.
| Field | Type | |
|---|---|---|
data |
List | A list you add people to yourself. |
ListInputThe fields of a list you can set.
| Field | Type | |
|---|---|---|
name |
string | max length: 120 |
description |
string or null | Send null to clear it. max length: 500 |
blockSending |
boolean | Makes it a "Never email these people" list. Take someone off the list and they can be emailed again as before. |
ListMemberA person on a list.
| Field | Type | |
|---|---|---|
id |
string | The person's id. |
email |
string | Their email address, sent only to a key with contacts:read. format: email |
name |
string | Their name, sent only to a key with contacts:read. |
subscriptionStatus |
"subscribed" | "unsubscribed" |
|
addedAt |
string | When they were added to the list. format: date-time |
MembersInputPeople already in your account, by id, by email address or both. Up to 500 in one request.
| Field | Type | |
|---|---|---|
contactIds |
array of string | |
emails |
array of string |
ProductInputA product to add or update.
| Field | Type | |
|---|---|---|
product_idrequired |
string or number | Your id for the product. Sending it again updates the product. |
title |
string | Its name. Needed for a new product. |
price |
number or string | Its price, such as 18 or "18.00". |
currency |
string | A currency code, such as GBP. |
image_url |
string | Its picture, as an http(s) address. format: uri |
url |
string | Its page in your shop. Product blocks link here. format: uri |
handle |
string | Its slug in your shop's addresses. Defaults to product_id. |
description |
string | max length: 300 |
product_type |
string | Its type, such as Mugs. |
vendor |
string | Who makes it. |
status |
string | active, draft or archived, in any case. Defaults to active for a new product. |
tags |
string or array of string | Its tags, as an array or as one string. |
inventory |
integer | How many you have in stock. When a product goes from 0 to more, the people who asked to hear when it was back are told (a drip that starts with "They asked to hear when it’s back"). minimum: 0 |
in_stock |
boolean | For a shop that only knows whether it is in stock: true or false. Ignored when inventory is sent. |
ProductA product in your catalogue.
| Field | Type | |
|---|---|---|
product_id |
string | The id you sent, or Shopify's id for a Shopify product. |
source |
"api" | "shopify" |
api for a product sent with POST /products. |
title |
string | |
handle |
string | |
price |
string or null | Its price, as text. |
currency |
string or null | |
image_url |
string or null | |
url |
string or null | Its page in your shop, when known. |
product_type |
string or null | |
vendor |
string or null | |
status |
"ACTIVE" | "DRAFT" | "ARCHIVED" |
Only ACTIVE products appear in emails. |
updated_at |
string | format: date-time |
DataSetSummaryA data set, as GET /data lists it.
| Field | Type | |
|---|---|---|
key |
string | Its identifier, used in the API and in email tags. |
name |
string | |
description |
string or null | |
source |
"live" | "manual" |
manual for a set you keep yourself, live for one read from a connected source. |
writable |
boolean | True when it takes rows over the API. |
row_count |
integer | |
updated_at |
string or null | When its rows last changed, when that's known. format: date-time |
fields |
array of DataField | |
joins |
array of DataJoin | The other sets it joins. |
DataFieldA column of a data set.
| Field | Type | |
|---|---|---|
key |
string | What rows and email tags call it. |
label |
string | Its name in the app. |
type |
"words" | "number" | "money" | "date" | "boolean" | "link" | "image" |
DataFieldInputA column of a data set you keep yourself.
| Field | Type | |
|---|---|---|
label |
string | Its name. Needed for a new field. max length: 60 |
type |
"words" | "number" | "money" | "date" | "boolean" | "link" | "image" |
What it holds. default: "words" |
key |
string | Its key, made from the label when left out. On an update, send a field's key to keep that field. |
example |
string | A sample value, shown in the app. |
DataJoinA join from this set to another one.
| Field | Type | |
|---|---|---|
fieldrequired |
string | One of this set's fields. A request can name it by key or label. |
torequired |
string | The other set's key. |
to_fieldrequired |
string | The other set's field with the matching values. A request can name it by key or label. |
MatchOnWhat a matched set matches on.
| Field | Type | |
|---|---|---|
detailrequired |
string | The person's side: email, name, source or the name of one of your details. |
fieldrequired |
string | The set's side: one of its fields, by key or label. |
RowValuesA row's values, keyed by a field's key in any case, or by its label.
Type: map of string, number, boolean or null
DataRowsA page of a data set's rows.
DataRowOne row: its values keyed by field key, and its id in a set you keep yourself.
| Field | Type | |
|---|---|---|
id |
string | The row's id, in a set you keep yourself. |
WebhookEnvelopeThe body of every webhook message.
| Field | Type | |
|---|---|---|
id |
string | The message's id, such as evt_…. The same on every retry and redelivery. |
event |
string | The event's name, such as order.placed. |
timestamp |
string | When this copy was sent. A redelivery has a new one. format: date-time |
data |
object | What happened, as ids and plain values. It never holds a secret. |