App Data API reference
Every storage row is addressed by its URL from the launch's storage claim — there are no paths to construct. Call the URL exactly as it arrives in the claim, and treat it as opaque.
All app data requests authenticate with the api.token from a launch (not your API key):
Authorization: Bearer <api.token>
See authenticating for how the token works and who can access what for the permission rules.
Operations
All four rows — context, location, member, and member_location — support the same three operations on their URL:
| Method | Effect |
|---|---|
GET | Read the row's JSON object. |
PUT | Replace the row's contents wholesale. |
PATCH | Apply an RFC 6902 JSON Patch to the row. |
The examples below use the row URL captured at launch time, e.g. row_url = launch["storage"]["member_location"].
GET: read a row
GET <row URL>
Returns 200 with your JSON object, exactly as last written — no envelope. A never-written row returns {}.
- curl
- Python
- Node
# $ROW_URL is storage.member_location from the launch
curl "$ROW_URL" \
-H "Authorization: Bearer $API_TOKEN"
import requests
resp = requests.get(
launch["storage"]["member_location"],
headers={"Authorization": f"Bearer {api_token}"},
)
resp.raise_for_status()
data = resp.json() # e.g. {"answers": {"q1": "B"}, "attempts": 1}
const resp = await fetch(
launch.storage.member_location,
{ headers: { Authorization: `Bearer ${apiToken}` } }
)
const data = await resp.json() // e.g. {"answers": {"q1": "B"}, "attempts": 1}
PUT: replace a row
PUT <row URL>
Content-Type: application/json
The body is your raw JSON object (no wrapper). It replaces the row's contents wholesale. Returns 204 No Content.
- curl
- Python
- Node
# $ROW_URL is storage.context from the launch
curl -X PUT "$ROW_URL" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"weeks": 14, "modules_open": ["intro", "loops"]}'
requests.put(
launch["storage"]["context"],
headers={"Authorization": f"Bearer {api_token}"},
json={"weeks": 14, "modules_open": ["intro", "loops"]},
).raise_for_status()
await fetch(
launch.storage.context,
{
method: 'PUT',
headers: {
Authorization: `Bearer ${apiToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ weeks: 14, modules_open: ['intro', 'loops'] }),
}
)
PUT overwrites everything in the row. If two parts of your app store different keys in the same row, use PATCH so one write can't clobber the other's keys.
PATCH: apply a JSON Patch
PATCH <row URL>
Content-Type: application/json
The body wraps an RFC 6902 JSON Patch in a patch key. Paths are relative to your object's root (/answers/q2, with no extra prefix). Returns 204 No Content.
{
"patch": [
{ "op": "add", "path": "/answers/q2", "value": "C" },
{ "op": "replace", "path": "/attempts", "value": 2 },
{ "op": "remove", "path": "/draft" }
]
}
- curl
- Python
- Node
# $ROW_URL is storage.member_location from the launch
curl -X PATCH "$ROW_URL" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"patch": [{"op": "add", "path": "/answers/q2", "value": "C"}]}'
requests.patch(
launch["storage"]["member_location"],
headers={"Authorization": f"Bearer {api_token}"},
json={"patch": [{"op": "add", "path": "/answers/q2", "value": "C"}]},
).raise_for_status()
await fetch(
launch.storage.member_location,
{
method: 'PATCH',
headers: {
Authorization: `Bearer ${apiToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
patch: [{ op: 'add', path: '/answers/q2', value: 'C' }],
}),
}
)
The patch engine is tolerant of missing intermediate paths: an add to /answers/q2 works even if answers doesn't exist yet — the intermediate object is created for you. Patches apply atomically under a row lock, so concurrent patches to different keys both land.
Errors
| Status | Cause |
|---|---|
401 | Missing or expired bearer token — take a fresh api.token from a new launch. |
403 | The token's user isn't allowed on this row — e.g. a student writing the shared context or location row, one student's token used on another student's member row, or a user who isn't a member of the row's course. |
404 | The row URL doesn't exist, or the row doesn't belong to your app. |
422 | Malformed body — e.g. a PATCH without the patch wrapper, or an invalid patch operation. Also how a context or location row answers while its level is still unprovisioned. |
Error responses use the standard error envelope.