Message Log API
Read the delivery log: one record for every message Stoked sent or received on your community’s behalf, with its channel, what triggered it, and whether it was delivered. This is the same log as Operations > Message Log in the admin portal. It covers notifications, reminders and verification codes as well as the texts relayed between advocates and prospects.
Requires the message_log permission. The phone number or email address on each side of a message needs message_log:pii as well. The text of the message is never served: conversation messages are in the Conversations API, and everything else is a notification.
| Endpoint | Returns |
|---|---|
GET /api/v1/message_log |
A paginated list of log records |
GET /api/v1/message_log/:id |
One log record |
List the message log
GET https://integrations.stokedhq.com/api/v1/message_log
With no filters, the list contains every record, including history imported from before Stoked kept its own log. See Backfilled records.
Parameters
| Parameter | Description |
|---|---|
filter[status] |
One or more of queued, sent, delivered, undelivered, failed, rejected, received, separated by commas: filter[status]=failed,undelivered |
filter[channel] |
One or more of sms, whatsapp, web, email |
filter[direction] |
outbound (Stoked sent it) or inbound (Stoked received it) |
filter[trigger] |
One or more trigger values: filter[trigger]=phone_verification_code |
filter[data_source] |
live or backfilled |
filter[conversation] |
Only records that belong to this conversation, by conversation id |
filter[advocate] |
Only records this advocate received or sent, by advocate id |
filter[prospect] |
Only records this prospect received or sent, by prospect id |
filter[created_since] |
Only records created at or after this time |
filter[updated_since] |
Only records updated at or after this time. Use this for incremental syncing. |
page[size] |
Records per page. Defaults to 50, maximum 200. |
page[after] |
The cursor from a previous response’s links.next. See Pagination. |
page[number] |
A page to jump to. Defaults to 1. |
An unknown value for a list filter, or an id that doesn’t belong to your community, returns a 400 error rather than an empty list. Any other parameter returns 400. Records come back least recently updated first.
Request
cURL
curl -G "https://integrations.stokedhq.com/api/v1/message_log" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/vnd.api+json" \
--data-urlencode "filter[updated_since]=2026-08-01T00:00:00Z"
Ruby
require "net/http"
require "json"
uri = URI("https://integrations.stokedhq.com/api/v1/message_log")
uri.query = URI.encode_www_form("filter[updated_since]" => "2026-08-01T00:00:00Z")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer YOUR_API_KEY"
request["Accept"] = "application/vnd.api+json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
puts JSON.parse(response.body)["data"]
Python
import requests
response = requests.get(
"https://integrations.stokedhq.com/api/v1/message_log",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Accept": "application/vnd.api+json",
},
params={"filter[updated_since]": "2026-08-01T00:00:00Z"},
)
response.raise_for_status()
print(response.json()["data"])
C#
using System.Net.Http.Headers;
using System.Text.Json;
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "YOUR_API_KEY");
client.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.api+json");
using var response = await client.GetAsync("https://integrations.stokedhq.com/api/v1/message_log?filter[updated_since]=2026-08-01T00:00:00Z");
response.EnsureSuccessStatusCode();
using var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
Console.WriteLine(document.RootElement.GetProperty("data"));
JavaScript
const url = new URL("https://integrations.stokedhq.com/api/v1/message_log");
url.searchParams.set("filter[updated_since]", "2026-08-01T00:00:00Z");
const response = await fetch(url, {
headers: {
Authorization: "Bearer YOUR_API_KEY",
Accept: "application/vnd.api+json",
},
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
const { data } = await response.json();
console.log(data);
Response
{
"data": [
{
"type": "message_log_entries",
"id": "01k5examplemessagel0g00001",
"attributes": {
"channel": "sms",
"direction": "outbound",
"trigger": "phone_verification_code",
"status": "failed",
"status_reason": "landline",
"provider_error_code": "30006",
"data_source": "live",
"media_count": 0,
"to_address": "+12025550101",
"from_address": "+12025550199",
"queued_at": "2026-08-31T09:00:00Z",
"sent_at": null,
"delivered_at": null,
"failed_at": "2026-08-31T09:00:03Z",
"received_at": null,
"last_status_at": "2026-08-31T09:00:03Z",
"created_at": "2026-08-31T09:00:00Z",
"updated_at": "2026-08-31T09:00:00Z"
},
"relationships": {
"recipient": {
"data": {
"type": "prospects",
"id": "01k5examplepr0spect0000001"
},
"links": {
"related": "https://integrations.stokedhq.com/api/v1/prospects/01k5examplepr0spect0000001"
}
},
"sender": {
"data": null
},
"conversation": {
"data": null
}
},
"links": {
"self": "https://integrations.stokedhq.com/api/v1/message_log/01k5examplemessagel0g00001"
},
"meta": {
"pii": true
}
},
{
"type": "message_log_entries",
"id": "01k5examplemessagel0g00002",
"attributes": {
"channel": "sms",
"direction": "outbound",
"trigger": "message_sent_notification_to_prospect",
"status": "delivered",
"status_reason": null,
"provider_error_code": null,
"data_source": "live",
"media_count": 0,
"to_address": "+12025550101",
"from_address": "+12025550199",
"queued_at": "2026-09-01T15:10:00Z",
"sent_at": "2026-09-01T15:10:01Z",
"delivered_at": "2026-09-01T15:10:04Z",
"failed_at": null,
"received_at": null,
"last_status_at": "2026-09-01T15:10:04Z",
"created_at": "2026-09-01T15:10:00Z",
"updated_at": "2026-09-01T15:10:00Z"
},
"relationships": {
"recipient": {
"data": {
"type": "prospects",
"id": "01k5examplepr0spect0000001"
},
"links": {
"related": "https://integrations.stokedhq.com/api/v1/prospects/01k5examplepr0spect0000001"
}
},
"sender": {
"data": null
},
"conversation": {
"data": {
"type": "conversations",
"id": "01k5examplec0nversat10n001"
},
"links": {
"related": "https://integrations.stokedhq.com/api/v1/conversations/01k5examplec0nversat10n001"
}
}
},
"links": {
"self": "https://integrations.stokedhq.com/api/v1/message_log/01k5examplemessagel0g00002"
},
"meta": {
"pii": true
}
},
{
"type": "message_log_entries",
"id": "01k5examplemessagel0g00003",
"attributes": {
"channel": "sms",
"direction": "inbound",
"trigger": "inbound_relay",
"status": "received",
"status_reason": null,
"provider_error_code": null,
"data_source": "live",
"media_count": 0,
"to_address": "+12025550199",
"from_address": "+12025550101",
"queued_at": null,
"sent_at": null,
"delivered_at": null,
"failed_at": null,
"received_at": "2026-09-01T15:20:00Z",
"last_status_at": "2026-09-01T15:20:00Z",
"created_at": "2026-09-01T15:20:00Z",
"updated_at": "2026-09-01T15:20:00Z"
},
"relationships": {
"recipient": {
"data": {
"type": "prospects",
"id": "01k5examplepr0spect0000001"
},
"links": {
"related": "https://integrations.stokedhq.com/api/v1/prospects/01k5examplepr0spect0000001"
}
},
"sender": {
"data": null
},
"conversation": {
"data": {
"type": "conversations",
"id": "01k5examplec0nversat10n001"
},
"links": {
"related": "https://integrations.stokedhq.com/api/v1/conversations/01k5examplec0nversat10n001"
}
}
},
"links": {
"self": "https://integrations.stokedhq.com/api/v1/message_log/01k5examplemessagel0g00003"
},
"meta": {
"pii": true
}
}
],
"links": {
"self": "https://integrations.stokedhq.com/api/v1/message_log?filter%5Bupdated_since%5D=2026-08-01T00%3A00%3A00Z&page%5Bnumber%5D=1&page%5Bsize%5D=50",
"first": "https://integrations.stokedhq.com/api/v1/message_log?filter%5Bupdated_since%5D=2026-08-01T00%3A00%3A00Z&page%5Bnumber%5D=1&page%5Bsize%5D=50",
"prev": null,
"next": null,
"last": "https://integrations.stokedhq.com/api/v1/message_log?filter%5Bupdated_since%5D=2026-08-01T00%3A00%3A00Z&page%5Bnumber%5D=1&page%5Bsize%5D=50"
},
"meta": {
"page": 1,
"per_page": 50,
"total_count": 3,
"total_pages": 1
}
}
Get a log record
GET https://integrations.stokedhq.com/api/v1/message_log/:id
Returns the same record as the list. An ID that doesn’t exist in your community returns 404. This example was fetched with a key that has message_log but not message_log:pii, so it carries no addresses.
Request
cURL
curl "https://integrations.stokedhq.com/api/v1/message_log/01k5examplemessagel0g00001" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/vnd.api+json"
Ruby
require "net/http"
require "json"
uri = URI("https://integrations.stokedhq.com/api/v1/message_log/01k5examplemessagel0g00001")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer YOUR_API_KEY"
request["Accept"] = "application/vnd.api+json"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
puts JSON.parse(response.body)["data"]
Python
import requests
response = requests.get(
"https://integrations.stokedhq.com/api/v1/message_log/01k5examplemessagel0g00001",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Accept": "application/vnd.api+json",
},
)
response.raise_for_status()
print(response.json()["data"])
C#
using System.Net.Http.Headers;
using System.Text.Json;
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "YOUR_API_KEY");
client.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.api+json");
using var response = await client.GetAsync("https://integrations.stokedhq.com/api/v1/message_log/01k5examplemessagel0g00001");
response.EnsureSuccessStatusCode();
using var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
Console.WriteLine(document.RootElement.GetProperty("data"));
JavaScript
const url = new URL("https://integrations.stokedhq.com/api/v1/message_log/01k5examplemessagel0g00001");
const response = await fetch(url, {
headers: {
Authorization: "Bearer YOUR_API_KEY",
Accept: "application/vnd.api+json",
},
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
const { data } = await response.json();
console.log(data);
Response
{
"data": {
"type": "message_log_entries",
"id": "01k5examplemessagel0g00001",
"attributes": {
"channel": "sms",
"direction": "outbound",
"trigger": "phone_verification_code",
"status": "failed",
"status_reason": "landline",
"provider_error_code": "30006",
"data_source": "live",
"media_count": 0,
"queued_at": "2026-08-31T09:00:00Z",
"sent_at": null,
"delivered_at": null,
"failed_at": "2026-08-31T09:00:03Z",
"received_at": null,
"last_status_at": "2026-08-31T09:00:03Z",
"created_at": "2026-08-31T09:00:00Z",
"updated_at": "2026-08-31T09:00:00Z"
},
"relationships": {
"recipient": {
"data": {
"type": "prospects",
"id": "01k5examplepr0spect0000001"
},
"links": {
"related": "https://integrations.stokedhq.com/api/v1/prospects/01k5examplepr0spect0000001"
}
},
"sender": {
"data": null
},
"conversation": {
"data": null
}
},
"links": {
"self": "https://integrations.stokedhq.com/api/v1/message_log/01k5examplemessagel0g00001"
},
"meta": {
"pii": false
}
}
}
Attributes
Records with "type": "message_log_entries".
| Attribute | Type | Personal data | Description |
|---|---|---|---|
channel |
string | sms, whatsapp, web (a message typed on a Stoked site), or email |
|
direction |
string | outbound or inbound |
|
trigger |
string | What caused the message. See Triggers. | |
status |
string | Where delivery stands. See Statuses. | |
status_reason |
string or null | Why a message was not delivered, as one of a fixed set of words. See Status reasons. null for delivered messages. |
|
provider_error_code |
string or null | The messaging provider’s own error code, when it gave one: the detail behind status_reason. Twilio’s codes are documented at twilio.com. |
|
data_source |
string | live, or backfilled for imported history. See Backfilled records. |
|
media_count |
integer | Number of images or other attachments | |
to_address |
string | Yes | The phone number (E.164) or email address the message went to |
from_address |
string | Yes | The phone number or email address it came from |
queued_at |
timestamp or null | When Stoked handed the message to the provider | |
sent_at |
timestamp or null | When the provider sent it | |
delivered_at |
timestamp or null | When the recipient’s carrier confirmed delivery | |
failed_at |
timestamp or null | When delivery failed or the message was rejected | |
received_at |
timestamp or null | When an inbound message arrived | |
last_status_at |
timestamp | When the status last changed. The most useful single timestamp. | |
created_at |
timestamp | When Stoked wrote the record, which for a backfilled record is the import, not the message. Use queued_at, received_at, or last_status_at for when the message happened. |
|
updated_at |
timestamp | Moves whenever the status changes, so a message that is sent in one sync and delivered in the next is picked up by filter[updated_since]. |
Attributes marked Personal data appear only for keys with message_log:pii, and are left out otherwise. The record’s meta.pii says which shape you received. A web message has no phone numbers: its two addresses are the participants’ names, as they were entered when the message was sent. Someone who typed an email address into a name field appears as that email address.
Statuses
| Status | Meaning |
|---|---|
queued |
Handed to the provider, not yet sent |
sent |
Sent by the provider, delivery not yet confirmed. Some carriers never confirm, so a message can stay sent forever and still have arrived. |
delivered |
Delivery confirmed by the recipient’s carrier |
undelivered |
The carrier could not deliver it (wrong number, landline, carrier filtering). status_reason says why. |
failed |
The provider could not send it |
rejected |
Stoked did not send it, for a reason in status_reason, for example the recipient had opted out |
received |
An inbound message Stoked received |
Status reasons
status_reason is always one of these, so you can group failures without decoding provider codes. The provider’s raw code, when there is one, stays in provider_error_code.
| Reason | Meaning |
|---|---|
blocked_by_opt_out |
Stoked did not send it: the recipient had opted out of texts |
blocked_by_allow_list |
Stoked did not send it: a test community only texts the numbers on its allow list |
landline |
The number is a landline |
invalid_number |
The number is not a valid mobile number |
opted_out |
The provider refused it: the recipient had replied STOP to your number |
unreachable |
The phone could not be reached: switched off, out of service, or unknown to the carrier |
carrier_filtered |
The carrier blocked the message |
content_too_large |
The message was too large for the carrier |
no_relay_target |
An inbound text from a number Stoked could not match to a conversation |
delivery_error |
Any other failure. provider_error_code has the detail. |
New reasons may be added, so treat a word you don’t recognize like delivery_error.
Triggers
trigger values are lower-case with underscores. The ones you will see most:
| Trigger | What it is |
|---|---|
conversation_message |
A message typed on a Stoked site (channel is web) |
inbound_relay |
A text from an advocate or prospect that Stoked relayed to the other party |
inbound_unrouted |
A text from a number Stoked could not match to a conversation |
message_sent_notification_to_prospect, message_sent_notification_to_advocate |
The text that carries a new message to the other party |
new_conversation_notification_to_prospect, new_conversation_notification_to_advocate |
A conversation started |
conversation_reminder_to_prospect, conversation_reminder_to_advocate |
A nudge about an unanswered conversation |
verification_reminder_to_prospect |
A nudge to finish verifying contact details |
phone_verification_code, email_verification_code |
A verification code |
waiver_signed_notification_to_advocate |
A prospect signed a waiver |
advocate_welcome_message |
Sent when an advocate is activated |
advocate_wallet_credited_notification, advocate_wallet_debited_notification |
Reward points changed |
backfilled_outbound, backfilled_inbound |
Imported history, where the trigger was not recorded |
New triggers are added as Stoked gains features; treat an unfamiliar value as one you have not seen yet, not as an error.
Relationships
| Relationship | Type | Description |
|---|---|---|
recipient |
advocates or prospects |
The person the record is filed under. For an outbound message, who it was sent to. For an inbound text, the person who sent it. null for texts from unknown numbers. Requires the matching permission to follow. |
sender |
advocates, prospects, or admins |
Who wrote a web message. null for texts and automated messages. Admins carry no link, as there is no admins endpoint. |
conversation |
conversations |
The conversation the message belongs to. null for messages about something else (a verification code, a reward, an introduction) and for imported texts. Requires the conversations permission to follow. |
A record never carries a person’s details, only their type, id, and link.
Backfilled records
When Stoked began keeping this log, earlier messages were imported from its own history so the record is complete. Those records have data_source of backfilled. Imported texts carry a trigger of backfilled_outbound or backfilled_inbound, a recipient where the phone number matched an advocate or prospect, and no conversation, because the history did not keep which conversation a text belonged to. Imported web messages keep their conversation_message trigger and their conversation. Addresses are real in both cases. Use filter[data_source]=live to leave imported records out.
Inbound texts received between June 12 and September 30, 2026 were not logged as they arrived and were added afterwards. They are backfilled records too, with a trigger of backfilled_inbound and no conversation.
Two things differ from a live record:
- Timestamps.
queued_at,received_at, andlast_status_atare when the message happened.created_atandupdated_atare when it was imported, sofilter[created_since]finds a backfilled record by its import date, not its message date. - Status. The history recorded that an outbound text was handed to the provider, not whether it was delivered, so imported outbound texts started out as
sent. Where the provider still held the outcome (roughly the last 400 days), the record has since been corrected todelivered,undelivered, orfailed, withdelivered_atorfailed_atat the time the provider reported andupdated_atmoved so a sync receives it. An imported text that is stillsentmeans “sent, outcome not recorded” rather than “not delivered”.