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

GET /api/v1/analytics/reply_speed

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

Response analytics-reply-speed-index.json Download
{
"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

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