Developers
API reference
All 110 endpoints of the relaybell REST API, with the scope each needs, its parameters, responses, error codes and examples. Generated from the OpenAPI 3.1.0 document (version 1.2.0).
Base URL https://relaybell.com/api/v1
Jump to an endpoint
Overview
The relaybell REST API: send web push notifications, track events, and manage segments, subscribers, webhooks and automations for a website. Base URL: https://relaybell.com/api/v1.
Authentication. Send a secret API key (it starts with rb_live_) as Authorization: Bearer rb_live_…. Create keys in the dashboard under Settings → Developers → API keys; a key belongs to one website, so calls need no project id. Each key has scopes: an operation's x-scope names the one it needs (full allows everything; reads without a scope work with any key). Keep keys on your server, never in browser code. The routes the browser SDK calls (Browser SDK) take your public Project ID instead. A few routes work only from the signed-in dashboard; they say so.
Errors. Every error is JSON { "error": "code", "message": "…", "request_id": "…" } with a standard HTTP status (message is not always there). Branch on error, never on message. Every response carries the same id in X-Request-Id — quote it when you contact support. A body that isn't valid JSON answers 400 invalid_json; bodies are limited to 1 MB (413 payload_too_large). Every error code: relaybell.com/docs#errors.
Rate limits.
| Requests | Limit | Counted per |
|---|---|---|
| POST /notifications and /notifications/{id}/send (saving a draft isn't counted) | 60 a minute and 600 an hour | API key (dashboard: account) |
| Broadcasts: sends to everyone, a segment, a topic or filters | 30 per clock hour | website |
| Sends to specific users (externalId / externalIds), or transactional with a segment or filters | 1,000 per clock hour | website |
| Test sends to your own devices | 120 per clock hour | website |
| POST / GET /events with an API key | 3,000 a minute | API key |
| Webhook resends and test events | 30 a minute | website |
| Browser SDK routes (/projects/…, click and delivery pings) and /events without a key | 120 a minute | visitor IP |
| POST /ai/suggest | 30 an hour | IP address |
| POST /upload | 60 an hour | IP address |
Going over a request limit answers 429 rate_limited; going over an hourly send budget answers 429 send_limit_reached with limit, kind and retryAfter. Limited routes send RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset; every 429 sends Retry-After (seconds) — wait that long instead of retrying in a loop. Other API-key routes have no request limit of their own.
SDKs. Server SDKs 1.2.0: npm relaybell, PyPI relaybell, Composer relaybell/relaybell-php. The full guide, with the browser SDK, merge tags and webhook signatures: relaybell.com/docs.
Notifications
Send to everyone, a segment, a topic or one of your users; schedule, cancel, keep drafts, and read how each send did.
/notificationsSend a notification
Accepts the send and answers 202; delivery happens in the background. With no targeting fields it goes to every subscriber of the website. Send to one of your own users with externalId (or externalIds), usually with transactional: true; see Send to one user.
With "draft": true it saves a draft instead (201, never sent or counted against limits; a website keeps up to 500 drafts of up to 256 KB each).
Until the account's email address is verified it answers 403 email_not_verified. Limits: 60 a minute and 600 an hour per API key (429 rate_limited), and per website 30 broadcasts and 1,000 sends to specific users per clock hour (429 send_limit_reached, with Retry-After).
Request body · application/json
titlestringNotification title. Required to send (
400 title_required). Supports merge tags such as{name|there}.bodystringMessage text. Supports merge tags.
urlstringPage opened on click. Left out, your Default link is used; your UTM tags are added. Supports merge tags.
iconstringSmall image (URL). Left out, your default notification logo is used.
imagestringLarge picture (URL). Shown mainly by Chrome and Edge on Windows and Android.
badgestringSmall one-colour icon (URL of a PNG with a transparent background, about 96×96 px) for the Android status bar.
actionsarray of ActionUp to 2 buttons.
actions[].actionstringButton id (defaults to
btn1/btn2).actions[].titlestringrequiredButton text (required).
actions[].urlstringOpened when the button is clicked; kept only when it is an http(s) link.
requireInteractionbooleanKeep the notification on screen until the person clicks or closes it (Chrome and Edge on computers).
tagstringA label: a new notification with the same tag replaces one still showing.
externalIdstringSend to one of your own users: the user id the browser SDK linked (
externalIdininit(), oridentify()). Exact match, case matters.externalIdsarray of stringThe same for a list. Used instead of
externalIdwhen both are sent. All empty:400 external_ids_empty(never everyone).transactionalbooleanA message a person is waiting for: goes out even during quiet hours, skips the frequency cap, never listed in the public inbox.
segmentIdstring (uuid)Target a saved segment. Not this website's:
400 segment_not_found.filtersFiltersThe segment filter format, shared by segments, inline send
filters/excludeFilters, schedules and the audience list.filters.matchstringall= every rule must match (default),any= at least one.filters.rulesarray of RuleThe rules.
rules[].fieldstringrequiredcountry(2-letter upper-case code from the visitor's IP),language,url(the subscriber's last page),external_id(orexternalId), ortag:KEYfor one of your tags. An unknown field matches nobody.rules[].opstringeq(default; also used for an unknown op),neq,contains(case-insensitive),in(value is a list),exists,not_exists. For a tag,neqalso matches subscribers without that tag.rules[].valuestring or array of stringCompared as text, exactly:
eq,neqandinare case-sensitive (write country codes in upper case). Not needed forexists/not_exists.topicIdstringOnly subscribers of this topic: its id or its slug (any case). Unknown:
400 unknown_topic.excludeSegmentIdstring (uuid)Leave this segment out. Unknown:
400 segment_not_found.excludeFiltersFiltersThe segment filter format, shared by segments, inline send
filters/excludeFilters, schedules and the audience list.excludeFilters.matchstringall= every rule must match (default),any= at least one.excludeFilters.rulesarray of RuleThe rules.
rules[].fieldstringrequiredcountry(2-letter upper-case code from the visitor's IP),language,url(the subscriber's last page),external_id(orexternalId), ortag:KEYfor one of your tags. An unknown field matches nobody.rules[].opstringeq(default; also used for an unknown op),neq,contains(case-insensitive),in(value is a list),exists,not_exists. For a tag,neqalso matches subscribers without that tag.rules[].valuestring or array of stringCompared as text, exactly:
eq,neqandinare case-sensitive (write country codes in upper case). Not needed forexists/not_exists.sendAtstring (date-time)ISO 8601 time to schedule the send. In the past, or unreadable: sends now.
perTimezonebooleanDeliver at
localTimein each subscriber's time zone (sendAtis ignored).localTimestring"H:MM" or "HH:MM", with
perTimezone.smartTimebooleanDeliver at each subscriber's most-active hour (
sendAtis ignored).ttlintegerSeconds the push service keeps trying an offline device. Default 24 hours, at most 28 days (2419200).
translationsmap of objectPer-language copy keyed by two-letter language:
{ "es": { "title", "body" } }.translations.{key}.titlestringtranslations.{key}.bodystringabobjectA/B test: each browser gets version A (the main fields) or B. Ignored unless
bhas at least one field.ab.bobjectrequiredVersion B; fields left out come from A.
b.titlestringb.bodystringb.urlstringb.imagestringabTestobjectAuto-winner, together with
ab.abTest.autoWinnerbooleanSend both versions to a test group, then the winner to the rest.
abTest.testPercentintegerTest group size, 1–90 (default 20).
abTest.afterHoursintegerHours before the winner goes out, 1–168 (default 4).
draftbooleantruesaves a draft instead of sending (201). The title may then be empty and nothing is checked yet.
Responses
- 201Draft saved (
draft: true).Response fields
oktruerequirednotificationIdstring (uuid)requiredstatus"draft"required
- 202Accepted.
Response fields
oktruerequirednotificationIdstring (uuid)requiredKeep it: the send appears under this id in reports and webhooks, and it is how you cancel it.
statusstringrequiredqueued(sending starts now),scheduled(a futuresendAt, or held until your quiet hours end) orheld_for_review(a new account's first large broadcast; you can cancel it meanwhile).messagestringOnly with
held_for_review: what happens next.
- 400Bad request
invalid_jsontitle_requiredunknown_fieldexternal_ids_emptyunknown_topicsegment_not_found - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
email_not_verifiedaccount_suspendedinsufficient_scope - 409Conflict
too_many_drafts - 413Too large
draft_too_largepayload_too_large - 429Too many requests
rate_limitedsend_limit_reached
Example
curl -X POST https://relaybell.com/api/v1/notifications \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"title": "Flash sale 🔥",
"body": "30% off ends tonight — {name|there}!",
"url": "https://shop.example/sale"
}'Example: one user
curl -X POST https://relaybell.com/api/v1/notifications \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"externalId": "user_123",
"transactional": true,
"title": "Your order has shipped",
"body": "Order #1042 is on its way.",
"url": "https://shop.example/orders/1042"
}'/notificationsList notifications
Newest first, 25 per page (limit up to 100). Without status, everything except drafts is listed. For the next page, send the nextCursor you got as cursor, with the same filters; pages don't shift when new sends arrive.
Query parameters
fromstring (date)First day (YYYY-MM-DD, in
tz) a notification was made on.tostring (date)Last day (YYYY-MM-DD, included).
statusstringOne or more of draft, scheduled, queued, held_for_review, sending, sent, failed, canceled, separated by commas.
qstringSearches titles and texts, in any case.
tzstringIANA time zone for
from/to(default: the website's).limitintegerItems per page, 1–100 (default 25).
cursorstringThe
nextCursorfrom the previous page (send the same filters again). Anything else answers400 invalid_cursor.
Responses
- 200A page of notifications.
Response fields
itemsarray of NotificationListItemrequireditems[].idstring (uuid)requireditems[].titlestringrequireditems[].bodystring or nullitems[].statusstringrequireddraft,scheduled,queued,held_for_review,sending,sent,failed(reason inerror) orcanceled.items[].created_atstring (date-time)requireditems[].scheduled_atstring (date-time) or nullWhen a scheduled send goes (or went) out; null for one that started straight away.
items[].recipientsintegerrequiredBrowsers targeted.
items[].sentintegerrequiredPushes the push services accepted.
items[].deliveredintegerrequiredPushes a device confirmed it showed.
items[].failedintegerPushes that failed.
items[].prunedintegerExpired subscriptions found and removed during the send.
items[].clickedintegerrequireditems[].ctrnumberrequiredclicked ÷ sent, as a percentage with one decimal.
items[].conversionsintegeritems[].revenuenumberitems[].abbooleanWhether it was an A/B test.
items[].targetSummarystringWho it went to, in words: "All subscribers", a segment's name, "Topic: News", "Specific users", …
items[].automationbooleanWhether an automation, a journey or a product alert made it.
items[].errorstring or nullWhy a failed send never went out (
email_not_verified,suspended,segment_missing,review_rejected,dispatch_error), else null.nextCursorstring or nullrequiredSend as
cursor(with the same filters) for the next page; null on the last page.
- 400Bad request
invalid_statusinvalid_rangeinvalid_tzinvalid_cursor - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/notifications?status=sent&limit=25" \
-H "Authorization: Bearer rb_live_xxxxx"/notifications/export.csvExport notifications as CSV
The same filters as the list (no cursor), the same rows in the same order, up to 10,000. Columns: id, created_at, scheduled_at, status, title, target, recipients, sent, failed, clicked, ctr, conversions, revenue. A title starting with =, +, - or @ gets a leading ' so a spreadsheet doesn't run it.
Query parameters
fromstring (date)First day (YYYY-MM-DD).
tostring (date)Last day (YYYY-MM-DD, included).
statusstringComma-separated statuses.
qstringSearch in titles and texts.
tzstringIANA time zone for the days.
Responses
- 200The CSV file.text/csv
- 400Bad request
invalid_statusinvalid_rangeinvalid_tz - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/notifications/export.csv" \
-H "Authorization: Bearer rb_live_xxxxx"/notifications/{id}Get one notification
Everything stored about one notification (or draft — its sendBody holds the saved fields), and how it did.
Path parameters
idstring (uuid)requiredThe notification's id. One that isn't this website's — or isn't a UUID — answers
404 not_found.
Responses
- 200The notification and its report.
Response fields
notificationobjectrequirednotification.idstring (uuid)requirednotification.titlestringrequirednotification.bodystring or nullnotification.urlstring or nullnotification.iconstring or nullnotification.imagestring or nullnotification.badgestring or nullnotification.actionsarray of Action or nullnotification.require_interactionbooleannotification.tagstring or nullnotification.statusstringrequireddraft,scheduled,queued,held_for_review,sending,sent,failed(reason inerror) orcanceled.notification.created_atstring (date-time)requirednotification.scheduled_atstring (date-time) or nullnotification.targetobject or nullHow the people were chosen ({ segmentId | filters, topicId, excludeSegmentId | excludeFilters, … }); null = everyone.
notification.targetSummarystringnotification.automationbooleannotification.automation_idstring (uuid) or nullnotification.transactionalbooleannotification.abobject or nullnotification.ab_testobject or nullnotification.translationsobject or nullnotification.ttlinteger or nullnotification.per_timezonebooleannotification.local_timestring or null"HH:MM", "smart", or null.
notification.statsobject or nullnotification.errorstring or nullnotification.recipientsintegernotification.processedintegernotification.sentintegernotification.deliveredintegernotification.failedintegernotification.prunedintegernotification.clickedintegernotification.ctrnumbernotification.conversionsintegernotification.revenuenumberfunnelobjectrequiredfunnel.recipientsintegerfunnel.sentintegerfunnel.deliveredintegerfunnel.clickedintegerfunnel.convertedintegerfunnel.failedintegerfunnel.expiredintegerdevicesarray of objectrequiredsent and clicked by device type (empty once data retention removed the send's event log).
devices[].devicestringdevices[].sentintegerdevices[].clickedintegervariantsarray of objectrequiredFor an A/B send, versions a and b; otherwise empty.
variants[].variantstringvariants[].sentintegervariants[].clickedintegervariants[].ctrnumberlanguagesarray of objectrequiredlanguages[].languagestringTwo letters, or
unknown.languages[].sentintegerlanguages[].clickedintegererrorsarray of objectrequirederrors[].codestringWhat the push service answered (an HTTP status,
timeout, …), orexpired.errors[].countintegerrevenuenumberrequiredcurrencystring or nullrequiredThe website's revenue currency, or null.
sendBodyobject or nullrequiredThe POST /notifications body that makes this send again, or a draft's saved fields. null for automation, journey and product-alert sends.
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended - 404Not found
not_found
Example
curl "https://relaybell.com/api/v1/notifications/5b0c2f7e-1d7a-4c3e-9a51-2f8e6b1d0c42" \
-H "Authorization: Bearer rb_live_xxxxx"/notifications/{id}Change a draft
Changes the fields you send; the others stay as they were. A field sent as null is cleared. Only a draft can be changed (409 not_a_draft).
Path parameters
idstring (uuid)requiredThe draft's id. One that isn't this website's — or isn't a UUID — answers
404 not_found.
Request body · application/json
The same fields as Send a notification.
Responses
- 200The draft now.
Response fields
oktruerequirednotificationIdstring (uuid)requiredstatus"draft"requiredsendBodyobjectrequiredThe draft's fields now.
- 400Bad request
invalid_json - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found - 409Conflict
not_a_draft - 413Too large
draft_too_largepayload_too_large
Example
curl -X PATCH https://relaybell.com/api/v1/notifications/ID \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"title": "Autumn sale — last day"
}'/notifications/{id}Delete a draft
Only a draft can be deleted (409 not_a_draft).
Path parameters
idstring (uuid)requiredThe draft's id. One that isn't this website's — or isn't a UUID — answers
404 not_found.
Responses
- 200Deleted.
Response fields
oktruerequireddeletedtruerequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found - 409Conflict
not_a_draft
Example
curl -X DELETE https://relaybell.com/api/v1/notifications/ID \
-H "Authorization: Bearer rb_live_xxxxx"/notifications/{id}/sendSend a draft
Sends the draft with exactly the checks, limits, quiet hours and sendAt scheduling of a normal send, and answers the same way. Fields in the body replace the draft's for this send. The notification keeps its id; its date becomes the moment it was sent. A sendAt that has passed sends now.
Path parameters
idstring (uuid)requiredThe draft's id. One that isn't this website's — or isn't a UUID — answers
404 not_found.
Request body · application/json · optional
The same fields as Send a notification.
Responses
- 202Accepted.
The same answer as Send a notification.
- 400Bad request
invalid_jsontitle_requiredunknown_fieldexternal_ids_emptyunknown_topicsegment_not_found - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
email_not_verifiedaccount_suspendedinsufficient_scope - 404Not found
not_found - 409Conflict
not_a_draft - 413Too large
payload_too_large - 429Too many requests
rate_limitedsend_limit_reached
Example
curl -X POST https://relaybell.com/api/v1/notifications/ID/send \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"sendAt": "2026-11-01T09:00:00Z"
}'/notifications/{id}/cancelCancel a scheduled send
Stops a send that is still scheduled (a future sendAt, or held for quiet hours) or held_for_review. A send that has already started (including a per-time-zone or smart-time send, which starts at once), has finished, was already canceled, doesn't exist or belongs to another website answers 404 not_scheduled.
Path parameters
idstring (uuid)requiredThe notification id.
Responses
- 200Canceled.
Response fields
oktruerequiredcanceledtruerequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_schedulednot_found
Example
curl -X POST https://relaybell.com/api/v1/notifications/5b0c2f7e-1d7a-4c3e-9a51-2f8e6b1d0c42/cancel \
-H "Authorization: Bearer rb_live_xxxxx"Events
Behavioral events from your server: conversions, automation triggers and product alerts.
/eventsTrack an event
A behavioral event from your server: it can count as a conversion, start or cancel automations, and enter people into journeys. Name the person with externalId (your user id) or subscriberId (relaybell's id); an event that names nobody is stored, but only a product alert can come of it.
Product alerts: back_in_stock or price_drop with a properties.productId and no externalId, subscriberId or endpoint goes to everyone interested in that product (see Product alerts). 3,000 requests a minute per API key.
Request body · application/json
eventstringThe event name (or
name). Cut to 120 characters; missing:400 event_required.namestringAnother spelling of
event.externalIdstringYour user id.
subscriberIdstring (uuid)relaybell's subscriber id.
endpointstringOnly disqualifies a product-alert broadcast; it does not identify the person on this route.
propertiesobjectAnything;
valueorrevenue(a number) makes it a conversion.
Responses
- 202Stored.
Response fields
oktruerequiredeventIdstringrequiredThe stored event's id.
subscriberIdstring (uuid) or nullrequiredThe subscriber it was linked to, or null.
isConversionbooleanrequiredpurchase, order.completed, checkout.completed (or your conversionEvents), or any event with a numeric value / revenue — except cart.updated and checkout.started.
attributionobject or nullrequiredThe notification credited with this conversion (last delivered in the attribution window), or null.
attribution.notificationIdstring (uuid)attribution.valuenumberautomationsTriggeredintegerrequiredsequencesEnrolledintegerrequiredproductAlertsintegerProduct alerts created (back_in_stock / price_drop naming no person).
alertAudienceintegerPeople the product alerts reach.
alertAudienceTruncatedbooleanThe audience was cut at 100,000.
dedupedbooleanA price_drop for this product already went out in the last 10 minutes.
- 400Bad request
invalid_jsonevent_required - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 413Too large
payload_too_large - 429Too many requests
rate_limited
Example
curl -X POST https://relaybell.com/api/v1/events \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"event": "purchase",
"externalId": "user_123",
"properties": {
"value": 79.99,
"orderId": "A123"
}
}'/eventsList recent events
The latest 100 events, newest first. They name people, so this needs subscribers:read.
Responses
- 200The events.
Response fields
eventsarray of objectrequiredevents[].idstringrequiredevents[].namestringrequiredevents[].external_idstring or nullevents[].subscriber_idstring (uuid) or nullevents[].propertiesobjectrequiredevents[].created_atstring (date-time)required
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 429Too many requests
rate_limited
Example
curl "https://relaybell.com/api/v1/events" \
-H "Authorization: Bearer rb_live_xxxxx"Segments
Saved filters to send to (or leave out of a send), and live counts.
/segmentsList segments
Saved filters, newest first.
Responses
- 200The segments.
Response fields
segmentsarray of Segmentrequiredsegments[].idstring (uuid)requiredsegments[].namestringrequiredsegments[].filtersFiltersrequiredThe segment filter format, shared by segments, inline send
filters/excludeFilters, schedules and the audience list.segments[].created_atstring (date-time)required
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/segments" \
-H "Authorization: Bearer rb_live_xxxxx"/segmentsCreate a segment
A segment needs a name and at least one complete rule (a field — a tag: field with its key — and a value, unless the op is exists / not_exists): a segment with no rules would include every subscriber.
Request body · application/json
namestringrequiredRequired (
400 name_required).filtersFiltersrequiredThe segment filter format, shared by segments, inline send
filters/excludeFilters, schedules and the audience list.filters.matchstringall= every rule must match (default),any= at least one.filters.rulesarray of RuleThe rules.
Responses
- 201Created.
Response fields
segmentSegmentrequired
- 400Bad request
invalid_jsonname_requiredsegment_needs_a_rule - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 413Too large
payload_too_large
Example
curl -X POST https://relaybell.com/api/v1/segments \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"name": "Indian Pro users",
"filters": {
"match": "all",
"rules": [
{
"field": "country",
"op": "in",
"value": [
"IN"
]
},
{
"field": "tag:plan",
"op": "eq",
"value": "pro"
}
]
}
}'/segments/{id}Change a segment
Changes the name and/or the filters (with the same rule check). Neither sent: 400 nothing_to_update.
Path parameters
idstring (uuid)requiredThe segment's id. One that isn't this website's — or isn't a UUID — answers
404 not_found.
Request body · application/json
namestringfiltersFiltersThe segment filter format, shared by segments, inline send
filters/excludeFilters, schedules and the audience list.filters.matchstringall= every rule must match (default),any= at least one.filters.rulesarray of RuleThe rules.
Responses
- 200The segment now.
Response fields
segmentSegmentrequired
- 400Bad request
invalid_jsonnothing_to_updatesegment_needs_a_rule - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found - 413Too large
payload_too_large
Example
curl -X PUT https://relaybell.com/api/v1/segments/ID \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"name": "Indian Pro users (all devices)"
}'/segments/{id}Delete a segment
While an enabled RSS feed, an enabled recurring schedule, or a scheduled or queued send uses it (to include or to exclude), it answers 409 segment_in_use with how many of each (feeds, schedules, pending). An unknown id answers { ok: true }.
Path parameters
idstring (uuid)requiredThe segment id (not a UUID:
404 not_found).
Responses
- 200Done.
Response fields
oktruerequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found - 409Conflict
segment_in_use
Example
curl -X DELETE https://relaybell.com/api/v1/segments/ID \
-H "Authorization: Bearer rb_live_xxxxx"/segments/previewCount who matches
How many subscribed browsers match the filters right now (a person on two devices counts twice), optionally within a topic (id or slug; an unknown one counts nobody). Works with any key.
Request body · application/json · optional
filtersFiltersThe segment filter format, shared by segments, inline send
filters/excludeFilters, schedules and the audience list.filters.matchstringall= every rule must match (default),any= at least one.filters.rulesarray of RuleThe rules.
topicIdstringA topic id or slug.
Responses
- 200The count.
Response fields
countintegerrequired
- 400Bad request
invalid_json - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended - 413Too large
payload_too_large
Example
curl -X POST https://relaybell.com/api/v1/segments/preview \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"filters": {
"match": "all",
"rules": [
{
"field": "country",
"op": "in",
"value": [
"IN"
]
},
{
"field": "tag:plan",
"op": "eq",
"value": "pro"
}
]
}
}'/audience/segmentsList segments with their size
Your saved segments with how many subscribed devices each matches now (the same number as /segments/preview; counted for the first 50, later ones are null), when a send last went to it, and what still uses it.
Responses
- 200The segments.
Response fields
segmentsarray of Segmentrequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope
Example
curl "https://relaybell.com/api/v1/audience/segments" \
-H "Authorization: Bearer rb_live_xxxxx"Topics
Preference-center categories subscribers opt in to.
/topicsList topics
Preference-center categories, oldest first, with how many subscribers opted in to each. Subscribers opt in with RelayBell.setTopics(["slug"]) or showPreferences(); target a send with topicId (id or slug).
Responses
- 200The topics.
Response fields
topicsarray of Topicrequiredtopics[].idstring (uuid)requiredtopics[].namestringrequiredtopics[].slugstringrequiredSubscribers opt in by slug; it never changes.
topics[].created_atstring (date-time)requiredtopics[].subscribersintegerrequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/topics" \
-H "Authorization: Bearer rb_live_xxxxx"/topicsCreate a topic
The slug is made from the name when left out. Posting an existing slug renames that topic instead of making a second one.
Request body · application/json
namestringrequiredRequired (
400 name_required).slugstringLetters, digits and dashes.
Responses
- 201Created (or renamed).
Response fields
topicTopicrequired
- 400Bad request
invalid_jsonname_required - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 413Too large
payload_too_large
Example
curl -X POST https://relaybell.com/api/v1/topics \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"name": "Deals",
"slug": "deals"
}'/topics/{id}Rename a topic
Only the name changes: the slug never does, since subscribers opt in by slug.
Path parameters
idstring (uuid)requiredThe topic's id. One that isn't this website's — or isn't a UUID — answers
404 not_found.
Request body · application/json
namestringrequiredRequired (
400 name_required).
Responses
- 200The topic now.
Response fields
topicTopicrequired
- 400Bad request
invalid_jsonname_required - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found - 413Too large
payload_too_large
Example
curl -X PUT https://relaybell.com/api/v1/topics/ID \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"name": "Deals & offers"
}'/topics/{id}Delete a topic
Its opt-ins go with it. An unknown id answers { ok: true }.
Path parameters
idstring (uuid)requiredThe topic id (not a UUID:
404 not_found).
Responses
- 200Done.
Response fields
oktruerequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found
Example
curl -X DELETE https://relaybell.com/api/v1/topics/ID \
-H "Authorization: Bearer rb_live_xxxxx"Subscribers
Export, import, profiles, GDPR access and erasure, and the filterable subscriber list.
/subscribers/exportExport subscribers as CSV
Newest first, up to 100,000. Columns: subscriber_id, external_id, country, language, timezone, status, consent_at, devices (separated by |), tags (key=value;…), created_at.
Responses
- 200The CSV file (attachment; filename="relaybell-subscribers.csv").text/csv
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope
Example
curl "https://relaybell.com/api/v1/subscribers/export" \
-H "Authorization: Bearer rb_live_xxxxx"/subscribers/recentList the newest subscribers
The newest subscribers first.
Query parameters
limitintegerItems per page, 1–100 (default 25).
Responses
- 200The subscribers.
Response fields
subscribersarray of SubscriberRowrequiredsubscribers[].idstring (uuid)requiredsubscribers[].external_idstring or nullsubscribers[].countrystring or nullsubscribers[].languagestring or nullsubscribers[].timezonestring or nullsubscribers[].created_atstring (date-time)requiredsubscribers[].unsubscribed_atstring (date-time) or nullsubscribers[].devicestring or nullThe most recently seen device type.
subscribers[].last_seen_atstring (date-time) or null
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope
Example
curl "https://relaybell.com/api/v1/subscribers/recent" \
-H "Authorization: Bearer rb_live_xxxxx"/subscribers/importImport or enrich subscribers
Creates or enriches subscribers by external id with tags and topics (by slug), up to 10,000 rows; rows without an external id are skipped. Push subscriptions themselves can't be imported: each browser must opt in on your site, and these attributes apply once it does.
Request body · application/json
subscribersarray of objectrequiredAt least one (
400 no_subscribers).subscribers[].externalIdstringrequiredAlso accepted as external_id.
subscribers[].tagsmap of string or number or boolean or nullsubscribers[].topicsarray of stringTopic slugs.
Responses
- 200Processed.
Response fields
oktruerequiredprocessedintegerrequiredRows with an external id.
notestring
- 400Bad request
invalid_jsonno_subscribers - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 413Too large
payload_too_large
Example
curl -X POST https://relaybell.com/api/v1/subscribers/import \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"subscribers": [
{
"externalId": "user_123",
"tags": {
"plan": "pro",
"name": "Jane"
},
"topics": [
"deals"
]
}
]
}'/subscribers/{externalId}/dataExport one subscriber's data (GDPR access)
Everything held about one subscriber (right of access): the stored row, tags, devices, up to 1,000 delivery events and 1,000 behavioral events (also those your server logged under their external id), and product interests.
Path parameters
externalIdstringrequiredYour external id for the person, or relaybell's subscriber id. Unknown:
404 not_found.
Responses
- 200The data.
Response fields
subscriberobjectrequiredThe stored subscriber row.
tagsarray of objectrequiredtags[].keystringtags[].valuestring or nullendpointsarray of objectrequiredendpoints[].idstring (uuid)endpoints[].typestringendpoints[].devicestring or nullendpoints[].user_agentstring or nullendpoints[].created_atstring (date-time)endpoints[].last_seen_atstring (date-time) or nulldeliveryEventsarray of objectrequireddeliveryEvents[].typestringdeliveryEvents[].notification_idstring (uuid) or nulldeliveryEvents[].devicestring or nulldeliveryEvents[].created_atstring (date-time)behavioralEventsarray of objectrequiredbehavioralEvents[].namestringbehavioralEvents[].propertiesobjectbehavioralEvents[].created_atstring (date-time)productInterestsarray of objectrequiredproductInterests[].product_idstringproductInterests[].kindstringproductInterests[].created_atstring (date-time)
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found
Example
curl "https://relaybell.com/api/v1/subscribers/EXTERNAL_ID/data" \
-H "Authorization: Bearer rb_live_xxxxx"/subscribers/{externalId}/profileGet a subscriber's profile
One person: attributes, tags, devices, topics, their last 50 deliveries and clicks (with titles), their last 50 events, and lifetime delivered / clicked counts.
Path parameters
externalIdstringrequiredYour external id for the person, or relaybell's subscriber id. Unknown:
404 not_found.
Responses
- 200The profile.
Response fields
subscriberobjectrequiredsubscriber.idstring (uuid)requiredsubscriber.externalIdstring or nullsubscriber.countrystring or nullsubscriber.languagestring or nullsubscriber.timezonestring or nullsubscriber.createdAtstring (date-time)subscriber.unsubscribedAtstring (date-time) or nullsubscriber.consentAtstring (date-time) or nulltagsarray of objectrequiredtags[].keystringtags[].valuestring or nullendpointsarray of objectrequiredendpoints[].idstring (uuid)endpoints[].devicestring or nullendpoints[].user_agentstring or nullendpoints[].created_atstring (date-time)endpoints[].last_seen_atstring (date-time) or nulltopicsarray of objectrequiredtopics[].namestringtopics[].slugstringsendsarray of objectrequiredsends[].typestringsends[].notification_idstring (uuid) or nullsends[].created_atstring (date-time)sends[].titlestring or nulleventsarray of objectrequiredevents[].namestringevents[].propertiesobjectevents[].created_atstring (date-time)statsobjectrequiredstats.deliveredintegerrequiredstats.clickedintegerrequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found
Example
curl "https://relaybell.com/api/v1/subscribers/user_123/profile" \
-H "Authorization: Bearer rb_live_xxxxx"/subscribers/{externalId}Erase a subscriber (GDPR erasure)
Permanently deletes everything about one subscriber: devices, tags, topics, journeys and their delivery and behavioral events (also those logged under their external id).
Path parameters
externalIdstringrequiredYour external id for the person, or relaybell's subscriber id. Unknown:
404 not_found.
Responses
- 200Erased.
Response fields
oktruerequirederasedtruerequiredsubscriberIdstring (uuid)required
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found
Example
curl -X DELETE https://relaybell.com/api/v1/subscribers/EXTERNAL_ID \
-H "Authorization: Bearer rb_live_xxxxx"/audience/subscribersList and filter subscribers
The dashboard's subscriber list: newest first, paged by cursor, every filter narrowing the list further (they combine with AND). List filters accept a comma-separated value or repeated parameters.
Query parameters
limitintegerItems per page, 1–100 (default 25).
cursorstringThe
nextCursorfrom the previous page (send the same filters again). Anything else answers400 invalid_cursor.qstringA subscriber id (exact) or part of an external id.
statusstringDefault all.
countrystring2-letter codes, comma-separated.
languagestring"en" matches en, en-US, en-GB…; "en-US" matches only en-US.
devicestringdesktop, android, ios, mobile, other (
400 invalid_device).topicstringTopic ids or slugs (up to 5); an unknown one matches nobody.
tagstringkey(has the label) orkey:value/key=value(with that value); up to 10.joinedFromstring (date)Joined on or after this day (YYYY-MM-DD, in
tz).joinedTostring (date)Joined on or before this day.
seenFromstring (date)Last seen on or after this day.
seenTostring (date)Last seen on or before this day.
tzstringIANA time zone for the days (default UTC).
filtersstringSegment rules as JSON, e.g. {"match":"all","rules":[…]} (at most 20 rules;
400 invalid_filters).countstring"1" or "true" adds
total, the number of matches.
Responses
- 200A page of subscribers.
Response fields
itemsarray of AudienceSubscriberrequireditems[].idstring (uuid)requireditems[].externalIdstring or nullitems[].countrystring or nullitems[].languagestring or nullitems[].timezonestring or nullitems[].statusstringrequireditems[].devicestring or nullitems[].devicesarray of stringitems[].topicsarray of objectitems[].tagsarray of objectUp to 20; rb_ labels are left out.
items[].createdAtstring (date-time)requireditems[].lastSeenAtstring (date-time) or nullitems[].unsubscribedAtstring (date-time) or nullnextCursorstring or nullrequiredtotalintegerOnly with
count=1.
- 400Bad request
invalid_statusinvalid_deviceinvalid_rangeinvalid_tzinvalid_filtersinvalid_cursor - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope
Example
curl "https://relaybell.com/api/v1/audience/subscribers" \
-H "Authorization: Bearer rb_live_xxxxx"/audience/subscribers/facetsGet filter options
The values this website's subscribers actually have, with counts: countries, languages, devices, labels (rb_ ones left out; up to 20 values each) and topics.
Responses
- 200The options.
Response fields
countriesarray of objectrequiredcountries[].valuestringcountries[].countintegerlanguagesarray of objectrequiredlanguages[].valuestringlanguages[].countintegerdevicesarray of objectrequireddevices[].valuestringdevices[].countintegertagsarray of objectrequiredtags[].keystringtags[].countintegertags[].valuesarray of stringtopicsarray of objectrequiredtopics[].idstring (uuid)topics[].namestringtopics[].slugstringtopics[].countinteger
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope
Example
curl "https://relaybell.com/api/v1/audience/subscribers/facets" \
-H "Authorization: Bearer rb_live_xxxxx"/audience/subscribers/bulk-tagLabel subscribers in bulk
Adds, changes or removes labels (tags) on up to 1,000 subscribers. At most 20 labels at a time; names are 1–64 characters, values up to 256; names starting with rb_ are reserved. Ids that aren't this website's are skipped.
Request body · application/json
idsarray of string (uuid)requiredSubscriber ids (
400 ids_required,too_many_ids,invalid_id).setmap of string or nullLabels to add or change: { key: value }.
removearray of stringLabel names to remove.
Responses
- 200Done.
Response fields
oktruerequiredupdatedintegerrequired
- 400Bad request
invalid_jsonnothing_to_changetoo_many_labelsinvalid_labelreserved_labelids_requiredtoo_many_idsinvalid_id - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 413Too large
payload_too_large
Example
curl -X POST https://relaybell.com/api/v1/audience/subscribers/bulk-tag \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"ids": [
"e1c9b7a5-3d2f-4e8b-9a6c-0f4d2b8e1a73"
],
"set": {
"plan": "pro"
},
"remove": [
"trial"
]
}'/audience/subscribers/bulk-deleteErase subscribers in bulk
The same full erasure as DELETE /subscribers/{externalId}, for up to 1,000 subscribers. Ids that aren't this website's are skipped.
Request body · application/json
idsarray of string (uuid)required
Responses
- 200Done.
Response fields
oktruerequireddeletedintegerrequired
- 400Bad request
invalid_jsonids_requiredtoo_many_idsinvalid_id - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 413Too large
payload_too_large
Example
curl -X POST https://relaybell.com/api/v1/audience/subscribers/bulk-delete \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"ids": [
"e1c9b7a5-3d2f-4e8b-9a6c-0f4d2b8e1a73"
]
}'Webhooks
Signed POSTs to your server when something happens, with a 30-day delivery log, resend and test events. See Verify a webhook signature.
/webhooksList webhooks
Your webhooks, newest first, with each one's last attempt and health, plus the event names you can subscribe to. Never the secrets.
Responses
- 200The webhooks.
Response fields
webhooksarray of Webhookrequiredwebhooks[].idstring (uuid)requiredwebhooks[].urlstringrequiredwebhooks[].eventsarray of stringrequiredEmpty = every event.
webhooks[].enabledbooleanrequiredwebhooks[].last_statusinteger or nullHTTP status of the last attempt (0 when no answer came back).
webhooks[].last_fired_atstring (date-time) or nullwebhooks[].created_atstring (date-time)requiredwebhooks[].consecutive_failuresintegerrequiredDeliveries failed in a row since the last success.
webhooks[].failing_sincestring (date-time) or nullFirst failure since the last success.
webhooks[].disabled_reasonstring or nullSet when relaybell turned it off.
webhooks[].disabled_atstring (date-time) or nulleventsarray of stringrequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/webhooks" \
-H "Authorization: Bearer rb_live_xxxxx"/webhooksCreate a webhook
The url must start with http:// or https:// (400 invalid_url). One pointing at a private or internal address is saved, but every delivery to it is refused. An empty events list means every event; unknown names are dropped. The secret (whsec_…) is shown only in this answer — store it to verify signatures.
Request body · application/json
urlstringrequiredWhere to POST events.
eventsarray of stringEmpty or left out = every event.
Responses
- 201Created. The only time you see the secret.
Response fields
webhookobjectrequiredwebhook.idstring (uuid)requiredwebhook.urlstringrequiredwebhook.eventsarray of stringrequiredwebhook.secretstringrequiredwebhook.enabledbooleanrequiredwebhook.created_atstring (date-time)required
- 400Bad request
invalid_jsoninvalid_url - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 413Too large
payload_too_large
Example
curl -X POST https://relaybell.com/api/v1/webhooks \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.example/webhooks/relaybell",
"events": [
"notification.clicked"
]
}'/webhooks/{id}Change a webhook
Changes url, events and/or enabled. Turning a webhook that was off back on (enabled: true) clears its failure count and the reason relaybell turned it off. Nothing sent: 400 nothing_to_update.
Path parameters
idstring (uuid)requiredThe webhook's id. One that isn't this website's — or isn't a UUID — answers
404 not_found.
Request body · application/json
urlstringMust start with http:// or https://.
eventsarray of stringenabledboolean
Responses
- 200The webhook now.
Response fields
webhookWebhookrequired
- 400Bad request
invalid_jsoninvalid_urlnothing_to_update - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found - 413Too large
payload_too_large
Example
curl -X PUT https://relaybell.com/api/v1/webhooks/c3e5a7b9-2d4f-4a6c-8e0b-1d3f5a7c9e2b \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"enabled": true
}'/webhooks/{id}Delete a webhook
Removes the webhook and its delivery log. An unknown id also answers { ok: true }.
Path parameters
idstring (uuid)requiredThe webhook id.
Responses
- 200Done.
Response fields
oktruerequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope
Example
curl -X DELETE https://relaybell.com/api/v1/webhooks/ID \
-H "Authorization: Bearer rb_live_xxxxx"/webhooks/{id}/deliveriesList a webhook's deliveries
Every delivery and the result of its last attempt, kept for 30 days: newest first, 25 a page (limit up to 100).
Path parameters
idstring (uuid)requiredThe webhook's id. One that isn't this website's — or isn't a UUID — answers
404 not_found.
Query parameters
limitintegerItems per page, 1–100 (default 25).
cursorstringThe
nextCursorfrom the previous page (send the same filters again). Anything else answers400 invalid_cursor.
Responses
- 200A page of deliveries.
Response fields
deliveriesarray of WebhookDeliveryrequireddeliveries[].idstring (uuid)requiredThe delivery id, also sent as X-Relaybell-Delivery (the same on every retry and resend).
deliveries[].eventstringrequireddeliveries[].statusstringrequiredpending(waiting for its first attempt, or a resend / test waiting to run),retrying,succeeded(the last attempt got a 2xx) orfailed.deliveries[].attemptsintegerrequiredEvery attempt, resends included.
deliveries[].last_status_codeinteger or nullnull when no answer came back (
errorsays why).deliveries[].duration_msinteger or nulldeliveries[].errorstring or nullWhy the last attempt failed, in plain words.
deliveries[].created_atstring (date-time)requireddeliveries[].last_attempt_atstring (date-time) or nulldeliveries[].next_attempt_atstring (date-time) or nullWhen the next retry runs.
deliveries[].delivered_atstring (date-time) or nullnextCursorstring or nullrequired
- 400Bad request
invalid_cursor - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found
Example
curl "https://relaybell.com/api/v1/webhooks/c3e5a7b9-2d4f-4a6c-8e0b-1d3f5a7c9e2b/deliveries?limit=25" \
-H "Authorization: Bearer rb_live_xxxxx"/webhooks/{id}/deliveries/{deliveryId}Get one delivery
The delivery with the exact body that was signed and sent, the last attempt's headers (signatures included, never your secret) and the start of your server's answer.
Path parameters
idstring (uuid)requiredThe webhook's id. One that isn't this website's — or isn't a UUID — answers
404 not_found.deliveryIdstring (uuid)requiredThe delivery id (X-Relaybell-Delivery).
Responses
- 200The delivery.
Response fields
deliveryWebhookDeliveryDetailrequireddelivery.idstring (uuid)requiredThe delivery id, also sent as X-Relaybell-Delivery (the same on every retry and resend).
delivery.eventstringrequireddelivery.statusstringrequiredpending(waiting for its first attempt, or a resend / test waiting to run),retrying,succeeded(the last attempt got a 2xx) orfailed.delivery.attemptsintegerrequiredEvery attempt, resends included.
delivery.last_status_codeinteger or nullnull when no answer came back (
errorsays why).delivery.duration_msinteger or nulldelivery.errorstring or nullWhy the last attempt failed, in plain words.
delivery.created_atstring (date-time)requireddelivery.last_attempt_atstring (date-time) or nulldelivery.next_attempt_atstring (date-time) or nullWhen the next retry runs.
delivery.delivered_atstring (date-time) or nulldelivery.webhook_idstring (uuid)delivery.request_bodystringExactly what was signed and sent, up to 64 KB.
delivery.request_truncatedbooleantrue when the body was longer than 64 KB.
delivery.request_headersobject or nullThe last attempt's headers (content-type, user-agent, x-relaybell-event, x-relaybell-delivery and both signatures). Never your secret.
delivery.response_bodystring or nullThe first 2 KB of your server's last answer.
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found
Example
curl "https://relaybell.com/api/v1/webhooks/c3e5a7b9-2d4f-4a6c-8e0b-1d3f5a7c9e2b/deliveries/f0e1d2c3-b4a5-4697-8869-5a4b3c2d1e0f" \
-H "Authorization: Bearer rb_live_xxxxx"/webhooks/{id}/deliveries/{deliveryId}/resendResend a delivery
One more attempt now, with the same delivery id and body and a fresh timestamp and signature — no retries after it. A waiting retry is replaced. Resends and test events together: 30 a minute per website.
Path parameters
idstring (uuid)requiredThe webhook's id. One that isn't this website's — or isn't a UUID — answers
404 not_found.deliveryIdstring (uuid)requiredThe delivery id.
Responses
- 202Queued.
Response fields
deliveryWebhookDeliveryrequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found - 409Conflict
delivery_in_progressbody_too_large - 429Too many requests
rate_limited
Example
curl -X POST https://relaybell.com/api/v1/webhooks/c3e5a7b9-2d4f-4a6c-8e0b-1d3f5a7c9e2b/deliveries/f0e1d2c3-b4a5-4697-8869-5a4b3c2d1e0f/resend \
-H "Authorization: Bearer rb_live_xxxxx"/webhooks/{id}/testSend a test event
Sends a webhook.test event ({ webhookId, message }) to this webhook once, no retries — even when the webhook is turned off or subscribes to other events. Follow its result with the delivery routes. Shares the 30-a-minute limit with resends.
Path parameters
idstring (uuid)requiredThe webhook's id. One that isn't this website's — or isn't a UUID — answers
404 not_found.
Responses
- 202Queued.
Response fields
deliveryWebhookDeliveryrequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found - 429Too many requests
rate_limited
Example
curl -X POST https://relaybell.com/api/v1/webhooks/c3e5a7b9-2d4f-4a6c-8e0b-1d3f5a7c9e2b/test \
-H "Authorization: Bearer rb_live_xxxxx"Automations
One push per event: cart reminders, back-in-stock and price-drop alerts, welcome-back flows.
/automationsList automations
Newest first, each with sent (all-time deliveries) and its last-30-day numbers.
Responses
- 200The automations.
Response fields
automationsarray of AutomationWithStatsrequiredautomations[].idstring (uuid)requiredautomations[].project_idstring (uuid)automations[].namestringrequiredautomations[].enabledbooleanrequiredautomations[].eventstringrequiredThe event name that starts it.
automations[].conditionsarray of objectrequiredautomations[].actionobjectrequiredautomations[].delay_secondsintegerrequiredautomations[].cancel_onstring or nullautomations[].once_per_subscriberbooleanautomations[].created_atstring (date-time)requiredautomations[].sentintegerrequiredAll-time deliveries.
automations[].stats30dStats30drequiredThe last 30 days, from the notifications it produced.
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/automations" \
-H "Authorization: Bearer rb_live_xxxxx"/automationsCreate an automation
One push to a person when they do something: an event arrives, the optional delay passes, the message goes out. With back_in_stock or price_drop as the event it powers product alerts. Automation pushes follow your quiet hours and frequency cap.
Request body · application/json
eventstringrequiredThe event name that starts it (required:
400 event_required). Up to 120 characters.actionobjectThe push. Merge tags take values from the event's properties:
{product},{url},{price|}…action.titlestringRequired (
400 action_title_requiredon create).action.bodystringaction.urlstringaction.iconstringnamestringUp to 120 characters (default "Automation").
delaySecondsintegerWait before sending (default 0). A newer event from the same person replaces their pending one.
cancelOnstringAn event name that cancels the person's pending push, e.g.
purchase.conditionsarray of objectChecked against the event's properties; all must match. On PUT, left out = kept as they are.
conditions[].fieldstringrequiredconditions[].opstringconditions[].valueanyoncePerSubscriberbooleanAt most once per person, ever.
enabledbooleanDefault true. On PUT, it goes back to true unless you send false.
Responses
- 201Created.
Response fields
automationAutomationrequired
- 400Bad request
invalid_jsonevent_requiredaction_title_required - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 413Too large
payload_too_large
Example
curl -X POST https://relaybell.com/api/v1/automations \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"name": "Cart abandon",
"event": "cart.updated",
"delaySeconds": 3600,
"cancelOn": "purchase",
"conditions": [
{
"field": "cartTotal",
"op": "gte",
"value": 20
}
],
"action": {
"title": "You left {product|something} in your cart 🛒",
"body": "Complete your order before it's gone.",
"url": "{url}"
}
}'/automations/{id}Get an automation
One automation with its all-time deliveries and last-30-day numbers.
Path parameters
idstring (uuid)requiredThe automation's id. One that isn't this website's — or isn't a UUID — answers
404 not_found.
Responses
- 200The automation.
Response fields
automationAutomationWithStatsrequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended - 404Not found
not_found
Example
curl "https://relaybell.com/api/v1/automations/ID" \
-H "Authorization: Bearer rb_live_xxxxx"/automations/{id}Replace an automation
Replaces the automation with the body you send — enabled goes back to true unless you send false — except conditions, which are kept when the body has no conditions key.
Path parameters
idstring (uuid)requiredThe automation's id. One that isn't this website's — or isn't a UUID — answers
404 not_found.
Request body · application/json
The same fields as Create an automation.
Responses
- 200The automation now.
Response fields
automationAutomationrequired
- 400Bad request
invalid_jsonevent_required - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found - 413Too large
payload_too_large
Example
curl -X PUT https://relaybell.com/api/v1/automations/ID \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"name": "Cart abandon",
"event": "cart.updated",
"delaySeconds": 7200,
"cancelOn": "purchase",
"action": {
"title": "Still thinking it over?",
"url": "{url}"
}
}'/automations/{id}Delete an automation
An unknown id answers { ok: true }.
Path parameters
idstring (uuid)requiredThe automation id (not a UUID:
404 not_found).
Responses
- 200Done.
Response fields
oktruerequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found
Example
curl -X DELETE https://relaybell.com/api/v1/automations/ID \
-H "Authorization: Bearer rb_live_xxxxx"Journeys
Multi-step journeys (drip sequences) that start on subscribe or on an event.
/sequencesList journeys
Multi-step journeys (drip sequences), newest first, with their steps, enrollments and last-30-day numbers.
Responses
- 200The journeys.
Response fields
sequencesarray of JourneyWithStatsrequiredsequences[].idstring (uuid)requiredsequences[].project_idstring (uuid)sequences[].namestringrequiredsequences[].enabledbooleanrequiredsequences[].trigger_typestringrequiredsequences[].trigger_eventstring or nullsequences[].cancel_onstring or nullsequences[].created_atstring (date-time)requiredsequences[].stepsarray of JourneySteprequiredsequences[].enrollmentsobjectrequiredsequences[].stats30dStats30drequiredThe last 30 days, from the notifications it produced.
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/sequences" \
-H "Authorization: Bearer rb_live_xxxxx"/sequencesCreate a journey
A subscriber enters when they subscribe (trigger: "subscribe", the default) or send an event (trigger: "event" with triggerEvent), then moves through the steps in order; cancelOn takes them out. Each subscriber enters a journey once, ever. A branch step continues only for people who match its condition.
Request body · application/json
namestringUp to 120 characters (default "Sequence").
triggerstring"subscribe" (default) or "event".
triggerEventstringRequired with "event" (
400 trigger_event_required).cancelOnstringAn event that takes the person out of the journey, e.g.
purchase.enabledbooleanDefault true.
stepsarray of objectAt least one (
400 at_least_one_step_required). On PUT, steps change only when you send them.steps[].idstring (uuid)An existing step's id, to keep it (and its numbers) when editing.
steps[].typestring"message" (default) or "branch".
steps[].delaySecondsintegerThe wait after the previous step (for the first step, after entering).
steps[].titlestringA message step without a title is dropped.
steps[].bodystringsteps[].urlstringsteps[].iconstringsteps[].conditionobjectBranch: the person goes on only if they match (segment rule format).
condition.fieldstringrequiredcondition.opstringcondition.valuestring
Responses
- 201Created (the steps are in GET /sequences/{id}).
Response fields
sequenceJourneyrequired
- 400Bad request
invalid_jsontrigger_event_requiredat_least_one_step_required - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 413Too large
payload_too_large
Example
curl -X POST https://relaybell.com/api/v1/sequences \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"name": "Welcome series",
"trigger": "subscribe",
"cancelOn": "purchase",
"steps": [
{
"delaySeconds": 0,
"title": "Welcome 👋",
"url": "https://shop.example"
},
{
"delaySeconds": 86400,
"title": "Here's how to start"
},
{
"type": "branch",
"condition": {
"field": "tag:plan",
"op": "eq",
"value": "free"
}
},
{
"delaySeconds": 604800,
"title": "A little something for you"
}
]
}'/sequences/{id}Get a journey
The steps with their ids and 30-day numbers, and enrollments { active, completed, canceled }.
Path parameters
idstring (uuid)requiredThe journey's id. One that isn't this website's — or isn't a UUID — answers
404 not_found.
Responses
- 200The journey.
Response fields
sequenceJourneyWithStatsrequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended - 404Not found
not_found
Example
curl "https://relaybell.com/api/v1/sequences/ID" \
-H "Authorization: Bearer rb_live_xxxxx"/sequences/{id}Change a journey
Always replaces name, enabled, trigger and cancelOn (send them all); steps change only when you send them. While nobody is in the journey you can reorder, add and remove steps (send a step's id to keep it and its numbers). While someone is in it, send every existing step back with its id, in the same order and type, and only add new steps at the end — anything else answers 409 structural_change_requires_no_active_enrollments.
Path parameters
idstring (uuid)requiredThe journey's id. One that isn't this website's — or isn't a UUID — answers
404 not_found.
Request body · application/json
The same fields as Create a journey.
Responses
- 200The journey now (without steps).
Response fields
sequenceJourneyrequired
- 400Bad request
invalid_jsontrigger_event_requiredat_least_one_step_required - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found - 409Conflict
structural_change_requires_no_active_enrollments - 413Too large
payload_too_large
Example
curl -X PUT https://relaybell.com/api/v1/sequences/ID \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"name": "Welcome series",
"trigger": "subscribe",
"cancelOn": "purchase",
"enabled": false
}'/sequences/{id}Delete a journey
An unknown id answers { ok: true }.
Path parameters
idstring (uuid)requiredThe journey id (not a UUID:
404 not_found).
Responses
- 200Done.
Response fields
oktruerequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found
Example
curl -X DELETE https://relaybell.com/api/v1/sequences/ID \
-H "Authorization: Bearer rb_live_xxxxx"RSS feeds
New posts from a feed, sent automatically.
/feedsList RSS feeds
RSS / Atom feeds whose new posts are sent automatically (checked about every 15 minutes).
Responses
- 200The feeds.
Response fields
feedsarray of Feedrequiredfeeds[].idstring (uuid)requiredfeeds[].urlstringrequiredfeeds[].enabledbooleanrequiredfeeds[].segment_idstring (uuid) or nullrequiredfeeds[].last_guidstring or nullfeeds[].last_checked_atstring (date-time) or nullfeeds[].created_atstring (date-time)
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/feeds" \
-H "Authorization: Bearer rb_live_xxxxx"/feedsAdd an RSS feed
New posts in the feed are sent to everyone, or to a segment.
Request body · application/json
urlstringrequiredhttp(s) feed URL (
400 invalid_url).segmentIdstring (uuid)A segment of this website (
400 invalid_segment).
Responses
- 201Added.
Response fields
feedFeedrequired
- 400Bad request
invalid_jsoninvalid_urlinvalid_segment - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 413Too large
payload_too_large
Example
curl -X POST https://relaybell.com/api/v1/feeds \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"url": "https://blog.example/feed.xml"
}'/feeds/{id}Change an RSS feed
Turns it on or off, or changes its segment (null = everyone). Nothing sent: 400 nothing_to_update.
Path parameters
idstring (uuid)requiredThe feed's id. One that isn't this website's — or isn't a UUID — answers
404 not_found.
Request body · application/json
enabledbooleansegmentIdstring (uuid) or null
Responses
- 200The feed now.
Response fields
feedobjectrequiredfeed.idstring (uuid)requiredfeed.urlstringrequiredfeed.enabledbooleanrequiredfeed.segment_idstring (uuid) or nullrequired
- 400Bad request
invalid_jsonnothing_to_updateinvalid_segment - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found - 413Too large
payload_too_large
Example
curl -X PUT https://relaybell.com/api/v1/feeds/ID \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"enabled": false
}'/feeds/{id}Delete an RSS feed
An unknown id answers { ok: true }.
Path parameters
idstring (uuid)requiredThe feed id (not a UUID:
404 not_found).
Responses
- 200Done.
Response fields
oktruerequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found
Example
curl -X DELETE https://relaybell.com/api/v1/feeds/ID \
-H "Authorization: Bearer rb_live_xxxxx"Recurring schedules
The same message every hour, day or week.
/schedulesList recurring campaigns
Newest first, with when each runs next and last ran.
Responses
- 200The schedules.
Response fields
schedulesarray of Schedulerequiredschedules[].idstring (uuid)requiredschedules[].project_idstring (uuid)schedules[].namestringrequiredschedules[].contentobjectrequired{ title, body, url, icon, segmentId | filters, topicId, excludeSegmentId | excludeFilters }
schedules[].cadencestringrequiredschedules[].at_timestring or null"HH:MM".
schedules[].day_of_weekinteger or null0–6, 0 = Sunday.
schedules[].timezonestringrequiredschedules[].enabledbooleanrequiredschedules[].next_run_atstring (date-time) or nullschedules[].last_run_atstring (date-time) or nullschedules[].created_atstring (date-time)
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/schedules" \
-H "Authorization: Bearer rb_live_xxxxx"/schedulesCreate a recurring campaign
The same message every hour, day or week, at a time in a time zone, to everyone or to the people the content targets.
Request body · application/json
namestringUp to 120 characters (default "Recurring campaign").
contentobjectrequiredcontent.titlestringrequiredRequired (
400 content_title_required).content.bodystringcontent.urlstringcontent.iconstringcontent.segmentIdstring (uuid)content.filtersFiltersThe segment filter format, shared by segments, inline send
filters/excludeFilters, schedules and the audience list.filters.matchstringall= every rule must match (default),any= at least one.filters.rulesarray of RuleThe rules.
content.topicIdstringId or slug (
400 unknown_topic).content.excludeSegmentIdstring (uuid)content.excludeFiltersFiltersThe segment filter format, shared by segments, inline send
filters/excludeFilters, schedules and the audience list.excludeFilters.matchstringall= every rule must match (default),any= at least one.excludeFilters.rulesarray of RuleThe rules.
cadencestringDefault daily.
atTimestring"HH:MM" (default "09:00").
dayOfWeekinteger0–6, 0 = Sunday (default 1).
timezonestringAn IANA name (default UTC;
400 invalid_timezone).enabledbooleanDefault true.
Responses
- 201Created.
Response fields
scheduleSchedulerequired
- 400Bad request
invalid_jsoncontent_title_requiredinvalid_timezoneunknown_topic - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 413Too large
payload_too_large
Example
curl -X POST https://relaybell.com/api/v1/schedules \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"name": "Daily digest",
"content": {
"title": "Today's picks",
"url": "https://shop.example/today",
"topicId": "deals"
},
"cadence": "daily",
"atTime": "09:00",
"timezone": "Asia/Kolkata"
}'/schedules/{id}Replace a recurring campaign
Replaces the whole schedule (fields left out take their defaults) and works out its next run again.
Path parameters
idstring (uuid)requiredThe schedule's id. One that isn't this website's — or isn't a UUID — answers
404 not_found.
Request body · application/json
The same fields as Create a recurring campaign.
Responses
- 200The schedule now.
Response fields
scheduleSchedulerequired
- 400Bad request
invalid_jsoncontent_title_requiredinvalid_timezoneunknown_topic - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found - 413Too large
payload_too_large
Example
curl -X PUT https://relaybell.com/api/v1/schedules/ID \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"name": "Daily digest",
"content": {
"title": "Today's picks"
},
"cadence": "weekly",
"dayOfWeek": 1,
"atTime": "10:00",
"timezone": "UTC",
"enabled": true
}'/schedules/{id}Delete a recurring campaign
An unknown id answers { ok: true }.
Path parameters
idstring (uuid)requiredThe schedule id (not a UUID:
404 not_found).
Responses
- 200Done.
Response fields
oktruerequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found
Example
curl -X DELETE https://relaybell.com/api/v1/schedules/ID \
-H "Authorization: Bearer rb_live_xxxxx"Templates
Saved notification copy.
/templatesList templates
Saved notification copy, newest first.
Responses
- 200The templates.
Response fields
templatesarray of Templaterequiredtemplates[].idstring (uuid)requiredtemplates[].namestringrequiredtemplates[].contentobjectrequiredAny object, e.g. a send body.
templates[].created_atstring (date-time)required
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/templates" \
-H "Authorization: Bearer rb_live_xxxxx"/templatesCreate a template
content is any object, e.g. a send body.
Request body · application/json
namestringrequiredRequired (
400 name_required).contentobject
Responses
- 201Created.
Response fields
templateTemplaterequired
- 400Bad request
invalid_jsonname_required - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 413Too large
payload_too_large
Example
curl -X POST https://relaybell.com/api/v1/templates \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"name": "Sale",
"content": {
"title": "Flash sale 🔥",
"body": "30% off ends tonight"
}
}'/templates/{id}Replace a template
Replaces name and content.
Path parameters
idstring (uuid)requiredThe template's id. One that isn't this website's — or isn't a UUID — answers
404 not_found.
Request body · application/json
namestringrequiredRequired (
400 name_required).contentobject
Responses
- 200The template now.
Response fields
templateTemplaterequired
- 400Bad request
invalid_jsonname_required - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found - 413Too large
payload_too_large
Example
curl -X PUT https://relaybell.com/api/v1/templates/ID \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"name": "Sale",
"content": {
"title": "Last day of the sale"
}
}'/templates/{id}Delete a template
An unknown id answers { ok: true }.
Path parameters
idstring (uuid)requiredThe template id (not a UUID:
404 not_found).
Responses
- 200Done.
Response fields
oktruerequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found
Example
curl -X DELETE https://relaybell.com/api/v1/templates/ID \
-H "Authorization: Bearer rb_live_xxxxx"Goals
Conversion goals and the funnel report.
/goalsList conversion goals
Oldest first.
Responses
- 200The goals.
Response fields
goalsarray of Goalrequiredgoals[].idstring (uuid)requiredgoals[].namestringrequiredgoals[].eventNamestringrequiredgoals[].createdAtstring (date-time)required
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/goals" \
-H "Authorization: Bearer rb_live_xxxxx"/goalsCreate a goal
A goal counts one of your events. Posting an existing name changes its event instead.
Request body · application/json
namestringrequiredUp to 80 characters.
eventNamestringrequiredUp to 120 characters.
Responses
- 201Created (or changed).
Response fields
goalGoalrequired
- 400Bad request
invalid_jsonname_and_event_required - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 413Too large
payload_too_large
Example
curl -X POST https://relaybell.com/api/v1/goals \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"name": "Purchases",
"eventName": "purchase"
}'/goals/statsGoal report and funnel
Over the last days days: unique subscribers delivered a push and who clicked, then per goal its conversions, unique converting subscribers and revenue (numeric value properties).
Query parameters
daysinteger1–365 (default 30).
Responses
- 200The report.
Response fields
windowDaysintegerrequiredfunnelobjectrequiredfunnel.deliveredintegerrequiredfunnel.clickedintegerrequiredgoalsarray of objectrequiredgoals[].idstring (uuid)goals[].namestringgoals[].eventNamestringgoals[].conversionsintegergoals[].subscribersintegergoals[].revenuenumber
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/goals/stats" \
-H "Authorization: Bearer rb_live_xxxxx"/goals/{id}Change a goal
Changes its name and event (both required). The name of another goal: 409 name_taken.
Path parameters
idstring (uuid)requiredThe goal's id. One that isn't this website's — or isn't a UUID — answers
404 not_found.
Request body · application/json
namestringrequiredeventNamestringrequired
Responses
- 200The goal now.
Response fields
goalGoalrequired
- 400Bad request
invalid_jsonname_and_event_required - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found - 409Conflict
name_taken - 413Too large
payload_too_large
Example
curl -X PUT https://relaybell.com/api/v1/goals/ID \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"name": "Orders",
"eventName": "purchase"
}'/goals/{id}Delete a goal
An unknown id answers { ok: true }.
Path parameters
idstring (uuid)requiredThe goal id (not a UUID:
404 not_found).
Responses
- 200Done.
Response fields
oktruerequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found
Example
curl -X DELETE https://relaybell.com/api/v1/goals/ID \
-H "Authorization: Bearer rb_live_xxxxx"On-site messages
Banners and slide-ins shown on your pages.
/onsiteList on-site messages
Banners and slide-ins the SDK shows to every visitor (the 5 newest active ones), with impressions and clicks.
Responses
- 200The messages.
Response fields
messagesarray of OnsiteMessagerequiredmessages[].idstring (uuid)requiredmessages[].project_idstring (uuid)messages[].titlestringrequiredmessages[].bodystring or nullmessages[].cta_textstring or nullmessages[].cta_urlstring or nullmessages[].stylestringrequiredmessages[].activebooleanrequiredmessages[].created_atstring (date-time)requiredmessages[].impressionsintegerrequiredmessages[].clicksintegerrequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/onsite" \
-H "Authorization: Bearer rb_live_xxxxx"/onsiteCreate an on-site message
A banner or a slide-in on your own pages, separate from push.
Request body · application/json
titlestringUp to 120 characters. Required on create (
400 title_required); on PUT, left out keeps the old one.bodystringUp to 300 characters.
ctaTextstringButton text, up to 40 characters.
ctaUrlstringButton link, up to 500 characters.
stylestringDefault "slidein".
activebooleanDefault true.
Responses
- 201Created.
Response fields
messageOnsiteMessagerequired
- 400Bad request
invalid_jsontitle_required - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 413Too large
payload_too_large
Example
curl -X POST https://relaybell.com/api/v1/onsite \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"title": "Free shipping this week",
"body": "On every order over $50.",
"ctaText": "Shop now",
"ctaUrl": "https://shop.example",
"style": "banner"
}'/onsite/{id}Replace an on-site message
Replaces every field (a field left out is cleared, active goes back to true unless false) — except title, which is kept when left out.
Path parameters
idstring (uuid)requiredThe message's id. One that isn't this website's — or isn't a UUID — answers
404 not_found.
Request body · application/json
The same fields as Create an on-site message.
Responses
- 200The message now.
Response fields
messageOnsiteMessagerequired
- 400Bad request
invalid_json - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found - 413Too large
payload_too_large
Example
curl -X PUT https://relaybell.com/api/v1/onsite/ID \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"title": "Free shipping this week",
"style": "banner",
"active": false
}'/onsite/{id}Delete an on-site message
An unknown id answers { ok: true }.
Path parameters
idstring (uuid)requiredThe message id (not a UUID:
404 not_found).
Responses
- 200Done.
Response fields
oktruerequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 404Not found
not_found
Example
curl -X DELETE https://relaybell.com/api/v1/onsite/ID \
-H "Authorization: Bearer rb_live_xxxxx"Stats and reports
Totals, daily numbers, growth and list health.
/statsGet totals and recent sends
Totals for the website, plus its 20 most recent sends (newest first). subscribers counts browsers (one person on two devices counts twice).
Responses
- 200The stats.
Response fields
subscribersintegerrequiredsubscriberDevicesmap of integerrequiredBy device: desktop, android, ios, mobile, other.
totalsobjectrequiredtotals.sentintegerrequiredtotals.clicksintegerrequiredtotals.ctrnumberrequiredtotals.conversionsintegerrequiredtotals.revenuenumberrequirednotificationsarray of objectrequirednotifications[].idstring (uuid)requirednotifications[].titlestringrequirednotifications[].bodystring or nullnotifications[].urlstring or nullnotifications[].statusstringrequireddraft,scheduled,queued,held_for_review,sending,sent,failed(reason inerror) orcanceled.notifications[].recipientsintegernotifications[].sentintegernotifications[].failedintegernotifications[].prunedintegernotifications[].receivedintegerPushes the browser confirmed it showed.
notifications[].clicksintegernotifications[].conversionsintegernotifications[].revenuenumbernotifications[].devicesmap of integernotifications[].abobject or nullnotifications[].abStatsobject or null{ a: { delivered, clicks }, b: { delivered, clicks } } for an A/B send.
notifications[].errorstring or nullnotifications[].scheduled_atstring (date-time) or nullnotifications[].created_atstring (date-time)requiredpromptAbobject or nullrequiredOpt-in prompt A/B results, or null.
emailVerifiedboolean or nullrequiredsendingBlockedbooleanrequiredSends are refused until the email is verified.
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/stats" \
-H "Authorization: Bearer rb_live_xxxxx"/stats/timeseriesGet daily numbers
One point per day (days without sends are zeros), the totals, and the same totals for the period of the same length just before. Each notification counts on the day it went out. Without from and to it covers the last 30 days; at most 366 days.
Query parameters
fromstring (date)First day (YYYY-MM-DD).
tostring (date)Last day (YYYY-MM-DD, included; default today).
tzstringIANA time zone (default: the website's, else UTC).
Responses
- 200The series.
Response fields
fromstring (date)requiredtostring (date)requiredtzstringrequiredinterval"day"requiredpointsarray of objectrequiredpoints[].datestring (date)points[].sentintegerpoints[].deliveredintegerpoints[].clickedintegerpoints[].conversionsintegerpoints[].revenuenumbertotalsobjectrequiredtotals.sentintegertotals.deliveredintegertotals.clickedintegertotals.ctrnumbertotals.conversionsintegertotals.revenuenumberpreviousobjectrequiredprevious.fromstringprevious.tostringprevious.sentintegerprevious.deliveredintegerprevious.clickedintegerprevious.ctrnumberprevious.conversionsintegerprevious.revenuenumbercurrencystring or nullrequired
- 400Bad request
invalid_rangeinvalid_tz - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/stats/timeseries" \
-H "Authorization: Bearer rb_live_xxxxx"/stats/growthGet subscriber growth
New and lost subscribers per day (days with neither are left out), oldest first.
Query parameters
daysinteger1–365 (default 30).
Responses
- 200The growth.
Response fields
daysintegerrequiredseriesarray of objectrequiredseries[].datestring (date)series[].subscribedintegerseries[].churnedintegertotalsobjectrequiredtotals.subscribedintegertotals.churnedintegertotals.netinteger
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/stats/growth" \
-H "Authorization: Bearer rb_live_xxxxx"/stats/healthGet list health
active (not unsubscribed), reachable (devices of active subscribers), unsubscribed, new30 / lost30 (last 30 days), dormant (no delivery in 60 days, older than a week) and churnRate (%).
Responses
- 200The numbers.
Response fields
activeintegerrequiredreachableintegerrequiredunsubscribedintegerrequirednew30integerrequiredlost30integerrequireddormantintegerrequiredchurnRatenumberrequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/stats/health" \
-H "Authorization: Bearer rb_live_xxxxx"/reports/testEmail a test report now
Sends the performance report (last 7 days, or 30 with frequency: "monthly") to the account's email now.
Request body · application/json · optional
frequencystring
Responses
- 200Sent.
Response fields
oktruerequiredsentTostringrequiredreportobjectrequiredreport.sentintegerreport.clicksintegerreport.ctrnumberreport.conversionsintegerreport.revenuenumberreport.newSubsintegerreport.topobject or null{ title, clicks } of the best send, or null.
- 400Bad request
invalid_jsonno_account_email - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 413Too large
payload_too_large
Example
curl -X POST https://relaybell.com/api/v1/reports/test \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"frequency": "weekly"
}'Website settings
Sending rules, the opt-in prompt, the welcome push and the default logo.
/meGet the website
The website the key belongs to: its id, name, VAPID public key, logo and number of stored push subscriptions.
Responses
- 200The website.
Response fields
projectIdstring (uuid)requirednamestringvapidPublicKeystringlogostring or nullsubscribersintegerStored push subscriptions.
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/me" \
-H "Authorization: Bearer rb_live_xxxxx"/setup-statusGet setup progress
Whether the SDK has been seen on the live site, and whether there are subscribers, sends, a key and an automation.
Responses
- 200The status.
Response fields
installedbooleanrequiredsdkLastSeenAtstring (date-time) or nullsubscribersintegerrequiredsentintegerrequiredNotifications made (drafts not counted).
hasApiKeybooleanrequiredhasAutomationbooleanrequiredAny automation, journey, schedule or feed.
emailVerifiedboolean or nullsendingBlockedbooleanrequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/setup-status" \
-H "Authorization: Bearer rb_live_xxxxx"/settingsGet website settings
A key without full or settings:write doesn't get shopifySecret back.
Responses
- 200The settings.
Response fields
settingsSettingsrequiredWebsite settings. Other keys are stored as they are.
settings.frequencyCapintegerMost pushes one browser gets in 24 hours; 0 or unset = no cap.
settings.quietHoursobjectsettings.defaultUrlstringThe link used when a send has no url ("" or null removes it).
settings.utmobjectsettings.timezonestringThe website's time zone for reports (IANA;
400 invalid_timezone; "" or null removes it).settings.retentionDaysintegerDelete activity history older than this; 0 = keep forever (
400 invalid_retention_days).settings.privacyUrlstringLinked from the opt-in prompt.
settings.conversionEventsarray of stringWhich events count as conversions.
settings.attributionWindowDaysintegerDays after a delivery a conversion is credited to it (default 7).
settings.currencystringRevenue currency.
settings.reportobjectThe emailed performance report.
settings.shopifySecretstringYour Shopify webhook signing secret. Not returned to a key without
fullorsettings:write.settings.identityVerificationbooleanRead-only here; change it with PUT /identity.
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/settings" \
-H "Authorization: Bearer rb_live_xxxxx"/settingsChange website settings
Merges the keys you send into the stored ones ({ "settings": { … } } or the keys at the top level). identityVerification is ignored here — use PUT /identity.
Request body · application/json
settingsSettingsrequiredWebsite settings. Other keys are stored as they are.
Responses
- 200The settings now.
Response fields
oktruerequiredsettingsSettingsrequiredWebsite settings. Other keys are stored as they are.
- 400Bad request
invalid_jsoninvalid_settingsinvalid_timezoneinvalid_retention_days - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 413Too large
payload_too_large
Example
curl -X PUT https://relaybell.com/api/v1/settings \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"settings": {
"frequencyCap": 3,
"quietHours": {
"enabled": true,
"start": "22:00",
"end": "08:00",
"timezone": "Europe/London"
}
}
}'/promptGet the opt-in prompt
The opt-in prompt the SDK shows.
Responses
- 200The prompt.
Response fields
promptPromptConfig or any or nullrequiredprompt.enabledbooleanprompt.typestringprompt.triggerstringprompt.delaySecondsintegerprompt.scrollPercentintegerprompt.clickSelectorstring or nullprompt.titlestringprompt.messagestringprompt.allowTextstringprompt.denyTextstringprompt.positionstringprompt.accentColorstringA color code like #0B7C93 (
400 invalid_color).prompt.iosInstallHintbooleanprompt.bobjectPrompt A/B test: version B.
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/prompt" \
-H "Authorization: Bearer rb_live_xxxxx"/promptChange the opt-in prompt
Replaces the prompt ({ "prompt": { … } } or the fields at the top level). Only known fields are stored, with safe values.
Request body · application/json
promptPromptConfigrequiredThe opt-in prompt. Only these fields are stored; unknown values fall back to the defaults.
Responses
- 200The prompt now.
Response fields
oktruerequiredpromptPromptConfigrequiredThe opt-in prompt. Only these fields are stored; unknown values fall back to the defaults.
- 400Bad request
invalid_jsoninvalid_promptinvalid_color - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 413Too large
payload_too_large
Example
curl -X PUT https://relaybell.com/api/v1/prompt \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"prompt": {
"type": "slidein",
"trigger": "delay",
"delaySeconds": 5,
"title": "Get deals first",
"allowText": "Allow",
"accentColor": "#0B7C93"
}
}'/welcomeGet the welcome push
The push sent straight away to each new browser.
Responses
- 200The welcome config.
Response fields
welcomeWelcomeConfig or any or nullrequiredwelcome.enabledbooleanwelcome.titlestringwelcome.bodystringwelcome.urlstringwelcome.iconstringwelcome.imagestring
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/welcome" \
-H "Authorization: Bearer rb_live_xxxxx"/welcomeChange the welcome push
{ "welcome": { enabled, title, body, url, icon, image } } (or the fields at the top level). It replaces the whole config.
Request body · application/json
welcomeWelcomeConfigrequiredThe welcome push sent straight away to each new browser. Replaces the whole config.
Responses
- 200The welcome config now.
Response fields
oktruerequiredwelcomeWelcomeConfigrequiredThe welcome push sent straight away to each new browser. Replaces the whole config.
- 400Bad request
invalid_jsoninvalid_welcome - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 413Too large
payload_too_large
Example
curl -X PUT https://relaybell.com/api/v1/welcome \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"welcome": {
"enabled": true,
"title": "Thanks for subscribing!",
"body": "We'll only send what matters.",
"url": "https://shop.example"
}
}'/logoGet the default logo
The default notification icon.
Responses
- 200The logo.
Response fields
logostring or nullrequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/logo" \
-H "Authorization: Bearer rb_live_xxxxx"/logoChange the default logo
The image used as a notification's icon when a send has none (null removes it). Upload one with POST /upload.
Request body · application/json
logostring or nullAn image URL.
Responses
- 200The logo now.
Response fields
oktruerequiredlogostring or nullrequired
- 400Bad request
invalid_json - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 413Too large
payload_too_large
Example
curl -X PUT https://relaybell.com/api/v1/logo \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"logo": "https://relaybell.com/uploads/3f9c….png"
}'API keys
List and revoke keys (creating one needs the dashboard).
/keysList API keys
The website's keys (never the keys themselves) and the scopes a key can have. Needs full — the only scope that can list keys.
Responses
- 200The keys.
Response fields
keysarray of ApiKeyrequiredkeys[].idstring (uuid)requiredkeys[].prefixstringrequiredThe visible start of the key.
keys[].labelstring or nullkeys[].namestring or nullkeys[].scopesarray of stringrequiredkeys[].expires_atstring (date-time) or nullkeys[].created_atstring (date-time)requiredkeys[].last_used_atstring (date-time) or nullkeys[].expiredbooleanrequiredkeys[].currentbooleanrequiredWhether this is the key making the request.
scopesarray of stringrequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope
Example
curl "https://relaybell.com/api/v1/keys" \
-H "Authorization: Bearer rb_live_xxxxx"/keysCreate an API key
Dashboard session only — a key can't make other keys (403 session_required). The answer's apiKey is the only time you see the key: relaybell stores only a hash. The owner gets an email about every new key.
Headers
X-Project-Idstring (uuid)requiredThe website the dashboard session acts on (or
?projectId=/projectIdin the body). Missing:400 project_id_required; not one of yours:404 project_not_found.
Request body · application/json · optional
namestringUp to 60 characters (default "API key").
scopesarray of stringDefault ["full"]; a list with full becomes just ["full"] (
400 invalid_scopes).expiresAtstring (date-time) or nullIn the future, within 3 years, or null for none (
400 invalid_expiry).
Responses
- 201Created.
Response fields
apiKeystringrequiredThe key — shown once.
keyApiKeyrequired
- 400Bad request
invalid_jsoninvalid_scopesinvalid_expiryproject_id_required - 401No valid credentials
not_authenticated - 403Not allowed
account_suspendedsession_required - 404Not found
project_not_found - 413Too large
payload_too_large
Only the signed-in dashboard calls this route: do it under Settings → Developers.
/keys/{id}Revoke an API key
The signed-in owner can revoke any key. An API key can revoke only itself (:id = its own id, or current); anything else answers 403 session_required.
Path parameters
idstringrequiredThe key id, or
currentfor the key making the request.
Responses
- 200Done.
Response fields
oktruerequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
session_requiredaccount_suspended - 404Not found
not_found
Example
curl -X DELETE https://relaybell.com/api/v1/keys/ID \
-H "Authorization: Bearer rb_live_xxxxx"Identity verification
The secret your server signs user ids with, so nobody can claim another person's notifications.
/identityGet the identity secret
The secret key your server signs external ids with (userHash = lowercase hex HMAC-SHA256 of the external id), and whether verified ids are required. Creates the secret on the first call. Never cached.
Responses
- 200The secret.
Response fields
secretstringrequired64 hex characters, used as text.
requiredbooleanrequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/identity" \
-H "Authorization: Bearer rb_live_xxxxx"/identityRequire verified user ids
true turns the requirement on (and creates the secret if needed). false turns it off — from the dashboard only: with an API key it answers 403 identity_off_requires_dashboard, so a leaked key can't switch the protection off.
Request body · application/json
requiredbooleanrequired
Responses
- 200The setting now.
Response fields
requiredbooleanrequired
- 400Bad request
invalid_jsoninvalid_required - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
identity_off_requires_dashboardaccount_suspended - 413Too large
payload_too_large
Example
curl -X PUT https://relaybell.com/api/v1/identity \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"required": true
}'/identity/rotateMake a new identity secret
Dashboard session only (an API key gets 403 session_required). The old secret stops working at once, so update your site straight away.
Headers
X-Project-Idstring (uuid)requiredThe website the dashboard session acts on (or
?projectId=/projectIdin the body). Missing:400 project_id_required; not one of yours:404 project_not_found.
Responses
- 200The new secret.
Response fields
secretstringrequired
- 400Bad request
project_id_required - 401No valid credentials
not_authenticated - 403Not allowed
account_suspendedsession_required - 404Not found
project_not_found
Only the signed-in dashboard calls this route: do it under Settings → Developers.
Uploads and AI
Image uploads and copy suggestions.
/uploadUpload an image
An image for a notification: PNG, JPEG, GIF or WebP up to 2 MB, as the multipart form field file. 60 uploads an hour.
Request body · multipart/form-data
filestringrequiredThe image.
Responses
- 201Uploaded.
Response fields
urlstringrequiredThe public URL of the image.
- 400Bad request
invalid_file - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspendedinsufficient_scope - 413Too large
file_too_large - 429Too many requests
rate_limited
Example
curl -X POST https://relaybell.com/api/v1/upload \
-H "Authorization: Bearer rb_live_xxxxx" \
-F "file=@notification-image.png"/ai/suggestSuggest notification copy
Three rewordings of your draft for you to edit — written by an AI model when ai is true, made from simple templates when it is false. 30 requests an hour.
Request body · application/json
titlestringbodystringgoalstringe.g. "more clicks".
Responses
- 200Suggestions.
Response fields
aibooleanrequiredsuggestionsarray of objectrequiredsuggestions[].titlestringsuggestions[].bodystring
- 400Bad request
invalid_json - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended - 413Too large
payload_too_large - 429Too many requests
rate_limited
Example
curl -X POST https://relaybell.com/api/v1/ai/suggest \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"title": "New arrivals",
"body": "Fresh styles just landed",
"goal": "more clicks"
}'Test devices
Your own browsers, for test sends that never reach subscribers.
/test-devicesList test devices
Your own browsers marked as test devices, so "send test to me" never reaches real subscribers.
Responses
- 200The devices.
Response fields
devicesarray of objectrequireddevices[].idstring (uuid)The subscriber id.
devices[].devicestringe.g. "Chrome on Windows".
devices[].lastSeenAtstring (date-time) or null
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl "https://relaybell.com/api/v1/test-devices" \
-H "Authorization: Bearer rb_live_xxxxx"/test-devicesMark a subscribed browser as a test device
Marks the browser with this push endpoint (it must already be subscribed to this website: 404 not_subscribed). Up to 20 test devices (409 too_many_test_devices).
Request body · application/json
endpointstringrequiredThe browser's push endpoint URL.
Responses
- 200Done.
Response fields
oktruerequired
- 400Bad request
invalid_json - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended - 404Not found
not_subscribed - 409Conflict
too_many_test_devices - 413Too large
payload_too_large
Example
curl -X POST https://relaybell.com/api/v1/test-devices \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"endpoint": "https://fcm.googleapis.com/fcm/send/…"
}'/test-devices/linkGet a test-device link code
A code valid for 24 hours, for several devices: open your site with ?rb_test=CODE in each browser and the SDK marks it.
Responses
- 200The code.
Response fields
codestringrequiredexpiresInintegerrequiredSeconds.
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended
Example
curl -X POST https://relaybell.com/api/v1/test-devices/link \
-H "Authorization: Bearer rb_live_xxxxx"/test-devices/{subscriberId}Remove a test device
Takes the test mark off (the browser stays subscribed).
Path parameters
subscriberIdstring (uuid)requiredThe id from GET /test-devices (not a UUID:
404 not_found).
Responses
- 200Done.
Response fields
oktruerequired
- 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended - 404Not found
not_found
Example
curl -X DELETE https://relaybell.com/api/v1/test-devices/SUBSCRIBER_ID \
-H "Authorization: Bearer rb_live_xxxxx"/test-devices/sendSend a test to your test devices
Sends right now to your test devices only (it never shows up in reports). One result per push endpoint: sent, expired (removed), key_mismatch (the browser must subscribe again) or failed (with reason server_auth or timeout). Test sends work before the email is verified (warning: "email_not_verified") and have their own budget of 120 per clock hour.
Request body · application/json
titlestringrequiredRequired (
400 title_required).bodystringurlstringiconstringimagestringactionsarray of ActionrequireInteractionboolean
Responses
- 200How it went.
Response fields
devicesintegerrequiredsentintegerrequiredfailedintegerrequiredresultsarray of objectrequiredresults[].idstring (uuid)results[].devicestringresults[].statusstringresults[].codeinteger or nullThe push service's HTTP status.
results[].reasonstringwarning"email_not_verified"
- 400Bad request
invalid_jsontitle_required - 401No valid credentials
not_authenticatedinvalid_api_keyapi_key_expired - 403Not allowed
account_suspended - 409Conflict
no_test_devices - 413Too large
payload_too_large - 429Too many requests
send_limit_reached
Example
curl -X POST https://relaybell.com/api/v1/test-devices/send \
-H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
-d '{
"title": "Test from relaybell",
"body": "It works!",
"url": "https://shop.example"
}'Browser SDK
Public routes the browser SDK and service worker call with your Project ID — no API key. 120 requests a minute per visitor IP.
/projects/{projectId}/configGet the SDK config
What the browser SDK needs to subscribe a visitor: the VAPID public key, the opt-in prompt (with your privacy-policy link), topics and the active on-site messages. 120 requests a minute per visitor IP.
Path parameters
projectIdstring (uuid)requiredYour Project ID (public: it is in every page that loads the SDK). One that is not a UUID or not a website answers
404 project_not_found.
Query parameters
fromstring"wp-admin" from the WordPress plugin's settings page (then it doesn't count as the SDK being seen on the live site).
Responses
- 200The config.
Response fields
vapidPublicKeystringrequiredpromptobjectrequiredThe prompt config, plus privacyUrl; enabled is false for a suspended account.
topicsarray of objectrequiredtopics[].namestringtopics[].slugstringonsiteMessagesarray of objectrequiredonsiteMessages[].idstring (uuid)onsiteMessages[].titlestringonsiteMessages[].bodystring or nullonsiteMessages[].ctaTextstring or nullonsiteMessages[].ctaUrlstring or nullonsiteMessages[].stylestring
- 404Not found
project_not_found - 429Too many requests
rate_limited
Example
curl "https://relaybell.com/api/v1/projects/PROJECT_ID/config"/projects/{projectId}/subscriptionsSave a push subscription
Called by the SDK when a visitor allows notifications (201 for a new browser, 200 when it was already known). externalId links the browser to your user; with identity verification it needs a valid userHash, else it is ignored for this request and identity says unverified. oldEndpoint (from the service worker) moves a replaced subscription onto the new endpoint under the same subscriber. A new browser gets your welcome push and fires subscriber.created.
Path parameters
projectIdstring (uuid)requiredYour Project ID (public: it is in every page that loads the SDK). One that is not a UUID or not a website answers
404 project_not_found.
Request body · application/json
subscriptionobjectrequiredsubscription.endpointstringrequiredAn https push-service URL (
400 invalid_subscriptionotherwise).subscription.keysobjectrequiredkeys.p256dhstringkeys.authstringexternalIdstringYour user id.
userHashstringHMAC-SHA256 of externalId with your identity secret, lowercase hex.
urlstringThe page the visitor is on.
languagestringtimezonestringtagsmap of string or number or boolean or nullrb_ keys are ignored.
policyVersionstringpromptVariantstringoldEndpointstring
Responses
- 200Already known: updated.
Response fields
oktruerequiredendpointIdstring (uuid)requiredidentitystringOnly when an externalId was sent:
verified,unverified(ignored for this request) orunchecked(no hash, verification not required).
- 201Subscribed.
Response fields
oktruerequiredendpointIdstring (uuid)requiredidentitystringOnly when an externalId was sent:
verified,unverified(ignored for this request) orunchecked(no hash, verification not required).
- 400Bad request
invalid_jsoninvalid_subscription - 404Not found
project_not_found - 413Too large
payload_too_large - 429Too many requests
rate_limited
Example
curl -X POST https://relaybell.com/api/v1/projects/PROJECT_ID/subscriptions \
-H "Content-Type: application/json" \
-d '{
"subscription": {
"endpoint": "https://fcm.googleapis.com/fcm/send/…",
"keys": {
"p256dh": "BNcRdreALRFX…",
"auth": "tBHItJI5svbp…"
}
},
"externalId": "jane@example.com",
"userHash": "<signature made on your server>",
"url": "https://shop.example/",
"language": "en-GB"
}'/projects/{projectId}/subscriptionsUnsubscribe a browser
Called by RelayBell.unsubscribe(): removes the endpoint, marks the subscriber unsubscribed and fires subscriber.unsubscribed. An unknown endpoint answers { ok: true, alreadyGone: true }.
Path parameters
projectIdstring (uuid)requiredYour Project ID (public: it is in every page that loads the SDK). One that is not a UUID or not a website answers
404 project_not_found.
Query parameters
endpointstringThe push endpoint (or in the body).
Request body · application/json · optional
endpointstring
Responses
- 200Done.
Response fields
oktruerequiredunsubscribedbooleanalreadyGoneboolean
- 400Bad request
invalid_jsonendpoint_required - 404Not found
project_not_found - 413Too large
payload_too_large - 429Too many requests
rate_limited
Example
curl -X DELETE https://relaybell.com/api/v1/projects/PROJECT_ID/subscriptions/projects/{projectId}/eventsTrack an event from the browser
What RelayBell.track() calls. Only the browser's own push endpoint identifies the person here (a bare externalId is stored only when verified, or when no hash is sent and verification isn't required), and it never starts a product-alert broadcast.
Path parameters
projectIdstring (uuid)requiredYour Project ID (public: it is in every page that loads the SDK). One that is not a UUID or not a website answers
404 project_not_found.
Request body · application/json
eventstringOr
name. Missing:400 event_required.namestringpropertiesobjectendpointstringexternalIdstringuserHashstring
Responses
- 202Stored.
Response fields
oktruerequiredeventIdstringrequired
- 400Bad request
invalid_jsonevent_required - 404Not found
project_not_found - 413Too large
payload_too_large - 429Too many requests
rate_limited
Example
curl -X POST https://relaybell.com/api/v1/projects/PROJECT_ID/events \
-H "Content-Type: application/json" \
-d '{
"event": "product.viewed",
"properties": {
"productId": "1234"
},
"endpoint": "https://fcm.googleapis.com/fcm/send/…"
}'/projects/{projectId}/prompt-eventRecord that the prompt was shown
For the opt-in prompt A/B results.
Path parameters
projectIdstring (uuid)requiredYour Project ID (public: it is in every page that loads the SDK). One that is not a UUID or not a website answers
404 project_not_found.
Request body · application/json · optional
variantstring"b", else counted as "a".
Responses
- 202Recorded.
Response fields
oktruerequired
- 400Bad request
invalid_json - 404Not found
project_not_found - 413Too large
payload_too_large - 429Too many requests
rate_limited
Example
curl -X POST https://relaybell.com/api/v1/projects/PROJECT_ID/prompt-event/projects/{projectId}/onsite-eventRecord an on-site message view or click
An impression (default) or a click on one of the website's on-site messages; unknown messages are ignored.
Path parameters
projectIdstring (uuid)requiredYour Project ID (public: it is in every page that loads the SDK). One that is not a UUID or not a website answers
404 project_not_found.
Request body · application/json
messageIdstring (uuid)required400 invalid_messagewhen missing or not a UUID.typestring
Responses
- 202Recorded.
Response fields
oktruerequired
- 400Bad request
invalid_jsoninvalid_message - 404Not found
project_not_found - 413Too large
payload_too_large - 429Too many requests
rate_limited
Example
curl -X POST https://relaybell.com/api/v1/projects/PROJECT_ID/onsite-event \
-H "Content-Type: application/json" \
-d '{
"messageId": "0a2c4e6f-8b0d-4c2e-a4f6-8b0d2e4f6a8c",
"type": "click"
}'/projects/{projectId}/inboxGet the public inbox
The 15 latest broadcasts for showInbox(). Transactional, automation, journey and per-person sends are never listed.
Path parameters
projectIdstring (uuid)requiredYour Project ID (public: it is in every page that loads the SDK). One that is not a UUID or not a website answers
404 project_not_found.
Responses
- 200The inbox.
Response fields
notificationsarray of objectrequirednotifications[].idstring (uuid)notifications[].titlestringnotifications[].bodystring or nullnotifications[].urlstring or nullnotifications[].iconstring or nullnotifications[].imagestring or nullnotifications[].created_atstring (date-time)
- 404Not found
project_not_found - 429Too many requests
rate_limited
Example
curl "https://relaybell.com/api/v1/projects/PROJECT_ID/inbox"/projects/{projectId}/preferencesGet a browser's topics
The topic slugs the browser with this endpoint opted in to (empty for an unknown browser).
Path parameters
projectIdstring (uuid)requiredYour Project ID (public: it is in every page that loads the SDK). One that is not a UUID or not a website answers
404 project_not_found.
Query parameters
endpointstringThe browser's push endpoint.
Responses
- 200The topics.
Response fields
topicsarray of stringrequired
- 404Not found
project_not_found - 429Too many requests
rate_limited
Example
curl "https://relaybell.com/api/v1/projects/PROJECT_ID/preferences"/projects/{projectId}/preferencesSet a browser's topics
Replaces the browser's topics with these slugs (unknown slugs are skipped). A browser that isn't subscribed: 404 subscriber_not_found.
Path parameters
projectIdstring (uuid)requiredYour Project ID (public: it is in every page that loads the SDK). One that is not a UUID or not a website answers
404 project_not_found.
Request body · application/json
endpointstringrequiredtopicsarray of string
Responses
- 200Applied.
Response fields
oktruerequiredappliedintegerrequiredTopics set.
- 400Bad request
invalid_json - 404Not found
project_not_foundsubscriber_not_found - 413Too large
payload_too_large - 429Too many requests
rate_limited
Example
curl -X PUT https://relaybell.com/api/v1/projects/PROJECT_ID/preferences \
-H "Content-Type: application/json" \
-d '{
"endpoint": "https://fcm.googleapis.com/fcm/send/…",
"topics": [
"deals",
"news"
]
}'/notifications/{id}/clickRecord a click
Called by relaybell's service worker when a notification is clicked; you don't call it yourself. It needs the signed token from the push payload: a missing or forged one, or an unknown notification, answers 204 and records nothing. Replays are ignored. Fires notification.clicked.
Path parameters
idstring (uuid)requiredThe notification id.
Query parameters
tokenstringThe signed token (or in the body).
Request body · application/json · optional
tokenstringepstringThe per-recipient endpoint id from the payload.
Responses
- 200Recorded.
Response fields
oktruerequired
- 204Ignored (no valid token, or no such notification).
- 400Bad request
invalid_json - 413Too large
payload_too_large - 429Too many requests
rate_limited
Example
curl -X POST https://relaybell.com/api/v1/notifications/ID/click/notifications/{id}/receiptRecord a delivery receipt
Called by relaybell's service worker when a push reaches the device (the delivered count in reports); you don't call it yourself. Same signed token and replay protection as clicks.
Path parameters
idstring (uuid)requiredThe notification id.
Query parameters
tokenstringThe signed token (or in the body).
Request body · application/json · optional
tokenstringepstring
Responses
- 200Recorded.
Response fields
oktruerequired
- 204Ignored (no valid token).
- 400Bad request
invalid_json - 413Too large
payload_too_large - 429Too many requests
rate_limited
Example
curl -X POST https://relaybell.com/api/v1/notifications/ID/receipt/test-devices/claimMark this browser as a test device
Called by the SDK on a page opened with ?rb_test=CODE (from POST /test-devices/link); the code is the credential.
Request body · application/json
projectIdstring (uuid)requiredcodestringrequiredendpointstringrequired
Responses
- 200Done.
Response fields
oktruerequired
- 400Bad request
invalid_jsoninvalid_requestinvalid_code - 404Not found
not_subscribed - 409Conflict
too_many_test_devices - 413Too large
payload_too_large - 429Too many requests
rate_limited
Example
curl -X POST https://relaybell.com/api/v1/test-devices/claim \
-H "Content-Type: application/json" \
-d '{
"projectId": "8c6f1e2a-4b7d-4f3a-9e21-5d0c7b3a1f64",
"code": "q3Vx9TzA1bCd",
"endpoint": "https://fcm.googleapis.com/fcm/send/…"
}'Integrations
Inbound webhooks from other platforms.
/integrations/shopify/{projectId}Receive a Shopify webhook
Point your Shopify webhooks here. checkouts/create, checkouts/update and carts/update become cart.updated; orders/paid or orders/create become purchase (subscribe to only one); orders/fulfilled becomes order.fulfilled. The person is found by the customer's email (their externalId). Other topics are acknowledged and ignored.
Requests must be signed with your Shopify signing secret (Settings → Privacy): a wrong X-Shopify-Hmac-Sha256 answers 401 invalid_hmac; without a saved secret every request is acknowledged (200) and ignored. Errors on relaybell's side also answer 200 so Shopify doesn't retry.
Path parameters
projectIdstring (uuid)requiredYour Project ID. Unknown:
404 project_not_found.
Headers
X-Shopify-Hmac-Sha256stringrequiredShopify's base64 HMAC-SHA256 of the raw body.
X-Shopify-Topicstringrequirede.g. orders/paid.
X-Shopify-Shop-DomainstringUsed for a cart link when your website address isn't set.
Request body · application/json
The Shopify order, checkout or cart.
Responses
- 200Acknowledged.
Response fields
okbooleanrequiredeventstringThe relaybell event it became.
ignoredstringA topic that isn't mapped.
error"shopify_secret_required"
- 400Bad request
invalid_json - 401No valid credentials
invalid_hmac - 404Not found
project_not_found - 413Too large
payload_too_large
Example
curl -X POST https://relaybell.com/api/v1/integrations/shopify/PROJECT_ID