Reply Speed API
Read the Reply Speed report: one record per advocate, ranked fastest first by a composite of their median first-reply and follow-up times. These are the same numbers as Operations > Reply Speed in the admin portal and its CSV export, computed the same way — see Reply Speed Report for how the report thinks.
Requires the analytics permission.
| Endpoint | Returns |
|---|---|
GET /api/v1/analytics/reply_speed |
Every ranked advocate for one period and weighting |
Get the ranking
GET https://integrations.stokedhq.com/api/v1/analytics/reply_speed
Nothing here is stored: the ranking is computed from the conversation history each time you ask, for the period you ask about. So there is no filter[updated_since], no page[after] cursor, and no updated_at on a record. The whole ranked list comes back in one response. A nightly sync should simply re-pull each period it charts and replace what it has.
Parameters
| Parameter | Description |
|---|---|
filter[period] |
A named window: last_7_days (the default), last_14_days, last_30_days, last_90_days, last_365_days, this_year, last_year, or all_time. See Periods. |
filter[since] |
The start of a custom window instead of a named period, as an ISO 8601 timestamp: filter[since]=2026-06-01T00:00:00Z |
filter[until] |
The end of a custom window. Defaults to now. Needs filter[since]. |
filter[weighting] |
How the two medians combine into the composite: equal, first_3x, first_5x, first_10x, or first_only. Defaults to the weighting last chosen on the portal’s Reply Speed page. See Weightings. |
filter[advocate] |
One advocate, by advocate id. Returns just that advocate’s record, with their rank among everyone. Returns an empty list when the advocate does not rank in the window. |
There are no page[...] parameters: every ranked advocate is in the one response, and meta.total_count says how many.
filter[period] and filter[since] are two ways of saying the same thing, so sending both returns 400. An unknown period or weighting, a timestamp that does not parse, filter[until] without filter[since], or an advocate id that doesn’t belong to your community also returns 400.
Request
cURL
curl -G "https://integrations.stokedhq.com/api/v1/analytics/reply_speed" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/vnd.api+json" \
--data-urlencode "filter[period]=last_30_days"
Ruby
require "net/http"
require "json"
uri = URI("https://integrations.stokedhq.com/api/v1/analytics/reply_speed")
uri.query = URI.encode_www_form("filter[period]" => "last_30_days")
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/analytics/reply_speed",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Accept": "application/vnd.api+json",
},
params={"filter[period]": "last_30_days"},
)
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/analytics/reply_speed?filter[period]=last_30_days");
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/analytics/reply_speed");
url.searchParams.set("filter[period]", "last_30_days");
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": "analytics-reply-speeds",
"id": "01k5exampleadv0cate0000001:last_30_days:equal",
"attributes": {
"rank": 1,
"first_reply_count": 1,
"first_reply_median_seconds": 300,
"follow_up_count": 0,
"follow_up_median_seconds": null,
"composite_seconds": 300
},
"relationships": {
"advocate": {
"data": {
"type": "advocates",
"id": "01k5exampleadv0cate0000001"
},
"links": {
"related": "https://integrations.stokedhq.com/api/v1/advocates/01k5exampleadv0cate0000001"
}
}
}
},
{
"type": "analytics-reply-speeds",
"id": "01k5exampleadv0cate0000002:last_30_days:equal",
"attributes": {
"rank": 2,
"first_reply_count": 1,
"first_reply_median_seconds": 2520,
"follow_up_count": 0,
"follow_up_median_seconds": null,
"composite_seconds": 2520
},
"relationships": {
"advocate": {
"data": {
"type": "advocates",
"id": "01k5exampleadv0cate0000002"
},
"links": {
"related": "https://integrations.stokedhq.com/api/v1/advocates/01k5exampleadv0cate0000002"
}
}
}
}
],
"meta": {
"period": "last_30_days",
"window_start": "2026-08-09T04:00:00Z",
"window_end": "2026-09-08T14:00:00Z",
"weighting": "equal",
"active_hours_start_hour": 8,
"active_hours_end_hour": 22,
"total_count": 2
}
}
Attributes
Records with "type": "analytics-reply-speeds". A record is a row in a computed report, not an entity, so its id is a deterministic composite of the advocate, the window and the weighting that produced it: the same request gives the same id, and a different period gives a different one. Store rows by (advocate, period) on your side rather than by this id.
| Attribute | Type | Description |
|---|---|---|
rank |
integer | Position in the full ranking, 1 being the fastest. The same rank the portal shows, whether or not you filtered to one advocate. |
first_reply_count |
integer | Conversations in which this advocate sent the first reply during the window. Always at least 1; see Who is listed. |
first_reply_median_seconds |
integer | Median time from a prospect’s message to the advocate’s first reply in the conversation, in seconds |
follow_up_count |
integer | Follow-up replies the advocate sent during the window, meaning any reply after the first one in a conversation |
follow_up_median_seconds |
integer or null | Median time from a prospect’s message to the follow-up reply, in seconds. null when there were no follow-ups. |
composite_seconds |
integer | The score the ranking sorts by: the weighted average of the two medians, in seconds. When there is no follow-up median, it equals the first-reply median. |
Seconds are the report’s active-hours seconds, not wall-clock seconds. The clock only runs during your community’s active hours (the Active Hours card under Settings > General > Community; see Active Hours), read in each advocate’s own time zone, so an advocate who replies first thing in the morning is not charged for the night in between. When both the prospect’s message and the reply fall inside the same off-hours stretch, wall-clock seconds are used instead, so a 2 a.m. reply to a 1:55 a.m. message counts as five minutes, not zero. The clock for a message the prospect sent before verifying starts when the conversation activated, not when the message was written. The portal shows these values rounded to minutes, hours or days; the API gives the seconds.
Periods
| Value | Window |
|---|---|
last_7_days, last_14_days, last_30_days, last_90_days, last_365_days |
From the start of the day that many days ago, up to now |
this_year |
From 1 January of the current year up to now |
last_year |
The whole of the previous calendar year |
all_time |
Everything, from your community’s first conversation up to now |
A rolling window ends “now”, so the same request gives slightly different numbers on different days. all_time is new with the API and also appears in the portal’s Period dropdown. The window actually used is in meta.window_start and meta.window_end.
A reply counts in a window when the reply was sent inside it; the prospect’s message it answers may be earlier.
Weightings
| Value | Portal label | Composite |
|---|---|---|
equal |
Equal weight | Both medians count the same |
first_3x |
First reply ×3 | First-reply median weighted 3× over the follow-up median |
first_5x |
First reply ×5 | 5× |
first_10x |
First reply ×10 | 10×; follow-ups are close to a tiebreaker |
first_only |
First reply only | Only the first-reply median counts. The follow-up median is still reported. |
With no filter[weighting], the API uses the weighting your community’s admins last picked on the portal’s Reply Speed page, which the portal saves for the community. The weighting in effect is always reported in meta.weighting, so a sync can tell whether someone changed the portal setting under it. Pass filter[weighting] explicitly when you want a stable definition.
Who is listed
Exactly the advocates the portal would rank: those who are not inactive or deleted and who sent at least one first reply during the window. An advocate whose only replies in the window were follow-ups does not appear. meta.total_count is the number listed.
Relationships
| Relationship | Type | Description |
|---|---|---|
advocate |
advocates |
The advocate the row is about. Requires the advocates permission to follow. |
A record carries no name or contact details, only the advocate’s type, id and link. The portal’s CSV export includes the advocate’s name and status; read those from the Advocates API.
Meta
Every response states the report it computed:
| Key | Description |
|---|---|
period |
The filter[period] value used, or null for a custom window |
window_start, window_end |
The window actually used, as timestamps |
weighting |
The weighting used, whether from filter[weighting] or the community’s saved choice |
active_hours_start_hour, active_hours_end_hour |
The active-hours window the clock ran in, as hours of the day (0–23), applied in each advocate’s own time zone |
total_count |
How many advocates ranked |