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

GET /api/v1/message_log

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

Response message-log-index.json Download
{
"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

GET /api/v1/message_log/01k5examplemessagel0g00001

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

Response message-log-show-without-pii.json Download
{
"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, and last_status_at are when the message happened. created_at and updated_at are when it was imported, so filter[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 to delivered, undelivered, or failed, with delivered_at or failed_at at the time the provider reported and updated_at moved so a sync receives it. An imported text that is still sent means “sent, outcome not recorded” rather than “not delivered”.

© 2024-2026 Stoked — Real conversations. Real trust.