Stoked API

A read-only JSON API for pulling your community’s Stoked data into your own database, data warehouse, or internal tools.

This section is written for developers. If you just need a spreadsheet, the CSV exports in the admin portal (Exporting Conversations) need no code.

Version 1 is in early access. It covers Conversations and their messages, Advocates, and Prospects.


Getting an API key

API keys are created by a community admin. Each key belongs to one community and can only read that community’s data.

  1. In the admin portal, go to Settings > API Keys
  2. Click New API key
  3. Enter a Name that describes the integration that will use it, for example “ERP sync”
  4. Under Permissions, check only what the integration needs — every permission is off by default
  5. Click Create API key
  6. Copy the key from the Your new API key box and store it somewhere safe, like a password manager or your secrets store

The key is shown once. Stoked stores only a hash of it, so nobody — including Stoked support — can show it to you again. If you lose it, revoke it and create a new one.

Create one key per integration so you can revoke them independently. The API Keys list shows each key’s permissions, who created it, and when it was last used.

  • Edit permissions changes what a key can read without changing the key itself, so a running integration keeps working. When new endpoints ship, you check a box rather than rotating the key.
  • Revoke stops a key working immediately and can’t be undone.

Authentication

Send the key as a Bearer token on every request:

Authorization: Bearer YOUR_API_KEY

Keys start with stk_live_. Treat a key like a password: keep it on your server, never in a browser, mobile app, or public repository.

A missing, mistyped, or revoked key returns 401:

Response error-401.json Download
{
"errors": [
{
"status": "401",
"title": "Unauthorized",
"detail": "Invalid or missing API key."
}
]
}

Permissions

A key can read only what it has been granted. A request to an endpoint the key lacks permission for returns 403, and the error names the missing permission.

Permission Grants
conversations Read conversations and their messages
advocates Read advocate records, without personal data
advocates:pii Adds advocate names, email, phone, display name and location, address, map coordinates, and custom fields
prospects Read prospect records, without personal data
prospects:pii Adds prospect names, email, and phone
analytics Read analytics events, aggregates, and reports

A :pii permission adds attributes to an endpoint; it doesn’t open one. advocates:pii without advocates still returns 403. Without the :pii permission, personal attributes are left out of the response rather than set to null.

Records point to each other with links.related URLs — a conversation links to its advocate and prospect, for example. A link is a pointer, not a grant: following it with a key that lacks that endpoint’s permission returns 403.


Making requests

The base URL is:

https://integrations.stokedhq.com/api/v1
  • Every endpoint is a GET. The API is read-only.
  • Responses follow the JSON:API format with the media type application/vnd.api+json. Each record has a type, an id, its attributes, and relationships that point to other records by type and id.
  • IDs are opaque strings. Store them as text, not numbers.
  • Timestamps are ISO 8601 in UTC, to the whole second: 2026-09-06T17:30:00Z.
  • Attributes with no value are null rather than missing. The one exception is personal data your key isn’t permitted to read, which is left out.
GET /api/v1/conversations

cURL

curl -G "https://integrations.stokedhq.com/api/v1/conversations" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/vnd.api+json" \
--data-urlencode "page[size]=100"

Ruby

require "net/http"
require "json"
uri = URI("https://integrations.stokedhq.com/api/v1/conversations")
uri.query = URI.encode_www_form("page[size]" => "100")
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/conversations",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Accept": "application/vnd.api+json",
},
params={"page[size]": "100"},
)
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/conversations?page[size]=100");
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/conversations");
url.searchParams.set("page[size]", "100");
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);

Your API key

Every sample in these docs uses the placeholder YOUR_API_KEY. Replace it with a key from Settings → API Keys in your admin portal.

A key can read personal data about your advocates and prospects. Keep it on your server: never put it in browser code, a mobile app or a public repository. If one leaks, revoke it and create another.


Pagination

List endpoints return one page at a time.

Parameter Default Description
page[number] 1 The page to return, starting at 1
page[size] 50 Records per page. The maximum is 200; a larger value is treated as 200.

Each list response includes:

  • linksself, first, prev, next, and last URLs. prev is null on the first page and next is null on the last page.
  • metapage, per_page, total_count, and total_pages.

To read everything, request the first page and keep following links.next until it is null:

GET /api/v1/conversations

cURL

