Applications API
Read every application submitted through your community site’s join form: who applied, how they answered your application questions, the photos they uploaded, and what you decided. Once an application is approved, it links to the advocate record it became.
Requires the applications permission. An application is mostly personal data, so there is no separate :pii permission: the one permission includes the applicant’s name, email, phone, address, answers and photos.
| Endpoint | Returns |
|---|---|
GET /api/v1/applications |
A paginated list of applications |
GET /api/v1/applications/:id |
One application |
List applications
GET https://integrations.stokedhq.com/api/v1/applications
With no filters, the list contains every application your community has received through Stoked, whatever its status. Applications collected before your community started using Stoked aren’t in Stoked, so the API can’t return them.
Parameters
| Parameter | Description |
|---|---|
filter[status] |
pending, approved or rejected, or several separated by commas |
filter[created_since] |
Only applications submitted at or after this time |
filter[updated_since] |
Only applications updated at or after this time. Use it for incremental syncing. |
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 any other parameter, returns a 400 error. Applications come back least recently updated first.
Request
cURL
curl -G "https://integrations.stokedhq.com/api/v1/applications" \
-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/applications")
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/applications",
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/applications?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/applications");
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": "applications",
"id": "01k5exampleappl1cat10n0001",
"attributes": {
"status": "approved",
"status_changed_at": "2026-08-20T14:00:00Z",
"first_name": "Sarah",
"last_name": "Example",
"email": "sarah@example.com",
"phone_number": "+12025550100",
"address": {
"address_1": "123 Example St",
"address_2": null,
"city": "Springfield",
"state": "IL",
"postal_code": "62701",
"country_code": "USA"
},
"latitude": null,
"longitude": null,
"testimonial": "Our longtail replaced the second car.",
"answers": {
"bike_model": "Longtail",
"interests": [
"Test rides",
"Social posts"
]
},
"avatar": {
"url": "https://storage.stokedhq.com/blobs/proxy/EXAMPLE-SIGNED-ID/sarah.png",
"filename": "sarah.png",
"content_type": "image/png",
"byte_size": 59099,
"checksum": "Vg8Sq0fxeT1qchDorvJuZQ=="
},
"photos": [],
"rejection_reason_id": null,
"rejection_note": null,
"created_at": "2026-08-18T09:00:00Z",
"updated_at": "2026-08-20T14:00:00Z"
},
"relationships": {
"advocate": {
"data": {
"type": "advocates",
"id": "01k5exampleadv0cate0000001"
},
"links": {
"related": "https://integrations.stokedhq.com/api/v1/advocates/01k5exampleadv0cate0000001"
}
}
},
"links": {
"self": "https://integrations.stokedhq.com/api/v1/applications/01k5exampleappl1cat10n0001"
}
}
],
"links": {
"self": "https://integrations.stokedhq.com/api/v1/applications?filter%5Bupdated_since%5D=2026-08-01T00%3A00%3A00Z&page%5Bnumber%5D=1&page%5Bsize%5D=50",
"first": "https://integrations.stokedhq.com/api/v1/applications?filter%5Bupdated_since%5D=2026-08-01T00%3A00%3A00Z&page%5Bnumber%5D=1&page%5Bsize%5D=50",
"prev": null,
"next": null,
"last": "https://integrations.stokedhq.com/api/v1/applications?filter%5Bupdated_since%5D=2026-08-01T00%3A00%3A00Z&page%5Bnumber%5D=1&page%5Bsize%5D=50"
},
"meta": {
"page": 1,
"per_page": 50,
"total_count": 1,
"total_pages": 1,
"application_questions": [
{
"id": "01k5exampleappquest10n0001",
"identifier": "bike_model",
"label": "Which bike do you ride?",
"field_type": "short_text",
"response_options": [],
"required": false,
"position": 1
},
{
"id": "01k5exampleappquest10n0002",
"identifier": "interests",
"label": "How would you like to help?",
"field_type": "multiple_select",
"response_options": [
"Test rides",
"Community events",
"Social posts"
],
"required": false,
"position": 2
}
],
"rejection_reasons": [
{
"id": "01k5examplereject10n000001",
"name": "Outside our area",
"description": "There is no Squad near the applicant yet.",
"system": false
}
]
}
}
Get an application
GET https://integrations.stokedhq.com/api/v1/applications/:id
Returns the same record as the list, with the same meta. An ID that doesn’t exist in your community returns 404.
cURL
curl "https://integrations.stokedhq.com/api/v1/applications/01k5exampleappl1cat10n0001" \
-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/applications/01k5exampleappl1cat10n0001")
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/applications/01k5exampleappl1cat10n0001",
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/applications/01k5exampleappl1cat10n0001");
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/applications/01k5exampleappl1cat10n0001");
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);
Attributes
Records with "type": "applications".
| Attribute | Type | Description |
|---|---|---|
status |
string | pending, approved or rejected |
status_changed_at |
timestamp or null | When the application was approved or rejected. null while it is pending. |
first_name, last_name |
string | The applicant’s name as they entered it |
email |
string | The applicant’s email address |
phone_number |
string or null | The applicant’s phone number in E.164 format, such as +12025550100. null once erased. |
address |
object or null | {address_1, address_2, city, state, postal_code, country_code}, the same shape as an advocate’s address. country_code is ISO 3166-1 alpha-3, such as USA. |
latitude, longitude |
number or null | Map coordinates for the address. Stoked doesn’t look up coordinates for application addresses, so these are usually null. |
testimonial |
string or null | What the applicant wrote about your product |
answers |
object | The applicant’s answers to your application questions. See Answers. |
avatar |
object or null | The profile photo the applicant uploaded. See Photos. |
photos |
array | Any other photos the applicant uploaded. Empty when there are none. |
rejection_reason_id |
string or null | Why the application was rejected: its entry in meta.rejection_reasons. null unless rejected. |
rejection_note |
string or null | The note the admin wrote when rejecting it |
created_at |
timestamp | When the applicant submitted the application |
updated_at |
timestamp | When anything above last changed |
Answers
answers is an object keyed by each question’s identifier, as it was when the applicant answered. Every response carries your community’s current questions in its top-level meta.application_questions: an array of {id, identifier, label, field_type, response_options, required, position}, in the order the form shows them.
Values are formatted like an advocate’s custom_fields: a multiple_select answer is an array of the chosen options, and everything else is a string (a ticked checkbox is "Yes"; an unticked one counts as blank, see below).
- A question the applicant left blank is not in
answers. Stoked doesn’t store blank answers, so it can’t tell a skipped question from one that wasn’t on the form yet. - If you rename a question’s identifier or delete a question, older applications keep the identifier they were answered under. That key may then be missing from
meta.application_questions, or, if a newer question reuses the identifier, match that newer question rather than the one the applicant answered. - Once a question is deleted, its answers are served exactly as stored, since there is no question type left to format them by: a
multiple_selectanswer becomes a JSON-encoded string such as"[\"Test rides\",\"Events\"]", and a checkbox"true"(an unticked checkbox counts as blank, so it is never stored). - Once an applicant is erased at their request, every answer is the string
"[redacted]", includingmultiple_selectanswers. See What updatesupdated_at. - Tag questions aren’t in
answers, though they are listed inmeta.application_questionswithfield_type"tags". Their picks become tag suggestions you accept or dismiss when reviewing the application.
Photos
avatar and each entry in photos are objects:
| Field | Type | Description |
|---|---|---|
url |
string | Where to download the original file. The URL doesn’t expire, and Stoked serves the file on each request, so download it rather than embedding the URL somewhere public. Anyone who has the URL can download the file without an API key. |
filename |
string | The file’s name as uploaded |
content_type |
string | Such as image/jpeg or image/png |
byte_size |
integer | The file’s size in bytes |
checksum |
string | The Base64-encoded MD5 of the file. Compare it with your stored copy to skip downloading a photo you already have. |
Rejection reasons
Every response also carries your community’s rejection reasons in meta.rejection_reasons: an array of {id, name, description, system}, in the order set under Settings > Close Reasons > Applications (Close Reasons). Look up a rejected application’s rejection_reason_id there. system is true only for “Unknown”, the built-in reason that applications move to when their reason is deleted.
Relationships
| Relationship | Type | Description |
|---|---|---|
advocate |
advocates |
The advocate record the application became once approved. null until an approved application is linked to an advocate. An application rejected after it was approved keeps the link. |
Following the link needs the advocates permission: the link is a pointer, not a grant.
What updates updated_at
An application’s updated_at changes when it is approved, rejected or reset to pending, when it is linked to an advocate, when its address changes, when its rejection reason is deleted (it moves to “Unknown”), when a question it answered changes type or is deleted (the answer is formatted by the question’s type), and when it is erased at the applicant’s request. Erasure blanks the personal attributes, removes the photos and replaces each answer with [redacted]. The record itself stays, so your copy receives the erasure on the next sync.
Answers and photos never change after an application is submitted. Renaming a question or a rejection reason doesn’t change the application, since meta carries the current definitions on every response.
Keeping a copy in sync
- On the first run, follow
links.nextthrough everything and upsert each application byid - Remember the newest
updated_atyou received - On the next run, pass it as
filter[updated_since], followlinks.nextto the end, and upsert each application byid - Download any photo whose
checksumyou don’t already have