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

GET /api/v1/private_events

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

Response private-events-index.json Download
{
"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

GET /api/v1/private_events/01k5examplepr1vateevent001

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

Response private-events-show.json Download
{
"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:

Response private-events-show-without-pii.json Download
{
"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

  1. On the first run, follow links.next through everything and upsert each event by id
  2. Remember the newest updated_at you received
  3. On the next run, pass it as filter[updated_since], follow links.next to the end, and upsert each event by id
  4. Handle the clock-driven change: for every stored event that is upcoming and has passed its ends_at (or starts_at), treat it as awaiting_outcome, or re-fetch filter[status]=awaiting_outcome (narrowed with filter[starts_since]) each run and upsert each event by id

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