REST API guide

Base URL, authentication, the case model, responses and errors, rate limits, uploads, and working examples for the Custody Commander REST API.

OpenAPI 3.1Bearer tokens120 requests/minute

The REST API gives you the same operations the app uses. This guide covers the conventions; for every operation's fields, see the Swagger reference or the live OpenAPI document.

Base URL and authentication#

https://app.custodycommander.com/api/v1

Authenticate every request with a personal API token:

export CASE_COMMANDER_URL="https://app.custodycommander.com"
export CASE_COMMANDER_API_TOKEN="cc_live_…"   # keep this out of shell history and code

curl "$CASE_COMMANDER_URL/api/v1/case" \
  -H "Authorization: Bearer $CASE_COMMANDER_API_TOKEN"

Cookies are never used for API authentication. A missing or invalid token returns 401 with a WWW-Authenticate: Bearer header.

Cases and court cases#

Two IDs appear everywhere:

  • caseId — your case (the matter). Get it from GET /case.
  • courtCaseId — one court file inside it. Get them from GET /court-cases?caseId=….

Evidence, documents, to-dos, the timeline, hearings, and messages accept an optional courtCaseId to narrow to one court file; leave it out to cover the whole case.

Working in a shared case#

Requests use your own case by default. To work in a case someone shared with you, send an X-Case-Context header — matter:<caseId> (or ws:<workspaceId>). POST /workspaces/switch returns the value to use. The header selects which view you're working in; the token's access and your sharing permissions still apply.

curl "$CASE_COMMANDER_URL/api/v1/case" \
  -H "Authorization: Bearer $CASE_COMMANDER_API_TOKEN" \
  -H "X-Case-Context: matter:CASE_ID"

Requests and responses#

  • Send JSON bodies with Content-Type: application/json (up to 4 MB). File uploads use multipart/form-data.
  • Responses are the same JSON the app receives — there's no wrapper. Some list endpoints return a bare array (GET /evidence); others return an object.
  • File downloads stream as binary with a Content-Disposition filename.
  • Creates usually return 201.
  • Every response includes x-api-operation (the operation ID) and cache-control: no-store.

Errors#

Errors return JSON with an error message and usually a code:

{ "error": "This operation requires evidence:write.", "code": "insufficient_scope" }
StatusCommon codesWhat to do
400invalid_request, invalid_context, invalid_fileFix the request body, query, or context header.
401invalid_tokenCheck the token; it may be expired or revoked.
402quota, evidence_limit, storage limitAI allowance used up, free-plan evidence limit, or storage full. The message explains which.
403insufficient_scope, forbidden, feature, email_unverifiedAdd the ability to the token, check sharing permissions, or the plan lacks that AI feature.
404operation_not_found, not_foundCheck the path and method against the reference.
409—The record changed since you read it. Re-fetch and retry.
413file_too_large, request_too_large, resource_limitSplit the upload or the file.
429rate_limited, rate, resource_limitWait for Retry-After seconds, then retry.

Rate limits#

  • 120 requests per minute per account, shared by every token and by REST and MCP together. The window resets on the clock minute; a 429 includes Retry-After.
  • Built-in AI operations are also limited to 20 per 10 minutes per user, as in the app.

Pagination#

Most list endpoints return everything you can see. The exceptions:

  • GET /messages returns up to 2,000 messages per call — pass offset for the next page.
  • GET /documents/{id}/versions returns 50 versions per page with nextCursor; pass it back as before.

Safe retries and conflicts#

There are no idempotency keys, so don't automatically retry a change after a timeout — read the resource first to see whether it went through. Some operations protect against overwriting someone else's changes:

  • PATCH /documents and version restore accept expectedUpdatedAt.
  • POST /preparation requires the current revision.
  • Court-document operations require expectedHash.

A mismatch returns 409.

Uploading files#

Uploads use multipart/form-data. Limits: 25 MB per file, 30 files and 32 MB per request.