# Requires jq. Writes one JSON object per line to conversations.jsonl.
url="https://integrations.stokedhq.com/api/v1/conversations?page[size]=200"
while [ "$url" != "null" ]; do
page=$(curl -sg "$url" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/vnd.api+json")
echo "$page" | jq -c '.data[]' >> conversations.jsonl
url=$(echo "$page" | jq -r '.links.next')
done

Ruby

require "net/http"
require "json"
url = "https://integrations.stokedhq.com/api/v1/conversations?page[size]=200"
conversations = []
while url
uri = URI(url)
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)
page = JSON.parse(response.body)
conversations.concat(page["data"])
url = page.dig("links", "next")
end
puts "Fetched #{conversations.size} conversations"

Python

import requests
url = "https://integrations.stokedhq.com/api/v1/conversations?page[size]=200"
headers = {
"Authorization": "Bearer YOUR_API_KEY",
"Accept": "application/vnd.api+json",
}
conversations = []
while url:
response = requests.get(url, headers=headers)
response.raise_for_status()
page = response.json()
conversations.extend(page["data"])
url = page["links"]["next"]
print(f"Fetched {len(conversations)} conversations")

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");
string? url = "https://integrations.stokedhq.com/api/v1/conversations?page[size]=200";
var conversations = new List<JsonElement>();
while (url is not null)
{
using var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
using var page = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
foreach (var item in page.RootElement.GetProperty("data").EnumerateArray())
{
conversations.Add(item.Clone());
}
url = page.RootElement.GetProperty("links").GetProperty("next").GetString();
}
Console.WriteLine($"Fetched {conversations.Count} conversations");

JavaScript

let url = "https://integrations.stokedhq.com/api/v1/conversations?page[size]=200";
const conversations = [];
while (url) {
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 page = await response.json();
conversations.push(...page.data);
url = page.links.next;
}
console.log(`Fetched ${conversations.length} conversations`);

Syncing incrementally

Lists are returned in a fixed order: least recently updated first (updated_at ascending). Sorting is not configurable. Records that change move to the end of the list, so new activity always arrives last.

To keep your own copy up to date:

  1. On the first run, page through everything and save each record, keyed by its id
  2. Remember the newest updated_at you received
  3. On the next run, pass that value as filter[updated_since] and page through the results
  4. Insert or update each record by id

filter[updated_since] is inclusive, so you will receive the record at the boundary a second time. Updating by id makes the repeat harmless.

If a record changes while you are paging, it moves to the end of the list and the records after it shift up by one, so a record can slip across a page boundary and be missed in that run. Run your sync when activity is low, use page[size]=200 to keep runs short, and schedule an occasional full sync as a safety net.

Each endpoint’s page lists what causes a record’s updated_at to change.


Errors

Errors use standard HTTP status codes and a JSON:API errors array. Each error has a status, a title, and a human-readable detail.

Status Meaning
400 Bad Request An unknown or malformed query parameter (only filter[...] and page[...] are accepted), an invalid or empty filter value
401 Unauthorized The API key is missing, invalid, or revoked
403 Forbidden The key doesn’t have the permission this endpoint requires
404 Not Found No record with that ID exists in your community
429 Too Many Requests The key has exceeded the rate limit
500 Internal Server Error Something went wrong on our side. Retry later, and contact support if it continues.

Unknown parameters are rejected rather than ignored, so a typo in a filter name fails loudly instead of quietly returning unfiltered data.

Response error-400.json Download
{
"errors": [
{
"status": "400",
"title": "Bad Request",
"detail": "Unknown filter[status] value(s): archived"
}
]
}
Response error-403.json Download
{
"errors": [
{
"status": "403",
"title": "Forbidden",
"detail": "This API key does not have the `conversations` permission."
}
]
}
Response error-404.json Download
{
"errors": [
{
"status": "404",
"title": "Not Found",
"detail": "Resource not found."
}
]
}

Rate limits

Each key can make 120 requests per minute. Beyond that, requests return 429 with a Retry-After header giving the number of seconds to wait before trying again.

At the maximum page size, 120 requests is 24,000 records per minute, so a well-behaved sync rarely reaches the limit. If yours does, wait for the Retry-After period and continue from the same URL.


Versioning and stability

The version is part of the URL (/api/v1). While v1 is in early access we may add endpoints, attributes, and filters at any time, so write your integration to ignore fields it doesn’t recognize. We’ll announce breaking changes in What’s New.


In this section


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