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>

App passwords / API tokens are created and managed by each user under Settings → API tokens (/api/v1/api-tokens); a token is limited to its explicit scopes (carddav, caldav, api:read, api:write, mcp), never has admin rights, and cannot manage credentials or enroll a second factor. Credential and identity management require an interactive session. The plaintext token is only returned once at creation time.

Error Responses

All errors follow the same structure:

{
  "error": {
    "code": "NOT_FOUND",
    "message": "Contact not found",
    "details": {}
  },
  "request_id": "…",
  "timestamp": "2026-01-01T00:00:00Z"
}

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
PAYLOAD_TOO_LARGE 413
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
POST /login/2fa Complete a two-step login with a TOTP or recovery code
POST /webauthn/login/begin Start a passkey assertion for a pending two-step login (public, rate-limited)
POST /webauthn/login/finish Complete a two-step login with a passkey assertion and set the session cookie
POST /webauthn/register/begin, /webauthn/register/finish Enroll a passkey (needs a live second-factor proof once one is already enrolled)
POST /webauthn/assert/begin Start a passkey assertion used as the proof for removing a passkey or adding a factor
GET /webauthn/credentials List the caller’s passkeys
PATCH /webauthn/credentials/:id Rename one of the caller’s passkeys ({"name": "…"}, 1–100 characters; no second-factor proof — a label is not a security change). An unnamed passkey is labelled after the enrolling browser and platform, e.g. “Chrome 153 on Linux”
DELETE /webauthn/credentials/:id Remove a passkey (needs a live second-factor proof)

These routes, and everything else under /webauthn/*, are absent from an embedded (Android local-profile) deployment.

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
PATCH /users/time-format Update time format (auto/12h/24h) 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/:id/score Relationship health score breakdown. Each facet (and target) carries an English reason plus a stable reason_code and reason_params for clients to render it in the user’s language
PUT /contacts/:id/target-closeness Set the contact’s target closeness ({"tier": "immediate"\|"close"\|"casual"\|"distant"}). The response includes a cadence_policy_offer when the contact has no cadence policy
DELETE /contacts/:id/target-closeness Clear the target and dismiss its pending “behind target” suggestions
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:

  1. POST /contacts/import/records (JSON batch, 1–500 records) — or an upload endpoint for a file — returns an ImportPreviewResponse with a session_id, per-row duplicate detection (duplicate_match + merge_diff), and within-batch detection (batch_duplicate_of).
  2. (CSV only) POST /contacts/import/preview applies column mappings to the session.
  3. 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), or skip. The response is an ImportResult summary.

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)
GET /export/account Download the full-fidelity, re-importable account bundle (JSON; every sensitivity level and suggested rows included; size limit in the X-Mycorrhizal-Bundle-Max-Bytes header)
POST /import/mycorrhizal/upload Upload an account bundle; also /import/mycorrhizal/fetch, /status, /preview, /confirm, /cancel for the source-import flow

Feeds

Private Atom feeds (ADR 0030). Feed management requires authentication; the feed itself is served to a feed reader by an unguessable token, not a session.

Method Path Description
GET /feeds List the caller’s active feeds (never returns a token)
POST /feeds Create a per-contact or aggregate feed; the feed URL, which carries the token, is returned once
POST /feeds/:id/rotate Replace a feed’s token and revoke the old URL
DELETE /feeds/:id Revoke a feed
POST /feeds/revoke-all Revoke every feed the caller owns
GET /feeds/atom?token=… Public, rate-limited Atom document; every miss is an identical empty 404. Sensitivity-filtered

Network

Method Path Description
GET /graph Contact network graph. By default the ego network (depth 2) of your self contact; ?from=<vCard UID> re-centres it on another (non-archived) contact, ?depth=1..5 sets the radius, and ?full=true returns the whole account. Accounts without a self contact get the full graph. The response’s scoped, from_vcard_uid and depth say which view it is
GET /graph/connections Multi-hop relationship chains from ?from=<vCard UID> (?depth up to 5, optional ?relation)

Dashboard

Method Path Description
GET /dashboard The dashboard composite. Each list block (birthdays, upcoming_reminders, overdue, reach_out_suggestions, contact_sync_conflicts, data_decay_overdue) holds at most 25 rows, most urgent first; the matching *_total field carries the uncapped count

App passwords / API tokens

Method Path Description
GET /api-tokens List all tokens for the current user
POST /api-tokens Create a token — returns the plaintext token once
POST /api-tokens/:id/rotate Revoke and reissue with the same scopes
POST /api-tokens/revoke-all Revoke every active token for the caller
DELETE /api-tokens/:id Revoke one token

POST /api-tokens body:

{ "name": "Thunderbird CardDAV", "expires_in_days": 90, "scopes": ["carddav"] }

scopes is an explicit set of carddav, caldav, api:read, api:write, mcp. The legacy scope: "full" | "carddav" field is still accepted when scopes is absent. A DAV-only scope set may send never_expires: true instead of expires_in_days; API/MCP tokens must expire. Response includes token (the mycorrhizal_… plaintext value) only on creation. Subsequent list responses omit it.

CardDAV/CalDAV account password

PATCH /users/dav-password-policy with { "allow": true | false } controls whether the account password may authenticate DAV clients when no second factor is enrolled. It is always rejected once TOTP or a passkey is enrolled; use an app password with the carddav and/or caldav scope.

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).

Back to top

Mycorrhizal CRM - Self-hosted personal contact management

This site uses Just the Docs, a documentation theme for Jekyll.