OperationFields
POST /evidencecaseId, files (repeat for each file); optional folderId, courtCaseId, paths (folder paths, parallel to files). recordingConsent=yes is required if any file is audio or video.
POST /documents/uploadSame as evidence. Text is extracted without AI.
POST /messages/importcaseId, file, and attest=yes (confirms the export is from your own device or account, or one you're authorized to access). Optional platform (sms, whatsapp, facebook, instagram, email, other), selfName, otherName, device, courtCaseId.

Examples#

curl "$CASE_COMMANDER_URL/api/v1/court-cases?caseId=CASE_ID" \
  -H "Authorization: Bearer $CASE_COMMANDER_API_TOKEN"
curl -X POST "$CASE_COMMANDER_URL/api/v1/folders" \
  -H "Authorization: Bearer $CASE_COMMANDER_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"caseId":"CASE_ID","name":"School expenses","kind":"evidence"}'
curl "$CASE_COMMANDER_URL/api/v1/evidence" \
  -H "Authorization: Bearer $CASE_COMMANDER_API_TOKEN" \
  -F "caseId=CASE_ID" -F "folderId=FOLDER_ID" -F "courtCaseId=COURT_CASE_ID" \
  -F "files=@./receipt.pdf"
curl -X PATCH "$CASE_COMMANDER_URL/api/v1/evidence/EVIDENCE_ID" \
  -H "Authorization: Bearer $CASE_COMMANDER_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"folderId":"FOLDER_ID","tagNames":["expenses"],"description":"School receipt","eventDate":"2026-08-21","exhibitNumber":"07"}'
curl -X POST "$CASE_COMMANDER_URL/api/v1/hearings" \
  -H "Authorization: Bearer $CASE_COMMANDER_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"caseId":"CASE_ID","courtCaseId":"COURT_CASE_ID","date":"2026-11-03","time":"09:30","purpose":"Status review"}'
curl -X POST "$CASE_COMMANDER_URL/api/v1/todos" \
  -H "Authorization: Bearer $CASE_COMMANDER_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"caseId":"CASE_ID","title":"Gather school records","due":"2026-10-15","courtCaseId":"COURT_CASE_ID"}'
curl -G "$CASE_COMMANDER_URL/api/v1/timeline" \
  -H "Authorization: Bearer $CASE_COMMANDER_API_TOKEN" \
  --data-urlencode "caseId=CASE_ID" \
  --data-urlencode "kinds=event,hearing" \
  --data-urlencode "years=2025,2026"
curl "$CASE_COMMANDER_URL/api/v1/messages/import" \
  -H "Authorization: Bearer $CASE_COMMANDER_API_TOKEN" \
  -F "caseId=CASE_ID" -F "attest=yes" -F "platform=sms" \
  -F "file=@./sms-backup.xml"
curl -o receipt.pdf "$CASE_COMMANDER_URL/api/v1/files/FILE_ID" \
  -H "Authorization: Bearer $CASE_COMMANDER_API_TOKEN"

Operations by area#

There are 140 operations. Paths are relative to /api/v1; the ability needed is <area>:read for GET and <area>:write otherwise.

AreaKey operations
caseGET /case · POST /case/update (caption, role, case numbers, key dates — arrays replace existing values)
court-casesGET · POST · PATCH ?id= · DELETE ?id= on /court-cases
evidenceGET /evidence (filters: q, issueId, priority, tag, courtCaseId, folderId) · POST /evidence · GET|PATCH|DELETE /evidence/{id} · POST /evidence/{id}/extract (AI) · POST /evidence/{id}/transcribe · POST /evidence/extract-bulk (AI)
filesGET /files/{id} (original; ?preview=1 for a browser-friendly copy) · GET /files/{id}/text
foldersGET /folders?kind=evidence|document · POST · PATCH ?id= · DELETE ?id=&deleteItems=
issuesPOST /issues
documentsGET|POST|PATCH|DELETE /documents · POST /documents/upload · POST /documents/draft (AI) · GET /documents/{id}/export?format=pdf|md · GET|POST /documents/{id}/versions · court formatting, review, signing, and final export under /documents/{id}/court and POST /documents/{id}/export
templatesGET /templates · GET /templates/{id} · GET /templates/{id}/pdf
messagesGET /messages (many filters, 2,000 per call) · POST /messages (log a sent/received message — nothing is transmitted) · PATCH /messages?id= (tags) · POST /messages/import · GET /messages/threads · GET /messages/search · POST /messages/bulk · POST /messages/analyze (AI) · POST /messages/check (AI tone check)
timelineGET /timeline (kinds, years, months) · POST · DELETE ?id=
hearingsGET|POST|PATCH|DELETE /hearings · GET|POST /hearings/{id}/resources
todosGET|POST|PATCH|DELETE /todos
contactsGET|POST|PATCH|DELETE /contacts
commsPOST /comms/draft (AI) · POST /comms/pull-imported
exportGET|POST /export/court-packet · GET /export/evidence-index · GET /export/message-log
preparationGET /preparation · POST /preparation (typed actions with revision) · GET /preparation/export (binders and backups)
mediationSessions, transcript turns, and suggestions under /mediation/* (suggestions, redo, and simulate use AI)
sharesGET /shares · POST /shares/invite (emails the recipient) · PATCH|DELETE /shares · comments, defaults, per-item sharing, and GET /shares/inbox
collaboration · packagesClient-exchange requests, submissions, and messages · release, view, acknowledge, and revoke packages
billingGET /billing/status (plan, entitlements, and usage) · GET /billing/invoices · checkout and portal URLs · POST /billing/upgrade (changes your subscription immediately)
accountGET /account (email, name, member since) · security settings · terms status and acceptance
workspacesGET /workspaces · POST /workspaces/switch
tokensGET|POST|PATCH|DELETE /developer/tokens
supportGET|POST /support/tickets
assistant (Full access only)POST /assistant/chat (AI) · two-step deletion: POST /assistant/deletions, then POST /assistant/deletions/{id}/confirm after a person confirms

For plan and usage information, use GET /billing/status rather than GET /account.

Deletes are permanent

DELETE /evidence/{id}, DELETE /documents, DELETE /folders with deleteItems=true, DELETE /messages/threads, and POST /messages/bulk with delete-batch can't be undone. For deletions a person should approve, use the two-step /assistant/deletions flow.

Not available through the API#

Sign-up, sign-in, password reset, and email verification (integrations use tokens instead), payment webhooks, analytics beacons, and the MCP and OpenAPI transports themselves.

Versioning#

There's one version, v1. The operation catalog is generated from the app's own routes, so new app features appear in the API as they ship. Regenerate your client from the live OpenAPI document when you upgrade. Request fields in the reference are discovery hints; the server validates the full request.

Something here out of date or unclear? Tell us from the Help page in the app and we'll fix the docs.