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 the messaging provider 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": "delivery_error",
"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": "delivery_error",
"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, in Stoked’s words, for example blocked_by_opt_out when the recipient had texted STOP. null for delivered messages. |
|
provider_error_code |
string or null | The messaging provider’s own error code, when it gave one. 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 | ||
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.
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). provider_error_code 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 |
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 texts imported from the provider. 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 so the history is complete. Those records have data_source of backfilled. Texts imported from the messaging provider 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 provider does not know which conversation a text belonged to. Web messages imported from Stoked’s own history keep their conversation_message trigger and their conversation. Addresses and timestamps are real in both cases. Use filter[data_source]=live to leave imported records out.