{"openapi":"3.1.0","info":{"title":"SplashSend API","version":"1.0.0","description":"The SplashSend REST API lets your own systems work with your people, lists, custom events, orders, products and data sets.\n\n**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](https://splashsend.com/developers#public-apps). 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.\n\n**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.\n\n**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.\n\n**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.\n\n**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.\n\n**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`).\n\n**Errors:** every error has the body `{ \"error\": { \"code\": \"…\", \"message\": \"…\" } }`. The message says what went wrong in plain words, and the codes are listed below.\n\n**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.\n\n**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](https://splashsend.com/help/integrations/connecting-an-ai-assistant).\n\n**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`."},"servers":[{"url":"https://splashsend.com"}],"security":[{"bearerAuth":[]}],"x-error-codes":[{"status":400,"code":"validation_error","meaning":"A field is missing, or has a value the endpoint can't take."},{"status":400,"code":"invalid_request","meaning":"The body isn't valid JSON, or isn't the shape the endpoint expects."},{"status":400,"code":"read_only","meaning":"The data set is built in or read from a connected source, so it can't be changed this way."},{"status":400,"code":"limit_reached","meaning":"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."},{"status":400,"code":"sender_not_verified","meaning":"A campaign can't be sent or scheduled until the account's sender email is verified in Setup."},{"status":400,"code":"design_not_ready","meaning":"The design can't be sent as it is, such as one without an unsubscribe link. The message says what to fix."},{"status":400,"code":"send_refused","meaning":"The campaign couldn't be sent, for the reason the message gives."},{"status":401,"code":"unauthorized","meaning":"The key or token is missing, wrong, expired or revoked, or the app was removed."},{"status":403,"code":"forbidden","meaning":"The key or token doesn't have the scope this request needs."},{"status":403,"code":"key_in_web_page","meaning":"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."},{"status":403,"code":"plan_limit_reached","meaning":"Your plan's people limit is reached, so a new person can't be added, or its sends for the month are used up."},{"status":403,"code":"not_open_yet","meaning":"The account isn't open for sending yet."},{"status":404,"code":"not_found","meaning":"The person, list, data set or row isn't in your account."},{"status":409,"code":"stopped_address","meaning":"The address is stopped because it bounced or was reported as spam, so it can't be subscribed."},{"status":409,"code":"sent_recently","meaning":"This design went, or is scheduled to go, to the same people within a day, so it wasn't sent again."},{"status":409,"code":"idempotency_in_progress","meaning":"A request with this Idempotency-Key is still running. Wait for the `Retry-After` header (5 seconds), then retry."},{"status":413,"code":"payload_too_large","meaning":"The body is over the size limit."},{"status":422,"code":"idempotency_key_reused","meaning":"This Idempotency-Key was already used for a different request. Send a new key."},{"status":429,"code":"rate_limited","meaning":"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."},{"status":500,"code":"internal_error","meaning":"Something went wrong on our side."}],"tags":[{"name":"Account","description":"Which account a key or token is for, and what it may do."},{"name":"People","description":"The people on your list. Their details, such as a birthday, are called `properties` here, and are named the way they are in SplashSend."},{"name":"Lists","description":"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."},{"name":"Events","description":"Things people do, such as a booking or a sign-up, recorded so they can start drips."},{"name":"Orders","description":"Orders from a shop that isn't connected through Shopify."},{"name":"Activity","description":"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.\n\n**The events SplashSend records:**\n\n- `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`.\n- `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`.\n- `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`.\n- `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`.\n- `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`.\n- `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`.\n- `email.complained` (Marked as spam): A spam report from the person's email provider. Needs `contacts:read`.\n- `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`.\n- `contact.unsubscribed` (Person unsubscribed): Leaving by any route, from the unsubscribe link to a spam report. Needs `contacts:read`.\n- `contact.suppressed` (Emails stopped): A person's emails stopped by hand or after a bounce. Needs `contacts:read`.\n- `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`.\n- `checkout.abandoned` (Checkout left unfinished): A Shopify checkout with an email address that wasn't finished, and whether they bought later. Needs `orders:read`.\n- `viewed_product` (Viewed a product): Recorded by the website script for someone who came from one of your emails.\n- `added_to_cart` (Added to basket): Recorded by the website script for someone who came from one of your emails.\n- `searched` (Searched the site): Recorded by the website script. A search that looks like an address or a long number isn't kept.\n- `viewed_collection` (Viewed a collection): Recorded by the website script for someone who came from one of your emails.\n- `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`.\n- `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`.\n- `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`."},{"name":"Data sets","description":"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."},{"name":"Products","description":"Your product catalogue, for product blocks in emails."},{"name":"Groups","description":"Groups pick people by rules and keep themselves up to date. A campaign can go to one."},{"name":"Campaigns","description":"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."},{"name":"Webhooks (outbound)","description":"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.\n\n**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.\n\n**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.\n\n**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.\n\n**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.\n\n**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.\n\n**Names:** every event about a person also carries their `firstName` and `lastName`, null when they have none.\n\n**Apps:** an app subscribes to these events with POST /webhooks rather than through Setup. See Webhook subscriptions."},{"name":"Webhook subscriptions","description":"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."},{"name":"Report cards","description":"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.\n\nReports 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."},{"name":"Drip steps","description":"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.\n\nNeeds `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."},{"name":"Email blocks","description":"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.\n\nNeeds `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."}],"paths":{"/api/v1/me":{"get":{"operationId":"whoAmI","tags":["Account"],"summary":"Who this key or token is for","description":"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.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Who the key or token is for.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"account":{"type":["string","null"],"description":"The account's name, as people see it on its emails."},"app":{"type":["string","null"],"description":"The app the key or token belongs to."},"test_mode":{"type":"boolean","description":"True while the app is in test mode: nothing it does starts a drip, sends an email or sends a webhook."},"scopes":{"type":"array","items":{"type":"string"},"description":"What it may do."}}}},"example":{"data":{"account":"Wrenfield Pottery","app":"Hearth Bookings sync","test_mode":true,"scopes":["contacts:write","events:write"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/contacts":{"get":{"operationId":"listPeople","tags":["People"],"summary":"List people","description":"Newest first.","security":[{"bearerAuth":["contacts:read"]}],"parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/Limit"},{"name":"status","in":"query","schema":{"type":"string","enum":["subscribed","unsubscribed"]},"description":"Only people with this status."},{"name":"email","in":"query","schema":{"type":"string","format":"email"},"description":"Only the person at this address, to look someone up before changing them."}],"responses":{"200":{"description":"A page of people.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Person"}},"meta":{"$ref":"#/components/schemas/PageMeta"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"upsertPerson","tags":["People"],"summary":"Add or update a person","description":"Adds a person by email address, subscribed, or updates the person already at that address.\n\nSomeone 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.\n\nWhen 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.\n\n`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.\n\n`meta` says what happened. `warnings` explains a kept opt-out, a stopped address and anything that was skipped.","security":[{"bearerAuth":["contacts:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","description":"Their email address."},"firstName":{"type":"string","description":"Their first name."},"lastName":{"type":"string","description":"Their last name."},"name":{"type":"string","description":"Their full name, split into first and last. Ignored when `firstName` or `lastName` is sent."},"source":{"type":"string","description":"Where they came from, such as `website`. Set only for someone new."},"properties":{"$ref":"#/components/schemas/PropertyValues"},"preferences":{"type":"array","items":{"type":"string"},"description":"The names of the email topics they chose."},"resubscribe":{"type":"boolean","default":false,"description":"Subscribe someone who unsubscribed. Send it only when they've asked to hear from you again."},"startAutomations":{"type":"boolean","default":true,"description":"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."}}},"example":{"email":"sam@example.com","firstName":"Sam","lastName":"Lee","source":"website","properties":{"Birthday":"1990-05-15"},"preferences":["Newsletter"]}}}},"responses":{"200":{"description":"They were already here, and are updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonResult"}}}},"201":{"description":"They're new, and have been added.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonResult"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key doesn't have `contacts:write` (`forbidden`), or adding them would go over your plan's people limit (`plan_limit_reached`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"$ref":"#/components/responses/IdempotencyInProgress"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/contacts/import":{"post":{"operationId":"importPeople","tags":["People"],"summary":"Import people","description":"Adds or updates up to 500 people in one request, for a migration or a backfill.\n\nSomeone 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.\n\nAutomations 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.\n\nEach 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.","security":[{"bearerAuth":["contacts:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["contacts"],"properties":{"contacts":{"type":"array","minItems":1,"maxItems":500,"items":{"$ref":"#/components/schemas/ImportContactInput"},"description":"The people, up to 500."},"startAutomations":{"type":"boolean","default":false,"description":"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."},"listId":{"type":"string","description":"Add everyone imported to this list. Needs `lists:write`."},"source":{"type":"string","description":"The `source` for someone new whose row has none. Defaults to `api_import`."}}},"example":{"contacts":[{"email":"sam@example.com","firstName":"Sam","lastName":"Lee"},{"email":"alex@example.com","name":"Alex Morgan","subscriptionStatus":"unsubscribed"}],"source":"old_platform"}}}},"responses":{"200":{"description":"The import ran. Rows that failed are listed in `meta.errors`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportResult"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key doesn't have `contacts:write`, or you sent `listId` and it doesn't have `lists:write`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The list in `listId` isn't in your account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"$ref":"#/components/responses/IdempotencyInProgress"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/contacts/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"cm4u2x8yq0003l208f1k9d7vz","description":"The person's id."}],"get":{"operationId":"getPerson","tags":["People"],"summary":"Get a person","description":"Includes their details in `properties`, keyed by name. A detail with no value is left out.","security":[{"bearerAuth":["contacts:read"]}],"responses":{"200":{"description":"The person.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PersonWithProperties"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"operationId":"updatePerson","tags":["People"],"summary":"Update a person","description":"Changes only the fields you send.\n\nSetting `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.\n\ncontact.updated is sent only when their name, source or details changed.","security":[{"bearerAuth":["contacts:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"firstName":{"type":"string","description":"Their first name."},"lastName":{"type":"string","description":"Their last name."},"name":{"type":"string","description":"Their full name, split into first and last. Ignored when `firstName` or `lastName` is sent. A blank value never clears a stored name."},"source":{"type":"string","description":"Where they came from."},"subscriptionStatus":{"type":"string","enum":["subscribed","unsubscribed"]},"properties":{"$ref":"#/components/schemas/PropertyValues"}}},"example":{"firstName":"Samantha","properties":{"Birthday":"1990-05-15"}}}}},"responses":{"200":{"description":"The person, with their details.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PersonWithProperties"},"warnings":{"type":"array","items":{"type":"string"},"description":"Notes about anything that was skipped. The field is left out when there are none."}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The address is stopped, so it can't be subscribed (`stopped_address`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"unsubscribePerson","tags":["People"],"summary":"Unsubscribe a person","description":"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.","security":[{"bearerAuth":["contacts:write"]}],"responses":{"200":{"description":"The person, now unsubscribed.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Person"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/contacts/{id}/properties":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"cm4u2x8yq0003l208f1k9d7vz","description":"The person's id."}],"get":{"operationId":"listPersonDetails","tags":["People"],"summary":"List a person's details","description":"Every detail in your account, with this person's value. A detail they have no value for has `\"\"`.","security":[{"bearerAuth":["contacts:read"]}],"responses":{"200":{"description":"Their details.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PropertyValue"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"put":{"operationId":"setPersonDetails","tags":["People"],"summary":"Set a person's details","description":"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.","security":[{"bearerAuth":["contacts:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["properties"],"properties":{"properties":{"$ref":"#/components/schemas/PropertyValues"}}},"example":{"properties":{"Birthday":"1990-05-15"}}}}},"responses":{"200":{"description":"Their details.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PropertyValue"}},"warnings":{"type":"array","items":{"type":"string"},"description":"Notes about anything that was skipped. The field is left out when there are none."}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/contacts/{id}/profile":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"cm4u2x8yq0003l208f1k9d7vz","description":"The person's id."}],"get":{"operationId":"getPersonProfile","tags":["People"],"summary":"Get a person's profile","description":"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.\n\nTheir 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.\n\nWhat 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.","security":[{"bearerAuth":["contacts:read"]}],"responses":{"200":{"description":"Their profile.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Profile"},"meta":{"type":"object","properties":{"left_out":{"type":"array","items":{"type":"string"},"description":"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."}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/lists":{"get":{"operationId":"listLists","tags":["Lists"],"summary":"List your lists","description":"Newest first.","security":[{"bearerAuth":["lists:read"]}],"parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/Limit"},{"name":"search","in":"query","schema":{"type":"string"},"description":"Only lists whose name or description contains this."}],"responses":{"200":{"description":"A page of lists.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/List"}},"meta":{"$ref":"#/components/schemas/PageMeta"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"createList","tags":["Lists"],"summary":"Create a list","description":"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.","security":[{"bearerAuth":["lists:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ListInput"},{"type":"object","required":["name"]}]},"example":{"name":"Workshop regulars","description":"People who have been to three or more workshops."}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListEnvelope"}}}},"400":{"description":"The request isn't valid, or the account already has 500 lists (`limit_reached`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/IdempotencyInProgress"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/lists/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"cm4u3b1zt0007l2083n6pq2xa","description":"The list's id."}],"get":{"operationId":"getList","tags":["Lists"],"summary":"Get a list","description":"Includes how many people are on it.","security":[{"bearerAuth":["lists:read"]}],"responses":{"200":{"description":"The list.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"operationId":"updateList","tags":["Lists"],"summary":"Update a list","description":"Changes only the fields you send.","security":[{"bearerAuth":["lists:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListInput"},"example":{"name":"Workshop regulars 2026"}}}},"responses":{"200":{"description":"The list, updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListEnvelope"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"deleteList","tags":["Lists"],"summary":"Delete a list","description":"Deletes the list. The people on it stay in your account, and their subscriptions don't change.","security":[{"bearerAuth":["lists:write"]}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"deleted":{"type":"boolean","const":true}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/lists/{id}/members":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"example":"cm4u3b1zt0007l2083n6pq2xa","description":"The list's id."}],"get":{"operationId":"listListMembers","tags":["Lists"],"summary":"List a list's members","description":"Newest additions first. A member's `email` and `name` are included only for a key with `contacts:read`.","security":[{"bearerAuth":["lists:read"]}],"parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"description":"A page of members.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ListMember"}},"meta":{"$ref":"#/components/schemas/PageMeta"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"addListMembers","tags":["Lists"],"summary":"Add people to a list","description":"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`.","security":[{"bearerAuth":["lists:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/MembersInput"},{"type":"object","properties":{"startAutomations":{"type":"boolean","default":false,"description":"Start drips that watch this list for the people added."}}}]},"example":{"emails":["sam@example.com"],"contactIds":["cm4u2x8yq0004l208t6r3b1nh"]}}}},"responses":{"200":{"description":"Added.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"added":{"type":"integer","description":"People added to the list."},"alreadyMembers":{"type":"integer","description":"People who were on it already."}}},"meta":{"type":"object","properties":{"startAutomations":{"type":"boolean","description":"Whether drips that watch this list were started."},"unknownIds":{"type":"array","items":{"type":"string"},"description":"Ids that aren't in your account, up to 50."},"unknownEmails":{"type":"array","items":{"type":"string"},"description":"Addresses that aren't in your account, up to 50."}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"removeListMembers","tags":["Lists"],"summary":"Remove people from a list","description":"Takes people off the list, by id or email address. The people and their subscriptions don't change.","security":[{"bearerAuth":["lists:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MembersInput"},"example":{"emails":["sam@example.com"]}}}},"responses":{"200":{"description":"Removed.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"removed":{"type":"integer","description":"People taken off the list."}}},"meta":{"type":"object","properties":{"unknownIds":{"type":"array","items":{"type":"string"},"description":"Ids that aren't in your account, up to 50."},"unknownEmails":{"type":"array","items":{"type":"string"},"description":"Addresses that aren't in your account, up to 50."}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/events":{"get":{"operationId":"listEvents","tags":["Events"],"summary":"List events","description":"Newest first. An event's `email` is included only for a key with `contacts:read`.","security":[{"bearerAuth":["events:read"]}],"parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/Limit"},{"name":"name","in":"query","schema":{"type":"string"},"description":"Only events with this name."},{"name":"email","in":"query","schema":{"type":"string","format":"email"},"description":"Only this person's events. Needs `contacts:read`, and without it the request gets a 403."}],"responses":{"200":{"description":"A page of events.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Event"}},"meta":{"$ref":"#/components/schemas/PageMeta"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key doesn't have `events:read`, or it filtered by `email` without `contacts:read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"recordEvent","tags":["Events"],"summary":"Record an event","description":"Records something a person did, such as booking a class, and starts any drip whose trigger is an event with this name.\n\nThe 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.\n\nDrips 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.\n\nSend `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.","security":[{"bearerAuth":["events:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","name"],"properties":{"email":{"type":"string","format":"email","description":"The person who did it."},"name":{"type":"string","maxLength":120,"description":"The event's name, such as `class_booked`."},"properties":{"type":"object","additionalProperties":true,"description":"Anything else about the event, up to 16KB as JSON."},"occurred_at":{"type":"string","format":"date-time","description":"When it happened. Defaults to now."},"external_id":{"oneOf":[{"type":"string"},{"type":"number"}],"description":"Your id for this event, so a retry is recorded once."},"startAutomations":{"type":"boolean","description":"`true` starts drips for an event of any age. `false` starts none."}}},"example":{"email":"sam@example.com","name":"class_booked","properties":{"class":"Beginners wheel","seats":2},"external_id":"booking-10482"}}}},"responses":{"201":{"description":"Recorded, or found already recorded.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","description":"The event's id."},"name":{"type":"string"},"email":{"type":"string","format":"email","description":"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`."},"occurred_at":{"type":"string","format":"date-time"},"external_id":{"type":["string","null"]}}},"meta":{"type":"object","properties":{"contact_created":{"type":"boolean","description":"True when this event added the person."},"duplicate":{"type":"boolean","description":"True when an event with this `external_id` was already recorded. Nothing new was stored."},"automations_started":{"type":"boolean","description":"True when drips with this event as their trigger were started, if there are any."}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key doesn't have `events:write` (`forbidden`), or adding the person would go over your plan's people limit (`plan_limit_reached`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"$ref":"#/components/responses/IdempotencyInProgress"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/activity":{"get":{"operationId":"listActivity","tags":["Activity"],"summary":"List what happened","description":"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.\n\nAn 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.\n\nEvents 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`.\n\nTo page, send the `meta.next_cursor` of one page as `cursor` to get the next. It's null on the last page.","security":[{"bearerAuth":["events:read"]}],"parameters":[{"$ref":"#/components/parameters/Limit"},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Where the last page ended: its `meta.next_cursor`."},{"name":"name","in":"query","schema":{"type":"string"},"description":"Only this event: one from GET /activity/catalogue, such as `email.clicked`, or one your systems send."},{"name":"kind","in":"query","schema":{"type":"string","enum":["built_in","custom"]},"description":"`built_in` for SplashSend's own events, `custom` for the ones your systems send."},{"name":"email","in":"query","schema":{"type":"string","format":"email"},"description":"Only this person's events. Needs `contacts:read`, and without it the request gets a 403."},{"name":"since","in":"query","schema":{"type":"string"},"description":"From this time: a date such as `2026-10-01` or a time such as `2026-10-01T09:00:00Z`."},{"name":"until","in":"query","schema":{"type":"string"},"description":"Up to this time, not including it."}],"responses":{"200":{"description":"A page of what happened.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ActivityEvent"}},"meta":{"type":"object","properties":{"limit":{"type":"integer","description":"How many fit on a page."},"next_cursor":{"type":["string","null"],"description":"Send as `cursor` for the next page. Null on the last page."},"left_out":{"type":"array","items":{"type":"string"},"description":"Events left out because the key can't see them. The field is left out when there are none."}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key doesn't have `events:read`, or it filtered by `email` without `contacts:read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/activity/count":{"get":{"operationId":"countActivity","tags":["Activity"],"summary":"Count an event","description":"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.","security":[{"bearerAuth":["events:read"]}],"parameters":[{"name":"name","in":"query","schema":{"type":"string"},"description":"The event to count: one from GET /activity/catalogue, or one your systems send.","required":true},{"name":"kind","in":"query","schema":{"type":"string","enum":["built_in","custom"]},"description":"`built_in` for SplashSend's own events, `custom` for the ones your systems send."},{"name":"email","in":"query","schema":{"type":"string","format":"email"},"description":"Only this person's events. Needs `contacts:read`, and without it the request gets a 403."},{"name":"since","in":"query","schema":{"type":"string"},"description":"From this time: a date such as `2026-10-01` or a time such as `2026-10-01T09:00:00Z`."},{"name":"until","in":"query","schema":{"type":"string"},"description":"Up to this time, not including it."}],"responses":{"200":{"description":"The count.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"name":{"type":"string"},"kind":{"type":"string","enum":["built_in","custom"],"description":"Which events were counted."},"count":{"type":"integer"},"since":{"type":["string","null"],"format":"date-time"},"until":{"type":["string","null"],"format":"date-time"},"email":{"type":"string","format":"email","description":"The person counted for, when you asked for one."}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The key doesn't have `events:read`, it counted an event about a person or filtered by `email` without `contacts:read`, or it counted orders or unfinished checkouts without `orders:read`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/activity/catalogue":{"get":{"operationId":"eventCatalogue","tags":["Activity"],"summary":"The events SplashSend records","description":"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.","security":[{"bearerAuth":["events:read"]}],"responses":{"200":{"description":"The catalogue.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CatalogueEvent"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/orders":{"get":{"operationId":"listOrders","tags":["Orders"],"summary":"List orders","description":"Orders sent in with POST /orders, newest order first. `email` is included only for a key with `contacts:read`.","security":[{"bearerAuth":["orders:read"]}],"parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"description":"A page of orders.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Order"}},"meta":{"$ref":"#/components/schemas/PageMeta"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"upsertOrder","tags":["Orders"],"summary":"Add or update an order","description":"Records an order from your shop by `order_id`. To update it, send the same `order_id` again with only what changed.\n\nA 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.\n\nWhen 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.\n\nDrips 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.","security":[{"bearerAuth":["orders:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["order_id"],"properties":{"order_id":{"oneOf":[{"type":"string"},{"type":"number"}],"description":"Your order number or id."},"email":{"type":"string","format":"email","description":"The buyer's email address. Needed for a new order."},"total":{"type":"number","minimum":0,"description":"The order's total. Needed for a new order."},"currency":{"type":"string","description":"A three-letter currency code, such as `GBP`. Defaults to `USD`."},"items":{"type":"array","items":{"type":"object","additionalProperties":true},"description":"What was bought. Each item's `name`, or `title`, names it in review emails."},"status":{"type":"string","description":"The order's status in your shop. `fulfilled`, `completed`, `complete`, `shipped` and `delivered` count as shipped."},"order_date":{"type":"string","format":"date-time","description":"When it was placed. Defaults to now."},"source":{"type":"string","description":"Where it came from, such as your shop's name. Defaults to `woocommerce`."},"accepts_marketing":{"type":"boolean","default":false,"description":"True when the buyer agreed to marketing email at checkout. Used only when this order adds them."},"startAutomations":{"type":"boolean","description":"For a buyer this order subscribes: `true` starts their welcome drips for an order of any age, and `false` never starts them."}}},"example":{"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"}}}},"responses":{"201":{"description":"Saved.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","description":"The order's id in SplashSend."},"order_id":{"type":"string","description":"Your order number or id."},"email":{"type":"string","format":"email","description":"Sent when the request included it, or the key has `contacts:read`."},"total":{"type":"number"},"currency":{"type":"string"},"status":{"type":["string","null"]},"order_date":{"type":"string","format":"date-time"},"contact_id":{"type":["string","null"],"description":"The buyer's person id. Null when the order has no person."}}},"meta":{"type":"object","properties":{"contact_created":{"type":"boolean","description":"True when this order added the buyer."},"warning":{"type":"string","description":"Present when your plan's people limit kept the order from being linked to a person."}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/IdempotencyInProgress"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/products":{"get":{"operationId":"listProducts","tags":["Products"],"summary":"List products","description":"Products from your Shopify store and products sent with POST /products, newest first.","security":[{"bearerAuth":["products:read"]}],"parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"description":"A page of products.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Product"}},"meta":{"$ref":"#/components/schemas/PageMeta"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"upsertProducts","tags":["Products"],"summary":"Add or update products","description":"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.\n\nAn 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.\n\n`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.\n\nA 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.","security":[{"bearerAuth":["products:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProductInput","title":"One product"},{"title":"Several products","type":"object","required":["products"],"properties":{"products":{"type":"array","minItems":1,"maxItems":100,"items":{"$ref":"#/components/schemas/ProductInput"},"description":"Up to 100 products."}}}]},"examples":{"one":{"summary":"One product","value":{"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"}},"several":{"summary":"Several products","value":{"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"}]}}}}}},"responses":{"200":{"description":"Saved.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"upserted":{"type":"integer","description":"How many products were saved."}}},"meta":{"type":"object","properties":{"errors":{"type":"array","items":{"type":"string"},"description":"Products that couldn't be saved, each with the reason."}}}}}}}},"400":{"description":"The request isn't valid, or none of the products could be saved. Then `meta.errors` lists each reason.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/groups":{"get":{"operationId":"listGroups","tags":["Groups"],"summary":"List groups","description":"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.","security":[{"bearerAuth":["contacts:read"]}],"responses":{"200":{"description":"The groups.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"built_in":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}},"example":{"data":[{"id":"cm4u2xh4k0011l208y6u8i0op","name":"Workshop regulars","description":"Booked three or more classes","built_in":false,"created_at":"2026-09-02T10:00:00.000Z","updated_at":"2026-10-01T08:30:00.000Z"}]}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/designs":{"get":{"operationId":"listDesigns","tags":["Campaigns"],"summary":"List designs","description":"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.","security":[{"bearerAuth":["campaigns:write"]}],"parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"description":"A page of designs.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"subject":{"type":["string","null"]},"folder":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}},"meta":{"$ref":"#/components/schemas/PageMeta"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/campaigns":{"post":{"operationId":"makeCampaign","tags":["Campaigns"],"summary":"Draft or send a campaign","description":"Without `send`, makes a draft for someone on your team to finish and send in SplashSend. It needs `campaigns:write`.\n\nWith `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.\n\nThe 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.","security":[{"bearerAuth":["campaigns:write"]},{"bearerAuth":["campaigns:send"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":200,"description":"What the campaign is called in SplashSend. Defaults to the design's name."},"subject":{"type":"string","maxLength":300,"description":"The subject line. Defaults to the one saved with the design."},"preview_text":{"type":"string","maxLength":150,"description":"The line shown after the subject in the inbox. Defaults to the design's own."},"design_id":{"type":"string","description":"A design from GET /designs. Needed to send."},"group_id":{"type":"string","description":"Send it to a group from GET /groups."},"list_id":{"type":"string","description":"Send it to a list from GET /lists."},"everyone":{"type":"boolean","description":"Send it to everyone subscribed."},"send":{"type":"string","description":"`now`, or a time to schedule it for, such as `2026-10-24T09:00:00Z`. Leave it out for a draft. Needs `campaigns:send`."}}},"example":{"design_id":"cm4u2xi5l0012l208u7i9o1pa","group_id":"cm4u2xh4k0011l208y6u8i0op","subject":"Three new glazes are in","send":"now"}}}},"responses":{"201":{"description":"Drafted, sent or scheduled.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"subject":{"type":["string","null"]},"status":{"type":"string","enum":["draft","scheduled","sending","sent"]},"scheduled_at":{"type":["string","null"],"format":"date-time","description":"When a scheduled campaign goes."},"edit_url":{"type":"string","format":"uri","description":"Where someone on your team opens it."}}},"meta":{"type":"object","properties":{"sent":{"type":"boolean","description":"True when it's on its way."},"scheduled":{"type":"boolean"},"test_mode":{"type":"boolean","description":"Present when the app is in test mode and nothing was sent."},"note":{"type":"string"}}}},"example":{"data":{"id":"cm4u2xg3j0010l208t5y7u9io","name":"Autumn glazes","subject":"Three new glazes are in","status":"sending","scheduled_at":null,"edit_url":"https://splashsend.com/dashboard/campaigns/cm4u2xg3j0010l208t5y7u9io"},"meta":{"sent":true,"scheduled":false}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"A permission is missing, or your plan's sends for the month are used up (`plan_limit_reached`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/webhooks":{"get":{"operationId":"listSubscriptions","tags":["Webhook subscriptions"],"summary":"List this app's subscriptions","description":"The subscriptions this app holds on this account, newest first. `active` is false while one is paused, after a day of failed deliveries.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"The subscriptions.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Subscription"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"subscribe","tags":["Webhook subscriptions"],"summary":"Subscribe to events","description":"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.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url","events"],"properties":{"url":{"type":"string","format":"uri","description":"An https address. Events can't be sent to internal or private addresses."},"events":{"type":"array","items":{"type":"string","enum":["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"]},"description":"The events to send, as Webhooks lists them."},"event_name":{"type":"string","description":"Only this one of your own events, with `event.recorded`."},"step_note":{"type":"string","maxLength":500,"description":"Only Notify your systems steps with this note, with `drip.step`."}}},"example":{"url":"https://hooks.example.com/splashsend/123","events":["event.recorded"],"event_name":"Class booked"}}}},"responses":{"201":{"description":"Subscribed.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"allOf":[{"$ref":"#/components/schemas/Subscription"},{"type":"object","properties":{"secret":{"type":"string","description":"Sign-checks each message (X-SplashSend-Signature). Shown once."}}}]}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/IdempotencyInProgress"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/webhooks/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The subscription's id."}],"get":{"operationId":"getSubscription","tags":["Webhook subscriptions"],"summary":"Read a subscription","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"The subscription.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Subscription"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"unsubscribe","tags":["Webhook subscriptions"],"summary":"End a subscription","description":"Nothing more is sent to it, and its delivery log goes with it.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Ended.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean","const":true}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/report-cards":{"get":{"operationId":"listReportCards","tags":["Report cards"],"summary":"List this app's report cards","security":[{"bearerAuth":["events:write"]}],"responses":{"200":{"description":"The cards, oldest first.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ReportCard"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/report-cards/{key}":{"parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,60}$"},"example":"classes-by-week","description":"The app's own key for the card."}],"get":{"operationId":"getReportCard","tags":["Report cards"],"summary":"Read a report card","security":[{"bearerAuth":["events:write"]}],"responses":{"200":{"description":"The card.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ReportCard"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"put":{"operationId":"saveReportCard","tags":["Report cards"],"summary":"Make or replace a report card","description":"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.\n\n`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.","security":[{"bearerAuth":["events:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportCardInput"},"example":{"name":"Classes booked, by class","description":"Bookings made through Hearth in these dates.","event":"Class booked","split":"property","property":"class","chart":"bars"}}}},"responses":{"200":{"description":"Replaced.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ReportCard"}}}}}},"201":{"description":"Made.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ReportCard"}}}}}},"400":{"description":"The card isn't valid (`invalid_request`), or the app already keeps 20 cards on the account (`limit_reached`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"removeReportCard","tags":["Report cards"],"summary":"Remove a report card","description":"It comes off every dashboard it's on.","security":[{"bearerAuth":["events:write"]}],"responses":{"200":{"description":"Removed.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"deleted":{"type":"boolean","const":true}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/drip-steps":{"get":{"operationId":"listDripSteps","tags":["Drip steps"],"summary":"List this app's drip steps","security":[{"bearerAuth":["events:read","contacts:read"]}],"responses":{"200":{"description":"The steps, oldest first.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DripStep"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/drip-steps/{key}":{"parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,60}$"},"example":"offer-next-class","description":"The app's own key for the step."}],"get":{"operationId":"getDripStep","tags":["Drip steps"],"summary":"Read a drip step","security":[{"bearerAuth":["events:read","contacts:read"]}],"responses":{"200":{"description":"The step.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DripStep"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"put":{"operationId":"saveDripStep","tags":["Drip steps"],"summary":"Make or replace a drip step","description":"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.\n\n`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.","security":[{"bearerAuth":["events:read","contacts:read"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DripStepInput"},"example":{"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"}]}}}},"responses":{"200":{"description":"Replaced.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DripStep"}}}}}},"201":{"description":"Made.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DripStep"}}}}}},"400":{"description":"The step isn't valid (`invalid_request`), or the app already keeps 20 steps on the account (`limit_reached`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"removeDripStep","tags":["Drip steps"],"summary":"Remove a drip step","description":"It leaves the builder's list. A drip that already uses it sends nothing at that step, and the person carries on.","security":[{"bearerAuth":["events:read","contacts:read"]}],"responses":{"200":{"description":"Removed.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"deleted":{"type":"boolean","const":true}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/email-blocks":{"get":{"operationId":"listEmailBlocks","tags":["Email blocks"],"summary":"List this app's email blocks","security":[{"bearerAuth":["data:write"]}],"responses":{"200":{"description":"The blocks, oldest first.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/EmailBlock"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/email-blocks/{key}":{"parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,60}$"},"example":"upcoming-classes","description":"The app's own key for the block."}],"get":{"operationId":"getEmailBlock","tags":["Email blocks"],"summary":"Read an email block","security":[{"bearerAuth":["data:write"]}],"responses":{"200":{"description":"The block.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/EmailBlock"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"put":{"operationId":"saveEmailBlock","tags":["Email blocks"],"summary":"Make or replace an email block","description":"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.","security":[{"bearerAuth":["data:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailBlockInput"},"example":{"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}}}},"responses":{"200":{"description":"Replaced.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/EmailBlock"}}}}}},"201":{"description":"Made.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/EmailBlock"}}}}}},"400":{"description":"The block isn't valid (`invalid_request`), or the app already keeps 20 blocks on the account (`limit_reached`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"removeEmailBlock","tags":["Email blocks"],"summary":"Remove an email block","description":"It leaves the designer's list. A design that already uses it shows nothing there when it sends.","security":[{"bearerAuth":["data:write"]}],"responses":{"200":{"description":"Removed.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"deleted":{"type":"boolean","const":true}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/data":{"get":{"operationId":"listDataSets","tags":["Data sets"],"summary":"List data sets","description":"Every data set that's switched on, with its fields. Use it to find a set's `key`.","security":[{"bearerAuth":["data:read"]}],"responses":{"200":{"description":"The data sets.","content":{"application/json":{"schema":{"type":"object","properties":{"data_sets":{"type":"array","items":{"$ref":"#/components/schemas/DataSetSummary"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"createDataSet","tags":["Data sets"],"summary":"Create a data set","description":"Creates a data set you keep yourself and send rows to. An account can have up to 100 data sets.\n\n`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\".\n\n`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.\n\n`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.\n\nThe response is the new set's `key`.","security":[{"bearerAuth":["data:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":80,"description":"The set's name. Its `key` is made from it."},"description":{"type":"string","maxLength":500},"category":{"type":"string","description":"Where it's filed in the app. Defaults to \"Kept by you\"."},"fields":{"type":"array","maxItems":40,"items":{"$ref":"#/components/schemas/DataFieldInput"},"description":"Its columns, up to 40."},"audienceMode":{"type":"string","enum":["everyone","matched","internal"],"default":"everyone","description":"Who a row is for."},"matchOn":{"$ref":"#/components/schemas/MatchOn"},"joins":{"type":"array","maxItems":4,"items":{"$ref":"#/components/schemas/DataJoin"},"description":"Other sets it joins, up to 4."}}},"example":{"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"}]}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","properties":{"key":{"type":"string","description":"The new set's key."}}}}}},"400":{"description":"The request isn't valid (`invalid_request`), or the account already has 100 data sets (`limit_reached`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/IdempotencyInProgress"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/data/{key}":{"parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string"},"example":"workshops","description":"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."}],"get":{"operationId":"getDataSet","tags":["Data sets"],"summary":"Get a data set","description":"Its fields, and how many rows it has.","security":[{"bearerAuth":["data:read"]}],"responses":{"200":{"description":"The data set.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DataSetSummary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"operationId":"updateDataSet","tags":["Data sets"],"summary":"Update a data set","description":"Changes the fields you send. `name`, `description`, `category` and `matchOn` work on any set. `fields` works only on a set you keep yourself.\n\n`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.\n\n`matchOn` makes it a matched set and says what it matches on, as on create.\n\n`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.","security":[{"bearerAuth":["data:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":80},"description":{"type":"string","maxLength":500},"category":{"type":"string"},"fields":{"type":"array","maxItems":40,"items":{"$ref":"#/components/schemas/DataFieldInput"},"description":"The whole new list of columns."},"matchOn":{"$ref":"#/components/schemas/MatchOn"},"joins":{"type":"array","maxItems":4,"items":{"$ref":"#/components/schemas/DataJoin"},"description":"The whole new list of joins."}}},"example":{"name":"Workshops and classes","description":"Upcoming workshops and classes, with places left."}}}},"responses":{"200":{"description":"The data set, updated.","content":{"application/json":{"schema":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"fields":{"type":"array","items":{"$ref":"#/components/schemas/DataField"}},"joins":{"type":"array","items":{"$ref":"#/components/schemas/DataJoin"}}}}}}},"400":{"description":"The request isn't valid (`invalid_request`), or it changes the fields of a set read from a connected source (`read_only`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"deleteDataSet","tags":["Data sets"],"summary":"Delete a data set","description":"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.","security":[{"bearerAuth":["data:write"]}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","const":true}}}}}},"400":{"description":"The set is built in (`read_only`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/data/{key}/rows":{"parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string"},"example":"workshops","description":"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."}],"get":{"operationId":"listDataRows","tags":["Data sets"],"summary":"List a data set's rows","description":"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.\n\nEach 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.\n\nSome sets leave out rows whose date has passed. Send `include_past=true` to list those too.\n\nWithout `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.","security":[{"bearerAuth":["data:read"]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"How many rows to read."},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0},"description":"How many rows to skip."},{"name":"include_past","in":"query","schema":{"type":"boolean","default":false},"description":"Include rows whose date has passed."}],"responses":{"200":{"description":"A page of rows.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DataRows"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"addDataRows","tags":["Data sets"],"summary":"Add rows","description":"Adds rows to a set you keep yourself, up to 5,000 in one request (4MB). A set holds up to 5,000 rows.\n\nEach 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.\n\n`\"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.\n\nA 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.","security":[{"bearerAuth":["data:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["rows"],"properties":{"rows":{"type":"array","minItems":1,"maxItems":5000,"items":{"$ref":"#/components/schemas/RowValues"},"description":"The rows, each an object of values."},"mode":{"type":"string","enum":["append","replace"],"default":"append","description":"`append` adds the rows after the ones already there. `replace` swaps every row in the set for these."}}},"example":{"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}]}}}},"responses":{"201":{"description":"Saved.","content":{"application/json":{"schema":{"type":"object","properties":{"added":{"type":"integer","description":"How many rows were saved."},"total":{"type":"integer","description":"How many rows the set has now."},"mode":{"type":"string","enum":["append","replace"]},"warnings":{"type":"array","items":{"type":"string"},"description":"Keys that didn't match a field. The field is left out when there are none."}}}}}},"400":{"description":"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`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/IdempotencyInProgress"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"operationId":"updateDataRows","tags":["Data sets"],"summary":"Update rows","description":"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.","security":[{"bearerAuth":["data:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["updates"],"properties":{"updates":{"type":"array","minItems":1,"maxItems":500,"items":{"type":"object","required":["id","values"],"properties":{"id":{"type":"string","description":"The row's id."},"values":{"$ref":"#/components/schemas/RowValues"}}},"description":"Up to 500 changes."}}},"example":{"updates":[{"id":"cm4u5d0re000bl208h4w1s6kc","values":{"places_left":6}}]}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"integer","description":"How many of the ids were found."},"warnings":{"type":"array","items":{"type":"string"},"description":"Keys that didn't match a field. The field is left out when there are none."}}}}}},"400":{"description":"The request isn't valid, sends more than 500 updates, or the set is read from a connected source (`read_only`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"deleteDataRows","tags":["Data sets"],"summary":"Delete rows","description":"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`.","security":[{"bearerAuth":["data:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","maxItems":1000,"items":{"type":"string"},"description":"The ids of the rows to delete."},"all":{"type":"boolean","description":"Delete every row in the set. Send it only when you mean to empty the set."}}},"example":{"ids":["cm4u5d0re000bl208h4w1s6kc","cm4u5d0re000cl208x9j2v3mn"]}}}},"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"removed":{"type":"integer","description":"How many rows were deleted."}}}}}},"400":{"description":"The request sends no ids or more than 1,000, or the set is read from a connected source (`read_only`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/data/{key}/rows/{rowId}":{"parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string"},"example":"workshops","description":"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."},{"name":"rowId","in":"path","required":true,"schema":{"type":"string"},"example":"cm4u5d0re000bl208h4w1s6kc","description":"The row's `id`, from GET /data/{key}/rows."}],"patch":{"operationId":"updateDataRow","tags":["Data sets"],"summary":"Update a row","description":"Changes one row. Send the values that change, inside `values` or on their own. Keys are matched the same way as when adding rows.","security":[{"bearerAuth":["data:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"values":{"$ref":"#/components/schemas/RowValues"}}},"example":{"values":{"places_left":6}}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"integer","const":1},"warnings":{"type":"array","items":{"type":"string"},"description":"Keys that didn't match a field. The field is left out when there are none."}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"deleteDataRow","tags":["Data sets"],"summary":"Delete a row","description":"Deletes one row from a set you keep yourself.","security":[{"bearerAuth":["data:write"]}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","const":true}}}}}},"400":{"description":"The set is read from a connected source (`read_only`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}}},"webhooks":{"contact.subscribed":{"post":{"tags":["Webhooks (outbound)"],"summary":"Person subscribed","description":"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}.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"},"example":{"id":"evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s","event":"contact.subscribed","timestamp":"2026-10-09T10:00:00.000Z","data":{"contactId":"cm4u2x8yq0003l208f1k9d7vz","email":"sam@example.com","name":"Sam Lee"}}}}},"responses":{"2XX":{"description":"Any 2xx status acknowledges the message."}}}},"contact.unsubscribed":{"post":{"tags":["Webhooks (outbound)"],"summary":"Person unsubscribed","description":"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.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"},"example":{"id":"evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s","event":"contact.unsubscribed","timestamp":"2026-10-09T10:00:00.000Z","data":{"contactId":"cm4u2x8yq0003l208f1k9d7vz","email":"sam@example.com"}}}}},"responses":{"2XX":{"description":"Any 2xx status acknowledges the message."}}}},"contact.updated":{"post":{"tags":["Webhooks (outbound)"],"summary":"A person's details changed","description":"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.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"},"example":{"id":"evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s","event":"contact.updated","timestamp":"2026-10-09T10:00:00.000Z","data":{"contactId":"cm4u2x8yq0003l208f1k9d7vz","email":"sam@example.com","changed":["name","Birthday"]}}}}},"responses":{"2XX":{"description":"Any 2xx status acknowledges the message."}}}},"campaign.sent":{"post":{"tags":["Webhooks (outbound)"],"summary":"Campaign sent","description":"A campaign finished sending. `totalSent` is how many emails it sent.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"},"example":{"id":"evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s","event":"campaign.sent","timestamp":"2026-10-09T10:00:00.000Z","data":{"campaignId":"cm4t9k2ab0001qz0870m3fh2c","name":"October newsletter","totalSent":1250}}}}},"responses":{"2XX":{"description":"Any 2xx status acknowledges the message."}}}},"email.delivered":{"post":{"tags":["Webhooks (outbound)"],"summary":"Email delivered","description":"The receiving mail server accepted an email.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"},"example":{"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"}}}}},"responses":{"2XX":{"description":"Any 2xx status acknowledges the message."}}}},"email.opened":{"post":{"tags":["Webhooks (outbound)"],"summary":"Email opened","description":"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.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"},"example":{"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"}}}}},"responses":{"2XX":{"description":"Any 2xx status acknowledges the message."}}}},"email.clicked":{"post":{"tags":["Webhooks (outbound)"],"summary":"Link clicked","description":"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.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"},"example":{"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"}}}}},"responses":{"2XX":{"description":"Any 2xx status acknowledges the message."}}}},"email.bounced":{"post":{"tags":["Webhooks (outbound)"],"summary":"Email bounced","description":"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.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"},"example":{"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"}}}}},"responses":{"2XX":{"description":"Any 2xx status acknowledges the message."}}}},"email.complained":{"post":{"tags":["Webhooks (outbound)"],"summary":"Marked as spam","description":"Someone reported an email as spam. They're unsubscribed and their address is stopped. contact.unsubscribed is sent too, if they were subscribed.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"},"example":{"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"}}}}},"responses":{"2XX":{"description":"Any 2xx status acknowledges the message."}}}},"order.placed":{"post":{"tags":["Webhooks (outbound)"],"summary":"Order placed","description":"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.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"},"example":{"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"}}}}},"responses":{"2XX":{"description":"Any 2xx status acknowledges the message."}}}},"order.fulfilled":{"post":{"tags":["Webhooks (outbound)"],"summary":"Order shipped","description":"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.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"},"example":{"id":"evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s","event":"order.fulfilled","timestamp":"2026-10-09T10:00:00.000Z","data":{"orderId":"cm4u7r3ty000dl208k2p8v6nb","source":"shopify","contactId":"cm4u2x8yq0003l208f1k9d7vz","email":"sam@example.com"}}}}},"responses":{"2XX":{"description":"Any 2xx status acknowledges the message."}}}},"submission.created":{"post":{"tags":["Webhooks (outbound)"],"summary":"Form answer / review received","description":"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.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"},"example":{"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"}}}}},"responses":{"2XX":{"description":"Any 2xx status acknowledges the message."}}}},"drip.enrolled":{"post":{"tags":["Webhooks (outbound)"],"summary":"Someone started a drip","description":"Someone started a drip. `trigger` is what started it, such as `contact_subscribed` or `custom_event`.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"},"example":{"id":"evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s","event":"drip.enrolled","timestamp":"2026-10-09T10:00:00.000Z","data":{"dripId":"cm4r0d2ef0004wx07c9n1h6ps","dripName":"Welcome series","contactId":"cm4u2x8yq0003l208f1k9d7vz","trigger":"contact_subscribed"}}}}},"responses":{"2XX":{"description":"Any 2xx status acknowledges the message."}}}},"drip.step":{"post":{"tags":["Webhooks (outbound)"],"summary":"A drip reached a Notify your systems step","description":"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.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"},"example":{"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"}}}}},"responses":{"2XX":{"description":"Any 2xx status acknowledges the message."}}}},"form.submitted":{"post":{"tags":["Webhooks (outbound)"],"summary":"Form submitted","description":"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.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"},"example":{"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"}}}}}},"responses":{"2XX":{"description":"Any 2xx status acknowledges the message."}}}},"event.recorded":{"post":{"tags":["Webhooks (outbound)"],"summary":"An event your systems sent","description":"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.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"},"example":{"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"}}}}},"responses":{"2XX":{"description":"Any 2xx status acknowledges the message."}}}},"webhook.test":{"post":{"tags":["Webhooks (outbound)"],"summary":"Test message","description":"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.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEnvelope"},"example":{"id":"evt_Q2xh5M8d0Tq1pZ7kVbN3yR6s","event":"webhook.test","timestamp":"2026-10-09T10:00:00.000Z","data":{"test":true,"message":"This is a test event from SplashSend."}}}}},"responses":{"2XX":{"description":"Any 2xx status acknowledges the message."}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Your SplashSend API key (`sk_live_…`)."}},"parameters":{"Page":{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1},"description":"The page to read, from 1."},"Limit":{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25},"description":"How many to read on a page, up to 100."},"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"A key of your own that makes a retry safe. Send a new key with each new request and the same key when you retry it."}},"responses":{"BadRequest":{"description":"The request isn't valid. `error.code` is `validation_error` or `invalid_request`, and `error.message` says what to change.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"The API key is missing, wrong or revoked (`unauthorized`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"The API key doesn't have the scope this endpoint needs (`forbidden`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"The person, list, data set or row isn't in your account (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"IdempotencyInProgress":{"description":"A request with this Idempotency-Key is still running (`idempotency_in_progress`). Retry after the `Retry-After` header's 5 seconds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"Retry-After":{"description":"Seconds to wait: 5.","schema":{"type":"integer"}}}},"IdempotencyKeyReused":{"description":"This Idempotency-Key was used for a different request (`idempotency_key_reused`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"PayloadTooLarge":{"description":"The body is over the size limit (`payload_too_large`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"The key made more than 100 requests in a minute (`rate_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"Retry-After":{"description":"Seconds to wait: 60.","schema":{"type":"integer"}}}}},"schemas":{"Error":{"type":"object","description":"The body of every error response.","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["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"],"description":"A fixed code your code can act on."},"message":{"type":"string","description":"What went wrong, in words you can show a person."}}}}},"StepField":{"type":"object","description":"A setting the account fills in on an app's drip step.","required":["key","label"],"properties":{"key":{"type":"string","pattern":"^[a-z][a-z0-9_]{0,39}$","description":"Its key in `values`."},"label":{"type":"string","maxLength":60,"description":"The words the account sees."},"type":{"type":"string","enum":["text","number","choice"],"default":"text"},"options":{"type":"array","items":{"type":"string"},"description":"With `type: choice`: 2 to 20 options."},"required":{"type":"boolean","default":false,"description":"The drip can't go live with it empty."}}},"DripStepInput":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":60,"description":"Its name in the drip builder."},"description":{"type":"string","maxLength":200,"description":"What it does, shown when it's chosen."},"fields":{"type":"array","maxItems":6,"items":{"$ref":"#/components/schemas/StepField"}}}},"DripStep":{"type":"object","description":"One of this app's drip steps.","properties":{"key":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"fields":{"type":"array","items":{"$ref":"#/components/schemas/StepField"}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"EmailBlockInput":{"type":"object","required":["name","data_set","title"],"properties":{"name":{"type":"string","maxLength":60,"description":"Its name in the designer."},"description":{"type":"string","maxLength":200},"data_set":{"type":"string","description":"The key of a data set this app made."},"title":{"type":"string","description":"The field each card's title shows."},"text":{"type":"string","description":"The field each card's line of text shows."},"image":{"type":"string","description":"The field holding each card's picture address."},"link":{"type":"string","description":"The field holding the address each card's button opens."},"button":{"type":"string","maxLength":30,"description":"The words on each card's button. Needs `link`. Defaults to View."},"count":{"type":"integer","minimum":1,"maximum":6,"default":3,"description":"How many cards at most."}}},"EmailBlock":{"type":"object","description":"One of this app's email blocks.","properties":{"key":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"data_set":{"type":"string"},"title":{"type":"string"},"text":{"type":["string","null"]},"image":{"type":["string","null"]},"link":{"type":["string","null"]},"button":{"type":["string","null"]},"count":{"type":"integer"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"ReportCardInput":{"type":"object","description":"A card for Reports, made from one of your events.","required":["name","event"],"properties":{"name":{"type":"string","maxLength":80,"description":"Its title in Reports."},"description":{"type":"string","maxLength":200,"description":"A line under the title saying what it shows."},"event":{"type":"string","maxLength":120,"description":"The name of the event it reads, as you send it to POST /events."},"split":{"type":"string","enum":["none","day","week","property"],"default":"week","description":"How its figure is split."},"property":{"type":"string","maxLength":60,"description":"With `split: property`: the event detail to split by, such as `class`. Events sent without it are under Not given."},"sum":{"type":"string","maxLength":60,"description":"Add up this number from each event, such as `price`, instead of counting them. An event without a number there adds nothing."},"money":{"type":"boolean","default":false,"description":"With `sum`: show the total as money, in the account's currency."},"chart":{"type":"string","enum":["number","line","bars","table"],"description":"How it's drawn. Defaults to number with no split, line by day or week, and bars by a detail."}}},"ReportCard":{"type":"object","description":"One of this app's report cards.","properties":{"key":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"event":{"type":"string"},"split":{"type":"string","enum":["none","day","week","property"]},"property":{"type":["string","null"]},"sum":{"type":["string","null"]},"money":{"type":"boolean"},"chart":{"type":"string","enum":["number","line","bars","table"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"example":{"key":"classes-by-class","name":"Classes booked, by class","description":"Bookings made through Hearth in these dates.","event":"Class booked","split":"property","property":"class","sum":null,"money":false,"chart":"bars","created_at":"2026-10-18T17:00:00.000Z","updated_at":"2026-10-18T17:00:00.000Z"}},"Subscription":{"type":"object","description":"One of an app's webhook subscriptions.","properties":{"id":{"type":"string"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string"}},"event_name":{"type":["string","null"],"description":"The one of your own events it's narrowed to, with event.recorded."},"step_note":{"type":["string","null"],"description":"The step note it's narrowed to, with drip.step."},"active":{"type":"boolean","description":"False while it's paused after a day of failed deliveries, or switched off in Setup."},"paused_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"}},"example":{"id":"cm4u9v6wx000hl208s3f9o7zc","url":"https://hooks.example.com/splashsend/123","events":["event.recorded"],"event_name":"Class booked","step_note":null,"active":true,"paused_at":null,"created_at":"2026-10-18T15:30:00.000Z"}},"PageMeta":{"type":"object","description":"Paging details for a list.","properties":{"page":{"type":"integer","description":"This page's number, from 1."},"limit":{"type":"integer","description":"How many fit on a page."},"total":{"type":"integer","description":"How many there are in all."},"totalPages":{"type":"integer","description":"How many pages there are."}}},"Person":{"type":"object","description":"A person, as every People endpoint returns them.","properties":{"id":{"type":"string","description":"The person's id."},"email":{"type":"string","format":"email"},"name":{"type":"string","description":"Their full name, or an empty string when it isn't known."},"firstName":{"type":["string","null"]},"lastName":{"type":["string","null"]},"source":{"type":"string","description":"Where they first came from."},"subscriptionStatus":{"type":"string","enum":["subscribed","unsubscribed"]},"suppressed":{"type":"boolean","description":"True when the address is stopped because it bounced or was reported as spam. A stopped address is never emailed, whatever `subscriptionStatus` says."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"PersonWithProperties":{"description":"A person with their details.","allOf":[{"$ref":"#/components/schemas/Person"},{"type":"object","properties":{"properties":{"type":"object","additionalProperties":{"type":"string"},"description":"Their details, keyed by name. Only details with a value are included."}}}]},"PersonResult":{"type":"object","description":"What POST /contacts returns.","properties":{"data":{"$ref":"#/components/schemas/Person"},"meta":{"type":"object","properties":{"created":{"type":"boolean","description":"True when they're new."},"subscribed":{"type":"boolean","description":"True when this request subscribed them."},"suppressed":{"type":"boolean","description":"True when the address is stopped, so it wasn't subscribed."},"keptOptOut":{"type":"boolean","description":"True when they had unsubscribed and still are, because `resubscribe` wasn't true."}}},"warnings":{"type":"array","items":{"type":"string"},"description":"Notes for you, such as a kept opt-out or details that were skipped. The field is left out when there are none."}}},"PropertyValues":{"type":"object","description":"Details 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.","additionalProperties":{"type":["string","number","boolean","array","null"],"items":{"type":["string","number"]}}},"PropertyValue":{"type":"object","description":"One of your details, with a person's value.","properties":{"id":{"type":"string","description":"The detail's id."},"name":{"type":"string","description":"The detail's name."},"type":{"type":"string","description":"What kind of value it holds, such as `text`, `date` or `list`."},"value":{"type":"string","description":"Their value, as text. Empty when they have none."}}},"Profile":{"description":"A person with everything SplashSend knows about them.","allOf":[{"$ref":"#/components/schemas/Person"},{"type":"object","properties":{"consent":{"type":"object","properties":{"email":{"type":"object","properties":{"status":{"type":"string","enum":["subscribed","unsubscribed"]},"mailable":{"type":"boolean","description":"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."},"consentedAt":{"type":["string","null"],"format":"date-time","description":"When they agreed to hear from you."},"consentMethod":{"type":["string","null"],"description":"How they agreed, such as `signup_form`, `import`, `shopify_sync` or `api`."},"unsubscribedAt":{"type":["string","null"],"format":"date-time"},"unsubscribeReason":{"type":["string","null"],"description":"The reason they gave for leaving, when they gave one."},"suppressedAt":{"type":["string","null"],"format":"date-time","description":"When their address was stopped."},"suppressionReason":{"type":["string","null"],"description":"Why it was stopped. One of `hard_bounce`, `soft_bounce_limit` and `complaint`."},"blockedByLists":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}}},"description":"The lists marked never to be emailed that they're on. Sent only to a key with `lists:read`."}}},"sms":{"type":"object","properties":{"phone":{"type":["string","null"],"description":"Their mobile number in international format, such as +447700900123."},"status":{"type":"string","enum":["not_subscribed","subscribed","unsubscribed"]},"consentedAt":{"type":["string","null"],"format":"date-time","description":"When they agreed to texts."},"consentMethod":{"type":["string","null"],"description":"How they agreed, such as `signup_form`, `shopify_sync` or `api`."},"unsubscribedAt":{"type":["string","null"],"format":"date-time"},"suppressedAt":{"type":["string","null"],"format":"date-time","description":"When texts to their number were stopped."},"suppressionReason":{"type":["string","null"],"description":"Why they were stopped. One of `invalid_number`, `carrier_rejected` and `stop`."}}},"topics":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"subscribed":{"type":"boolean"}}},"description":"The topics on your preferences page that they've chosen to be in or out of."}}},"details":{"type":"array","description":"Their details that have a value.","items":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","description":"What kind of value it holds, such as `text`, `date` or `list`."},"value":{"type":"string"},"source":{"type":"string","enum":["manual","auto","guessed"],"description":"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":{"type":"object","description":"The emails sent to them, counted the way Reports counts them.","properties":{"sent":{"type":"integer","description":"Emails sent to them. An email sent again after a failed try counts once."},"delivered":{"type":"integer"},"bounced":{"type":"integer"},"opened":{"type":"integer","description":"Emails they opened."},"clicked":{"type":"integer","description":"Emails they clicked a link in. A click made by link-checking software isn't counted."},"lastSentAt":{"type":["string","null"],"format":"date-time"}}},"recentActivity":{"type":"array","items":{"$ref":"#/components/schemas/ActivityEvent"},"description":"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":{"type":"object","description":"Their orders, from a Shopify store or sent with POST /orders. Sent only to a key with `orders:read`.","properties":{"count":{"type":"integer","description":"Their sales. An order refunded in full, voided, cancelled or failed isn't one."},"revenue":{"type":"number","description":"What their sales came to."},"currency":{"type":["string","null"],"description":"The currency of their latest sale."},"recent":{"type":"array","description":"Their 10 most recent orders, newest first, refunded and cancelled ones included.","items":{"type":"object","properties":{"id":{"type":"string","description":"The order's id in SplashSend."},"order_id":{"type":["string","null"],"description":"The shop's order number."},"total":{"type":"number"},"currency":{"type":"string"},"status":{"type":["string","null"],"description":"The order's status, as the shop sent it."},"order_date":{"type":"string","format":"date-time"},"source":{"type":"string","enum":["shopify","api"],"description":"`shopify` for a Shopify store's order, `api` for one sent with POST /orders."},"is_sale":{"type":"boolean","description":"False when it was refunded in full, voided, cancelled or failed."},"credited_to_email":{"type":"boolean","description":"True when it's credited to an email they were sent."}}}}}},"data":{"type":"array","description":"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`.","items":{"type":"object","properties":{"set":{"type":"string","description":"The data set's key."},"name":{"type":"string","description":"The data set's name."},"values":{"type":"object","additionalProperties":{"type":"string"},"description":"The row's values keyed by column name, written the way an email prints them."},"through":{"type":"string","description":"For a joined row, the key of the set whose row joins to it. Left out for their own rows."}}}}}}]},"ImportContactInput":{"type":"object","description":"One person in an import.","required":["email"],"properties":{"email":{"type":"string","format":"email","description":"Their email address."},"firstName":{"type":"string","description":"Their first name."},"lastName":{"type":"string","description":"Their last name."},"name":{"type":"string","description":"Their full name, split into first and last. Ignored when `firstName` or `lastName` is sent."},"source":{"type":"string","description":"Where they came from. Used only for someone new."},"subscriptionStatus":{"type":"string","enum":["subscribed","unsubscribed"],"description":"How someone new arrives. Defaults to `subscribed`. Someone already here keeps their status."},"properties":{"$ref":"#/components/schemas/PropertyValues"}}},"ImportResult":{"type":"object","description":"What POST /contacts/import returns.","properties":{"data":{"type":"object","properties":{"created":{"type":"integer","description":"People added."},"updated":{"type":"integer","description":"People already here, updated."},"failed":{"type":"integer","description":"Rows that weren't saved."},"addedToList":{"type":"integer","description":"People added to the list. The field is there only when you sent `listId`."}}},"meta":{"type":"object","properties":{"startAutomations":{"type":"boolean","description":"Whether automations ran."},"errors":{"type":"array","description":"Up to 50 rows that failed.","items":{"type":"object","properties":{"index":{"type":"integer","description":"The row's position in `contacts`, from 0."},"email":{"type":"string","description":"The row's email address, when it had one."},"message":{"type":"string","description":"Why it failed."}}}},"unknownProperties":{"type":"array","items":{"type":"string"},"description":"Detail names that aren't in your account, up to 50."},"planLimitReached":{"type":"boolean","description":"True when the import stopped at your plan's people limit."},"stoppedAtIndex":{"type":"integer","description":"The row the import stopped at. It and the rows after it weren't added."}}},"warnings":{"type":"array","items":{"type":"string"},"description":"Notes about details that were skipped, or a list that couldn't be updated. The field is left out when there are none."}}},"Event":{"type":"object","description":"A recorded event.","properties":{"id":{"type":"string","description":"The event's id."},"name":{"type":"string"},"email":{"type":"string","format":"email","description":"The person's email address, sent only to a key with `contacts:read`."},"properties":{"type":["object","null"],"additionalProperties":true,"description":"What was sent with it."},"occurred_at":{"type":"string","format":"date-time"},"external_id":{"type":["string","null"],"description":"Your id for it, when you sent one."}}},"ActivityEvent":{"type":"object","description":"Something that happened.","properties":{"id":{"type":"string","description":"The id of what it records: the email, the order, the answer and so on."},"name":{"type":"string","description":"The event, such as `email.opened` or one your systems send."},"kind":{"type":"string","enum":["built_in","custom"],"description":"`built_in` for SplashSend's own events, `custom` for the ones your systems send."},"occurred_at":{"type":"string","format":"date-time"},"email":{"type":["string","null"],"format":"email","description":"Whose it was, sent only to a key with `contacts:read`. Null for an answer given without an email address."},"first_name":{"type":["string","null"],"description":"Their first name, when SplashSend has it. Sent only to a key with `contacts:read`."},"last_name":{"type":["string","null"],"description":"Their last name, when SplashSend has it. Sent only to a key with `contacts:read`."},"properties":{"type":"object","additionalProperties":true,"description":"What it carries. GET /activity/catalogue lists each built-in event's; a custom event's are what your systems sent with it."}}},"CatalogueEvent":{"type":"object","description":"A kind of event SplashSend records.","properties":{"name":{"type":"string","description":"Its name. Webhooks use the same one."},"label":{"type":"string","description":"Its name in plain words."},"group":{"type":"string","description":"Email, People, Shop, Website, Forms or Drips."},"properties":{"type":"array","description":"What it carries in `properties`.","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"type":{"type":"string"}}}},"counting":{"type":"string","description":"How it's counted."},"webhook":{"type":"boolean","description":"Webhooks send it under this name."},"needs":{"type":["string","null"],"description":"The scope needed beyond `events:read` to see it: `contacts:read` or `orders:read`."}}},"Order":{"type":"object","description":"An order sent in with POST /orders.","properties":{"id":{"type":"string","description":"The order's id in SplashSend."},"order_id":{"type":"string","description":"Your order number or id."},"email":{"type":"string","format":"email","description":"The buyer's email address, sent only to a key with `contacts:read`."},"total":{"type":"number"},"currency":{"type":"string"},"status":{"type":["string","null"]},"order_date":{"type":"string","format":"date-time"},"contact_id":{"type":["string","null"],"description":"The buyer's person id. Null when the order has no person."},"source":{"type":"string"}}},"List":{"type":"object","description":"A list you add people to yourself.","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"blockSending":{"type":"boolean","description":"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":{"type":"integer","description":"How many people are on it."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"ListEnvelope":{"type":"object","description":"A list, as the single-list endpoints return it.","properties":{"data":{"$ref":"#/components/schemas/List"}}},"ListInput":{"type":"object","description":"The fields of a list you can set.","properties":{"name":{"type":"string","maxLength":120},"description":{"type":["string","null"],"maxLength":500,"description":"Send `null` to clear it."},"blockSending":{"type":"boolean","description":"Makes it a \"Never email these people\" list. Take someone off the list and they can be emailed again as before."}}},"ListMember":{"type":"object","description":"A person on a list.","properties":{"id":{"type":"string","description":"The person's id."},"email":{"type":"string","format":"email","description":"Their email address, sent only to a key with `contacts:read`."},"name":{"type":"string","description":"Their name, sent only to a key with `contacts:read`."},"subscriptionStatus":{"type":"string","enum":["subscribed","unsubscribed"]},"addedAt":{"type":"string","format":"date-time","description":"When they were added to the list."}}},"MembersInput":{"type":"object","description":"People already in your account, by id, by email address or both. Up to 500 in one request.","properties":{"contactIds":{"type":"array","items":{"type":"string"}},"emails":{"type":"array","items":{"type":"string","format":"email"}}}},"ProductInput":{"type":"object","description":"A product to add or update.","required":["product_id"],"properties":{"product_id":{"oneOf":[{"type":"string"},{"type":"number"}],"description":"Your id for the product. Sending it again updates the product."},"title":{"type":"string","description":"Its name. Needed for a new product."},"price":{"oneOf":[{"type":"number"},{"type":"string"}],"description":"Its price, such as `18` or `\"18.00\"`."},"currency":{"type":"string","description":"A currency code, such as `GBP`."},"image_url":{"type":"string","format":"uri","description":"Its picture, as an http(s) address."},"url":{"type":"string","format":"uri","description":"Its page in your shop. Product blocks link here."},"handle":{"type":"string","description":"Its slug in your shop's addresses. Defaults to `product_id`."},"description":{"type":"string","maxLength":300},"product_type":{"type":"string","description":"Its type, such as `Mugs`."},"vendor":{"type":"string","description":"Who makes it."},"status":{"type":"string","description":"`active`, `draft` or `archived`, in any case. Defaults to `active` for a new product."},"tags":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Its tags, as an array or as one string."},"inventory":{"type":"integer","minimum":0,"description":"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\")."},"in_stock":{"type":"boolean","description":"For a shop that only knows whether it is in stock: `true` or `false`. Ignored when `inventory` is sent."}}},"Product":{"type":"object","description":"A product in your catalogue.","properties":{"product_id":{"type":"string","description":"The id you sent, or Shopify's id for a Shopify product."},"source":{"type":"string","enum":["api","shopify"],"description":"`api` for a product sent with POST /products."},"title":{"type":"string"},"handle":{"type":"string"},"price":{"type":["string","null"],"description":"Its price, as text."},"currency":{"type":["string","null"]},"image_url":{"type":["string","null"]},"url":{"type":["string","null"],"description":"Its page in your shop, when known."},"product_type":{"type":["string","null"]},"vendor":{"type":["string","null"]},"status":{"type":"string","enum":["ACTIVE","DRAFT","ARCHIVED"],"description":"Only `ACTIVE` products appear in emails."},"updated_at":{"type":"string","format":"date-time"}}},"DataSetSummary":{"type":"object","description":"A data set, as GET /data lists it.","properties":{"key":{"type":"string","description":"Its identifier, used in the API and in email tags."},"name":{"type":"string"},"description":{"type":["string","null"]},"source":{"type":"string","enum":["live","manual"],"description":"`manual` for a set you keep yourself, `live` for one read from a connected source."},"writable":{"type":"boolean","description":"True when it takes rows over the API."},"row_count":{"type":"integer"},"updated_at":{"type":["string","null"],"format":"date-time","description":"When its rows last changed, when that's known."},"fields":{"type":"array","items":{"$ref":"#/components/schemas/DataField"}},"joins":{"type":"array","items":{"$ref":"#/components/schemas/DataJoin"},"description":"The other sets it joins."}}},"DataField":{"type":"object","description":"A column of a data set.","properties":{"key":{"type":"string","description":"What rows and email tags call it."},"label":{"type":"string","description":"Its name in the app."},"type":{"type":"string","enum":["words","number","money","date","boolean","link","image"]}}},"DataFieldInput":{"type":"object","description":"A column of a data set you keep yourself.","properties":{"label":{"type":"string","maxLength":60,"description":"Its name. Needed for a new field."},"type":{"type":"string","enum":["words","number","money","date","boolean","link","image"],"default":"words","description":"What it holds."},"key":{"type":"string","description":"Its key, made from the label when left out. On an update, send a field's `key` to keep that field."},"example":{"type":"string","description":"A sample value, shown in the app."}}},"DataJoin":{"type":"object","description":"A join from this set to another one.","required":["field","to","to_field"],"properties":{"field":{"type":"string","description":"One of this set's fields. A request can name it by key or label."},"to":{"type":"string","description":"The other set's key."},"to_field":{"type":"string","description":"The other set's field with the matching values. A request can name it by key or label."}}},"MatchOn":{"type":"object","description":"What a matched set matches on.","required":["detail","field"],"properties":{"detail":{"type":"string","description":"The person's side: `email`, `name`, `source` or the name of one of your details."},"field":{"type":"string","description":"The set's side: one of its fields, by key or label."}}},"RowValues":{"type":"object","description":"A row's values, keyed by a field's key in any case, or by its label.","additionalProperties":{"type":["string","number","boolean","null"]}},"DataRows":{"type":"object","description":"A page of a data set's rows.","properties":{"data_set":{"type":"string","description":"The set's key."},"fields":{"type":"array","items":{"$ref":"#/components/schemas/DataField"},"description":"Its fields. Columns holding email addresses are left out without `contacts:read`."},"rows":{"type":"array","items":{"$ref":"#/components/schemas/DataRow"}},"total":{"type":"integer","description":"How many rows there are to read, for paging."},"limit":{"type":"integer"},"offset":{"type":"integer"}}},"DataRow":{"type":"object","description":"One row: its values keyed by field key, and its `id` in a set you keep yourself.","properties":{"id":{"type":"string","description":"The row's id, in a set you keep yourself."}},"additionalProperties":{"type":["string","number","boolean","null"]}},"WebhookEnvelope":{"type":"object","description":"The body of every webhook message.","properties":{"id":{"type":"string","description":"The message's id, such as `evt_…`. The same on every retry and redelivery."},"event":{"type":"string","description":"The event's name, such as `order.placed`."},"timestamp":{"type":"string","format":"date-time","description":"When this copy was sent. A redelivery has a new one."},"data":{"type":"object","additionalProperties":true,"description":"What happened, as ids and plain values. It never holds a secret."}}}}}}