API Reference
Base URL
/api/v1
Authentication
Auth endpoints (/login, /register, /logout, /password-reset/*) are public. All other endpoints require authentication.
Admin endpoints (/admin/*) additionally require the user to have the admin flag set.
Login sets an httpOnly JWT cookie. Subsequent requests must include it (i.e. send with credentials: 'include'). The cookie name and domain are configured via COOKIE_DOMAIN and COOKIE_SECURE.
Alternatively, API tokens can be used in the Authorization header:
Authorization: Bearer mycorrhizal_<token>
API tokens are created and managed via the admin endpoints, the tokens always have normal user rights. The plaintext token is only returned once at creation time.
Error Responses
All errors follow the same structure:
{
"code": "NOT_FOUND",
"message": "Contact not found",
"details": {}
}
Common error codes:
| Code | HTTP status |
|---|---|
UNAUTHORIZED | 401 |
INVALID_CREDENTIALS | 401 |
TOKEN_EXPIRED | 401 |
FORBIDDEN | 403 |
NOT_FOUND | 404 |
VALIDATION_ERROR | 400 |
INVALID_INPUT | 400 |
ALREADY_EXISTS | 409 |
RATE_LIMIT_EXCEEDED | 429 |
INTERNAL_ERROR | 500 |
Request IDs
Every request and response carries an X-Request-ID header created by the middleware.
Endpoints
Auth
| Method | Path | Description |
|---|---|---|
POST | /register | Create a new user account |
POST | /login | Authenticate and set session cookie |
POST | /logout | Clear session cookie |
POST | /check-password-strength | Validate a password without registering |
POST | /password-reset/request | Send a password reset email |
POST | /password-reset/confirm | Apply a password reset token |
Users
| Method | Path | Description |
|---|---|---|
GET | /users/me | Get the current user |
POST | /users/change-password | Change password |
PATCH | /users/language | Update UI language preference |
PATCH | /users/date-format | Update date format preference |
GET | /users/custom-fields | Get custom field names |
PATCH | /users/custom-fields | Update custom field names |
Contacts
| Method | Path | Description |
|---|---|---|
GET | /contacts | List contacts (supports search and circle filter) |
POST | /contacts | Create a contact |
GET | /contacts/:id | Get a contact (supports filtering the returned fields) |
PUT | /contacts/:id | Update a contact |
DELETE | /contacts/:id | Delete a contact |
POST | /contacts/:id/archive | Archive a contact |
POST | /contacts/:id/unarchive | Unarchive a contact |
GET | /contacts/circles | List all circles in use |
GET | /contacts/random | Get five random contacts |
GET | /contacts/birthdays | Get upcoming birthdays |
POST | /contacts/:id/profile_picture | Upload a profile picture (multipart) |
GET | /contacts/:id/profile_picture | Get a contact’s profile picture |
GET | /proxy/image | Proxy an external image URL for upload preview |
Relationships
| Method | Path | Description |
|---|---|---|
GET | /contacts/:id/relationships | List outgoing relationships |
GET | /contacts/:id/incoming-relationships | List incoming relationships |
POST | /contacts/:id/relationships | Create a relationship |
PUT | /contacts/:id/relationships/:rid | Update a relationship |
DELETE | /contacts/:id/relationships/:rid | Delete a relationship |
Notes
| Method | Path | Description |
|---|---|---|
GET | /contacts/:id/notes | List notes for a contact |
POST | /contacts/:id/notes | Create a note for a contact |
GET | /notes | List unassigned notes |
POST | /notes | Create an unassigned note |
GET | /notes/:id | Get a note |
PUT | /notes/:id | Update a note |
DELETE | /notes/:id | Delete a note |
Activities
| Method | Path | Description |
|---|---|---|
GET | /activities | List all activities |
POST | /activities | Create an activity |
GET | /activities/:id | Get an activity |
PUT | /activities/:id | Update an activity |
DELETE | /activities/:id | Delete an activity |
GET | /contacts/:id/activities | List activities for a contact |
Reminders
| Method | Path | Description |
|---|---|---|
GET | /reminders | List all reminders |
GET | /reminders/upcoming | List upcoming reminders (used by dashboard) |
GET | /reminders/:id | Get a reminder |
PUT | /reminders/:id | Update a reminder |
DELETE | /reminders/:id | Delete a reminder |
POST | /reminders/:id/complete | Mark a reminder complete (creates timeline entry) |
GET | /contacts/:id/reminders | List reminders for a contact |
POST | /contacts/:id/reminders | Create a reminder for a contact |
GET | /contacts/:id/reminder-completions | List completion history for a contact (timeline entries) |
DELETE | /reminder-completions/:id | Delete a completion entry |
Import
| Method | Path | Description |
|---|---|---|
POST | /contacts/import/upload | Upload a CSV file, returns parsed preview data |
POST | /contacts/import/preview | Apply column mapping, returns contacts with duplicate detection |
POST | /contacts/import/confirm | Execute the import with per-row decisions |
POST | /contacts/import/vcf/upload | Upload a VCF file, returns contacts with duplicate detection |
POST | /contacts/import/vcf/confirm | Execute the VCF import |
POST | /contacts/import/jscontact/upload | Upload a JSContact (RFC 9553) JSON file, returns contacts with duplicate detection |
POST | /contacts/import/records | Start an import from a JSON batch of neutral Card/CRM records (bulk-import API for external clients) |
Bulk import contract (external clients)
The records endpoint is the repeatable bulk-import contract for external clients (e.g. the Android app): it serves both a first-run “import your contacts” prompt and a standing “Import from contacts” entry point with the same flow.
Session lifecycle. An import is a three-step, server-side-session flow:
POST /contacts/import/records(JSON batch, 1–500 records) — or an upload endpoint for a file — returns anImportPreviewResponsewith asession_id, per-row duplicate detection (duplicate_match+merge_diff), and within-batch detection (batch_duplicate_of).- (CSV only)
POST /contacts/import/previewapplies column mappings to the session. POST /contacts/import/vcf/confirm(VCF/JSContact/records) or/contacts/import/confirm(CSV) executes the import with one action per row:add,update(merge into the matched existing contact), orskip. The response is anImportResultsummary.
Idempotent confirm. Confirming a session consumes it. A retry of the same session_id within the 15-minute session window returns the original result as a no-op — it does not re-apply the import. This makes a dropped confirm response safe to retry: a client that times out mid-request can simply re-send the same confirm instead of re-uploading (which would create duplicates). To change a decision, start a new upload (new session_id).
Retry guidance. If a confirm is retried and the session has aged out (>15 min, or the server restarted), it returns 404 NOT_FOUND; the client must re-upload. The confirm endpoints are type-scoped: /contacts/import/confirm rejects records-sourced sessions and /contacts/import/vcf/confirm rejects CSV-sourced ones — a retried confirm must target the same endpoint the preview came from.
Guarantees. Sessions are scoped to the owning user; a foreign or expired session_id is 404. Sessions are held in memory only and lost on server restart. Batch limit is 500 records; the import commits per-row, so one failing row does not abort the rest. Ownership scoping, per-row decisions, and the 15-minute expiry are enforced server-side.
Export
| Method | Path | Description |
|---|---|---|
GET | /export | Download all data as CSV |
GET | /export/vcf | Download all contacts as VCF (includes photos) |
Network
| Method | Path | Description |
|---|---|---|
GET | /graph | Get contact network graph data |
API Tokens
| Method | Path | Description |
|---|---|---|
GET | /admin/api-tokens | List all API tokens for the current user |
POST | /admin/api-tokens | Create an API token — returns the plaintext token once |
DELETE | /admin/api-tokens/:id | Revoke an API token |
POST /admin/api-tokens body:
{ "name": "my token" }
Response includes token (the mycorrhizal_… plaintext value) only on creation. Subsequent list responses omit it.
Admin
| Method | Path | Description |
|---|---|---|
GET | /admin/users | List all users |
GET | /admin/users/:id | Get a user |
PATCH | /admin/users/:id | Update a user (e.g. set admin flag) |
DELETE | /admin/users/:id | Delete a user |
Health
All three are unauth, unversioned, and accessible directly (without prepending the base URL). See deployment.md for which probe each consumer should use.
| Method | Path | Description |
|---|---|---|
GET | /health/live | Liveness — is the process running? Returns immediately, no I/O. For restart policies. |
GET | /health/ready | Readiness — can this instance serve? DB + migrations + filesystem. 503 when not. For traffic gating. |
GET | /health | Deep health — the healthy/degraded/unhealthy roll-up, plus the build version and client-compat fields. Only 503s on a DB read failure. The per-facet breakdown (job names, integrity/restore-drill state, integration reachability) is admin-only at /admin/system-status (issue #864). |