Private Events API
Read your community’s private events: the test rides and other one-to-one meetings an advocate schedules with a prospect. Each record has its time, its status, the outcome once one is recorded, and who scheduled or cancelled it. This is the same data as the test ride list under Events in the admin portal.
Requires the private_events permission. The free-text details note and the meeting address (with map coordinates) need private_events:pii as well.
| Endpoint | Returns |
|---|---|
GET /api/v1/private_events |
A paginated list of private events |
GET /api/v1/private_events/:id |
One private event |
List private events
GET https://integrations.stokedhq.com/api/v1/private_events
With no filters, the list contains every private event: upcoming, finished, cancelled, and those whose personal data has been erased. An event leaves the list only if its conversation is deleted, which is also when the conversation leaves the Conversations API.
Parameters
| Parameter | Description |
|---|---|
filter[status] |
One or more of upcoming, awaiting_outcome, happened, no_show, did_not_happen, cancelled, separated by commas: filter[status]=upcoming,awaiting_outcome. See Statuses. |
filter[conversation] |
Only events that belong to this conversation, by conversation id |
filter[advocate] |
Only events whose conversation has this advocate, by advocate id |
filter[prospect] |
Only events whose conversation has this prospect, by prospect id |
filter[starts_since] |
Only events that start at or after this time. Takes an ISO 8601 timestamp or a date. |
filter[created_since] |
Only events created at or after this time |
filter[updated_since] |
Only events updated at or after this time. Use this for incremental syncing, with the caveat in Syncing status changes. |
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 status, or an id that doesn’t belong to your community, returns a 400 error rather than an empty list. Any other parameter returns 400. Events come back least recently updated first.
Request
cURL
curl -G "https://integrations.stokedhq.com/api/v1/private_events" \
-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/private_events")
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/private_events",
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/private_events?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/private_events");
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": "private_events",
"id": "01k5examplepr1vateevent001",
"attributes": {
"status": "happened",
"starts_at": "2026-09-05T10:00:00Z",
"ends_at": "2026-09-05T11:00:00Z",
"completion_status": "happened",
"completion_recorded_at": "2026-09-05T12:00:00Z",
"cancelled_at": null,
"event_type": {
"id": "01k5exampleeventtype000001",
"name": "Test ride"
},
"details": "Meet at the north entrance; bring a helmet.",
"address": {
"address_1": "100 Example Park Rd",
"address_2": null,
"city": "Springfield",
"state": "IL",
"postal_code": "62701",
"country_code": "USA"
},
"latitude": 39.8,
"longitude": -89.64,
"created_at": "2026-09-01T15:22:00Z",
"updated_at": "2026-09-05T12:00:00Z"
},
"relationships": {
"conversation": {
"data": {
"type": "conversations",
"id": "01k5examplec0nversat10n001"
},
"links": {
"related": "https://integrations.stokedhq.com/api/v1/conversations/01k5examplec0nversat10n001"
}
},
"advocate": {
"data": {
"type": "advocates",
"id": "01k5exampleadv0cate0000001"
},
"links": {
"related": "https://integrations.stokedhq.com/api/v1/advocates/01k5exampleadv0cate0000001"
}
},
"prospect": {
"data": {
"type": "prospects",
"id": "01k5examplepr0spect0000001"
},
"links": {
"related": "https://integrations.stokedhq.com/api/v1/prospects/01k5examplepr0spect0000001"
}
},
"created_by": {
"data": {
"type": "advocates",
"id": "01k5exampleadv0cate0000001"
},
"links": {
"related": "https://integrations.stokedhq.com/api/v1/advocates/01k5exampleadv0cate0000001"
}
},
"cancelled_by": {
"data": null
},
"waiver_template": {
"data": null
}
},
"links": {
"self": "https://integrations.stokedhq.com/api/v1/private_events/01k5examplepr1vateevent001"
},
"meta": {
"pii": true
}
}
],
"links": {
"self": "https://integrations.stokedhq.com/api/v1/private_events?filter%5Bupdated_since%5D=2026-09-01T00%3A00%3A00Z&page%5Bnumber%5D=1&page%5Bsize%5D=50",
"first": "https://integrations.stokedhq.com/api/v1/private_events?filter%5Bupdated_since%5D=2026-09-01T00%3A00%3A00Z&page%5Bnumber%5D=1&page%5Bsize%5D=50",
"prev": null,
"next": null,
"last": "https://integrations.stokedhq.com/api/v1/private_events?filter%5Bupdated_since%5D=2026-09-01T00%3A00%3A00Z&page%5Bnumber%5D=1&page%5Bsize%5D=50"
},
"meta": {
"page": 1,
"per_page": 50,
"total_count": 1,
"total_pages": 1
}
}
Get a private event
GET https://integrations.stokedhq.com/api/v1/private_events/:id
Returns the same record as the list. An ID that doesn’t exist in your community returns 404.
Request
cURL
curl "https://integrations.stokedhq.com/api/v1/private_events/01k5examplepr1vateevent001" \
-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/private_events/01k5examplepr1vateevent001")
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/private_events/01k5examplepr1vateevent001",
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/private_events/01k5examplepr1vateevent001");
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/private_events/01k5examplepr1vateevent001");
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": "private_events",
"id": "01k5examplepr1vateevent001",
"attributes": {
"status": "happened",
"starts_at": "2026-09-05T10:00:00Z",
"ends_at": "2026-09-05T11:00:00Z",
"completion_status": "happened",
"completion_recorded_at": "2026-09-05T12:00:00Z",
"cancelled_at": null,
"event_type": {
"id": "01k5exampleeventtype000001",
"name": "Test ride"
},
"details": "Meet at the north entrance; bring a helmet.",
"address": {
"address_1": "100 Example Park Rd",
"address_2": null,
"city": "Springfield",
"state": "IL",
"postal_code": "62701",
"country_code": "USA"
},
"latitude": 39.8,
"longitude": -89.64,
"created_at": "2026-09-01T15:22:00Z",
"updated_at": "2026-09-05T12:00:00Z"
},
"relationships": {
"conversation": {
"data": {
"type": "conversations",
"id": "01k5examplec0nversat10n001"
},
"links": {
"related": "https://integrations.stokedhq.com/api/v1/conversations/01k5examplec0nversat10n001"
}
},
"advocate": {
"data": {
"type": "advocates",
"id": "01k5exampleadv0cate0000001"
},
"links": {
"related": "https://integrations.stokedhq.com/api/v1/advocates/01k5exampleadv0cate0000001"
}
},
"prospect": {
"data": {
"type": "prospects",
"id": "01k5examplepr0spect0000001"
},
"links": {
"related": "https://integrations.stokedhq.com/api/v1/prospects/01k5examplepr0spect0000001"
}
},
"created_by": {
"data": {
"type": "advocates",
"id": "01k5exampleadv0cate0000001"
},
"links": {
"related": "https://integrations.stokedhq.com/api/v1/advocates/01k5exampleadv0cate0000001"
}
},
"cancelled_by": {
"data": null
},
"waiver_template": {
"data": null
}
},
"links": {
"self": "https://integrations.stokedhq.com/api/v1/private_events/01k5examplepr1vateevent001"
},
"meta": {
"pii": true
}
}
}
The same event fetched with a key that has private_events but not private_events:pii has no details, address, latitude or longitude:
{
"data": {
"type": "private_events",
"id": "01k5examplepr1vateevent001",
"attributes": {
"status": "happened",
"starts_at": "2026-09-05T10:00:00Z",
"ends_at": "2026-09-05T11:00:00Z",
"completion_status": "happened",
"completion_recorded_at": "2026-09-05T12:00:00Z",
"cancelled_at": null,
"event_type": {
"id": "01k5exampleeventtype000001",
"name": "Test ride"
},
"created_at": "2026-09-01T15:22:00Z",
"updated_at": "2026-09-05T12:00:00Z"
},
"relationships": {
"conversation": {
"data": {
"type": "conversations",
"id": "01k5examplec0nversat10n001"
},
"links": {
"related": "https://integrations.stokedhq.com/api/v1/conversations/01k5examplec0nversat10n001"
}
},
"advocate": {
"data": {
"type": "advocates",
"id": "01k5exampleadv0cate0000001"
},
"links": {
"related": "https://integrations.stokedhq.com/api/v1/advocates/01k5exampleadv0cate0000001"
}
},
"prospect": {
"data": {
"type": "prospects",
"id": "01k5examplepr0spect0000001"
},
"links": {
"related": "https://integrations.stokedhq.com/api/v1/prospects/01k5examplepr0spect0000001"
}
},
"created_by": {
"data": {
"type": "advocates",
"id": "01k5exampleadv0cate0000001"
},
"links": {
"related": "https://integrations.stokedhq.com/api/v1/advocates/01k5exampleadv0cate0000001"
}
},
"cancelled_by": {
"data": null
},
"waiver_template": {
"data": null
}
},
"links": {
"self": "https://integrations.stokedhq.com/api/v1/private_events/01k5examplepr1vateevent001"
},
"meta": {
"pii": false
}
}
}
Attributes
Records with "type": "private_events".
| Attribute | Type | Personal data | Description |
|---|---|---|---|
status |
string | Where the event stands. See Statuses. | |
starts_at |
timestamp | When the event starts | |
ends_at |
timestamp or null | When it ends. null means no end time was set; the event then counts as over once it starts. |
|
completion_status |
string | The recorded outcome: unknown (none yet), happened, no_show, or did_not_happen |
|
completion_recorded_at |
timestamp or null | When the outcome was recorded | |
cancelled_at |
timestamp or null | When the event was cancelled. null if it wasn’t. |
|
event_type |
object | {"id": "...", "name": "Test Ride"}: the kind of event, as set up in your community |
|
details |
string or null | Yes | The note entered when scheduling. Reads [redacted] after a GDPR erasure. |
address |
object or null | Yes | Where the meeting is: address_1, address_2, city, state, postal_code, country_code. The same shape as the advocate address, and country_code is a three-letter code. null after a GDPR erasure. |
latitude |
number or null | Yes | Map coordinate of the meeting. null when unknown or after erasure. |
longitude |
number or null | Yes | Map coordinate of the meeting. null when unknown or after erasure. |
created_at |
timestamp | When the event was scheduled | |
updated_at |
timestamp | When the record last changed. See Syncing status changes. |
Personal data
Attributes marked Personal data appear only for keys with private_events:pii, and are left out otherwise rather than set to null. The record’s meta.pii says which shape you received. The permission gates only those four attributes: starts_at, the outcome, the status and the people involved are served with private_events alone.
When a person’s data is erased, their events stay in the list with the details blanked out and the meeting address removed, so a sync receives the redaction.
Statuses
status is worked out from the event’s cancellation, its outcome and the clock. It is not stored.
| Status | Meaning |
|---|---|
upcoming |
Not cancelled, no outcome recorded, and not over yet. An event that is under way is still upcoming. |
awaiting_outcome |
Not cancelled and no outcome recorded, but it has ended: ends_at is in the past, or starts_at is when there is no ends_at |
happened |
The outcome was recorded as having happened |
no_show |
The prospect didn’t turn up |
did_not_happen |
The meeting was called off |
cancelled |
The event was cancelled. This wins over everything else, including a recorded outcome. |
The three outcome statuses match completion_status. To get the outcome of a cancelled event, read completion_status, not status.
Syncing status changes
Because status depends on the clock, an event moves from upcoming to awaiting_outcome when its end time passes, with no change to the record. Its updated_at does not move, so filter[updated_since] won’t deliver it again.
Every other change does move updated_at: rescheduling, editing the details or address, cancelling, recording an outcome, renaming the event type or changing its waiver, and a GDPR erasure.
Work out the time-driven transition yourself from ends_at (or starts_at when ends_at is null), or periodically re-fetch filter[status]=awaiting_outcome to pick up events that have since ended. Add filter[starts_since] to skip the older ones you already have.
Relationships
| Relationship | Type | Description |
|---|---|---|
conversation |
conversations |
The conversation the event belongs to |
advocate |
advocates |
The advocate in that conversation |
prospect |
prospects |
The prospect in that conversation |
created_by |
advocates or admins |
Who scheduled the event |
cancelled_by |
advocates or admins |
Who cancelled it. null when the event isn’t cancelled, and also for events cancelled before Stoked recorded who did it. |
waiver_template |
waiver_templates |
The event type’s current waiver template. null if the type needs no waiver. |
A relationship carries only a type, id and link, never the person’s details. Admins have no endpoint, so an admins identifier has no link. Following a link needs the matching permission (conversations, advocates, prospects or waivers): the link is a pointer, not a grant.
waiver_template is the template the event type uses now, so it changes if you change the type’s waiver. Beats on a conversation about a private event point back here; see Beats.
Keeping a copy in sync
- On the first run, follow
links.nextthrough everything and upsert each event byid - Remember the newest
updated_atyou received - On the next run, pass it as
filter[updated_since], followlinks.nextto the end, and upsert each event byid - Handle the clock-driven change: for every stored event that is
upcomingand has passed itsends_at(orstarts_at), treat it asawaiting_outcome, or re-fetchfilter[status]=awaiting_outcome(narrowed withfilter[starts_since]) each run and upsert each event byid