Public API
Status: Implemented (subscribed tier)
Configure in: Settings → Developer → API Keys
Base URL (production): https://arcos.corgicy.com/api/public/v1
Base URL (local): http://localhost:3001/api/public/v1
OpenAPI: https://arcos.corgicy.com/api/public/v1/openapi.yaml
Swagger UI: https://arcos.corgicy.com/api/public/v1/docs
Related: Outgoing webhooks · MCP · API reference
The Public API is a write-only fitness and lifestyle log for scripts, devices, and automations. It uses API keys (arc_…). It is not the session API the web app uses, and it is not MCP.
It does not read your data, search Brain, manage forms, or push events. For those, use MCP or webhooks.
Choose this surface when
| You want… | Use |
|---|---|
| A Garmin/script/cron to log water, steps, workouts, sleep, mood, cardio, meditation, body weight | Public API (/api/public/v1) |
| Create / publish / submit Brain forms over HTTP | Brain session API on this page (/api/v1/brain/forms) |
| An assistant to read and write across ARC | MCP |
| ARC to POST to your server when something happens (including Brain forms) | Webhooks |
Quick start
- Subscribe (API keys require an active plan).
- Settings → Developer → create an API key → copy
arc_…once. - Call an endpoint:
curl -X POST https://arcos.corgicy.com/api/public/v1/water \
-H "Authorization: Bearer arc_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"volume": 500}'
- Confirm the entry on Today in the app.
Interactive docs: Swagger UI.
Authentication
Every Public API request:
Authorization: Bearer arc_<key>
Content-Type: application/json
| Rule | Detail |
|---|---|
| Prefix | Keys start with arc_ then 32 random bytes, base64url |
| Storage | ARC stores a hash, not the plaintext. You cannot retrieve the full key later |
| Scope | The key is tied to your user. It cannot see other accounts |
| Revoke / rotate | Settings UI or /api/v1/api-keys (session auth, not the public key itself) |
| Expiry | Optional expiresAt on the key |
Missing/invalid/revoked/expired key → 401 UNAUTHORIZED.
API keys cannot call /api/v1/webhooks, /api/v1/brain/*, or MCP. Those use a session cookie or OAuth.
Behaviour shared by all write endpoints
- Data is attached to today’s ARC Day automatically. You do not send
dayId. - Optional
time/startTimefields are ISO 8601 datetimes. - Success envelope:
{ "ok": true, "data": { … } }. - Error envelope:
{ "ok": false, "error": { "code", "message", "details?" } }. - Responses are sanitized (ids + what you logged, not emails or other profile fields).
- Logging a workout/cardio/water/meditation through this API uses the same services as the app, so matching webhooks can fire (
training_logged,cardio_logged,water_logged,meditation_logged).
Endpoints
All paths below are relative to /api/public/v1.
GET /exercises/search
The only read endpoint. Use it to resolve exerciseId before POST /workouts.
Query: q (optional search string).
curl "https://arcos.corgicy.com/api/public/v1/exercises/search?q=bench" \
-H "Authorization: Bearer arc_YOUR_KEY"
{
"ok": true,
"data": [
{ "id": "uuid", "name": "Bench press", "muscleGroup": "Chest" }
]
}
Ids are from your exercise catalog, not a global library.
POST /water
| Field | Type | Required | Notes |
|---|---|---|---|
volume | number | yes | Millilitres, must be positive |
time | ISO datetime | no | Defaults to now |
notes | string | no |
{ "volume": 500, "notes": "After workout" }
POST /workouts
| Field | Type | Required | Notes |
|---|---|---|---|
splitLabel | string | yes | e.g. Push, Pull, Legs (your split names) |
startTime / endTime | ISO datetime | no | |
sessionRPE | int 1–10 | no | |
notes | string | no | |
exercises | array | no | { exerciseId, sets: [{ setNumber, reps?, weight? }] } |
exerciseId must be a UUID from GET /exercises/search. setNumber and reps are positive integers; weight is a positive number.
{
"splitLabel": "Push",
"sessionRPE": 7,
"exercises": [
{
"exerciseId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"sets": [{ "setNumber": 1, "reps": 10, "weight": 100 }]
}
]
}
POST /steps
| Field | Type | Required | Notes |
|---|---|---|---|
steps | int | yes | Non-negative |
time | ISO datetime | no | |
notes | string | no |
POST /meditation
| Field | Type | Required | Notes |
|---|---|---|---|
duration | int | yes | Minutes, positive |
type | string | no | Free text, e.g. mindfulness |
time | ISO datetime | no | |
notes | string | no |
POST /cardio
| Field | Type | Required | Notes |
|---|---|---|---|
type | string | yes | e.g. Running, Cycling |
duration | int | yes | Minutes, positive |
intensity | enum | no | easy | zone2 | hard (default easy) |
distance | number | no | Positive |
pace | number | no | Positive |
averageHeartRate / maxHeartRate | int | no | Positive bpm |
calories | int | no | Positive |
elevationGain | int | no | Non-negative |
startTime / endTime | ISO datetime | no | |
notes | string | no |
POST /mood
| Field | Type | Required | Notes |
|---|---|---|---|
mood | int 1–10 | yes | |
energy | int 1–10 | yes | |
notes | string | no | |
time | ISO datetime | no |
Writes today’s day mood/energy fields.
POST /sleep
| Field | Type | Required | Notes |
|---|---|---|---|
duration | number | yes | Hours, positive |
bedtime / wakeTime | ISO datetime | no | |
quality | int 1–10 | no | |
notes | string | no |
Sets today’s sleepHours (and echoes bedtime/wake when provided).
POST /body-metrics
| Field | Type | Required | Notes |
|---|---|---|---|
weight | number | yes | Kilograms, positive |
bodyFat | number | no | 0–100 percent |
notes | string | no | |
time | ISO datetime | no | Used as the measurement date |
Errors
| Code | HTTP | Meaning |
|---|---|---|
UNAUTHORIZED | 401 | Missing, invalid, expired, or revoked key |
VALIDATION_ERROR | 400 | Zod schema failed (details.errors[] with path + message) |
RATE_LIMIT_EXCEEDED | 429 | Too many requests for this key |
INTERNAL_ERROR | 500 | Server error |
{
"ok": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": {
"errors": [{ "path": ["volume"], "message": "Expected number, received string" }]
}
}
}
Rate limits
Default: 500 requests / 15 minutes / API key (env: RATE_LIMIT_PUBLIC_API_MAX, RATE_LIMIT_PUBLIC_API_TIME_WINDOW).
Response headers: x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset, retry-after.
{
"statusCode": 429,
"error": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded for public API. Maximum 500 requests per 900 seconds.",
"retryAfter": 300
}
Managing keys (session API)
These routes use the logged-in cookie, not arc_…. Same UI: Settings → Developer.
| Method | Path | Notes |
|---|---|---|
GET | /api/v1/api-keys | Prefix, last used, expiry, revoked |
POST | /api/v1/api-keys | { "name": "Watch sync" } → returns key once |
POST | /api/v1/api-keys/:keyId/revoke | Immediate |
POST | /api/v1/api-keys/:keyId/rotate | Revokes old, returns new key once |
DELETE | /api/v1/api-keys/:keyId |
Treat keys like passwords. Do not commit them. Prefer one key per integration so you can revoke a single device.
Examples
Python
import os, requests
BASE = "https://arcos.corgicy.com/api/public/v1"
headers = {
"Authorization": f"Bearer {os.environ['ARC_API_KEY']}",
"Content-Type": "application/json",
}
r = requests.post(f"{BASE}/water", headers=headers, json={"volume": 500})
r.raise_for_status()
print(r.json())
Node
const BASE = 'https://arcos.corgicy.com/api/public/v1';
async function logMood(mood, energy) {
const res = await fetch(`${BASE}/mood`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ARC_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ mood, energy }),
});
return res.json();
}
What this API cannot do
- Read tasks, Brain notes, finance, people, or forms (
arc_…keys cannot call/api/v1/brain/*) - Submit or list Brain forms with an API key — use the session Brain HTTP API below, MCP
brain_form_*, or the public form URL - Register webhook URLs (session + subscription)
- Replace MCP for assistants
If you log water here and subscribe to water_logged, your webhook URL will receive the event after the write succeeds.
Troubleshooting
| Symptom | Check |
|---|---|
| 401 | Bearer arc_…, key not revoked, subscription still active |
| 400 | Types: numbers not strings; ISO dates; intensity exactly easy/zone2/hard |
Workout 400 on exerciseId | Call GET /exercises/search first; id must exist in your catalog |
| 429 | Back off using retry-after |
| Data on the wrong day | Public API always uses today in the server’s day logic; it is not a historical import API |
| Nothing in the app | You are looking at a different user/day; refresh Today |
Brain HTTP API (session)
Brain notes, databases, and forms are not on /api/public/v1. They live on /api/v1/brain/* with the login cookie (same as the web app). MCP brain_* / brain_form_* calls the same services.
Full catalog: HTTP API endpoint catalog. Product: Brain.
Forms (authenticated)
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/brain/forms | List your forms |
POST | /api/v1/brain/forms | Create (name + databaseId or createDatabase.name; optional config, syncFields) |
GET | /api/v1/brain/forms/:id | Editor payload (form + fields) |
PATCH | /api/v1/brain/forms/:id | Update name/config |
DELETE | /api/v1/brain/forms/:id | Delete |
POST | /api/v1/brain/forms/:id/publish | { "published": true | false } — assigns a publicSlug when publishing |
POST | /api/v1/brain/forms/:id/submit | { "values": { … } } — owner/session submit (same pipeline as MCP brain_form_submit) |
POST …/submit and the public submit both create a database record and fire webhook brain_form_submitted for the form owner. See Outgoing webhooks.
# Session cookie from a logged-in browser, or replay it in curl
curl -X POST https://arcos.corgicy.com/api/v1/brain/forms \
-H "Content-Type: application/json" \
-b "token=YOUR_SESSION" \
-d '{"name":"Intake","createDatabase":{"name":"Intake responses"}}'
POST /api/v1/brain/forms/{id}/submit
Content-Type: application/json
{ "values": { "message": "Hello" } }
201 with { form, database, record, quiz?, thankYouMessage, … }. Errors: NOT_FOUND 404, VALIDATION 400, ALREADY_SUBMITTED 409, CLOSED 410.
Forms (public, no login required)
Published UI: https://arcos.corgicy.com/brain/f/{publicSlug}
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/brain/public/forms/:publicSlug | Load questions (optional session). Password: header X-Brain-Access-Password or query accessPassword |
POST | /api/v1/brain/public/forms/:publicSlug/submit | { "values": { … }, "accessPassword"?, "respondentKey"? } |
Submit sets respondent cookies when the form is one-response-per-person. Rate-limited per slug.
curl -X POST https://arcos.corgicy.com/api/v1/brain/public/forms/support-intake/submit \
-H "Content-Type: application/json" \
-d '{"values":{"message":"Need help with billing"}}'
Other Brain session routes (summary)
Notes, folders, search, tags, links, graph, views, canvases, attachments, properties, databases (fields/records/views/CSV), templates, filing rules, sync, export/import, stats, embeds. See ENDPOINTS.md — Brain.