FastAPI adapter that exposes a clean JSON REST API over a Baïkal CardDAV server. Designed for automation platforms like n8n — no hand-crafted WebDAV or vCard XML required.
Multiple address books are supported. Each book is a separate namespace in the URL, so leads, customers, and personal contacts can be managed through one adapter instance while staying isolated from each other.
cp .env.example .env
docker compose up -dBoth Baïkal (port 8800) and the adapter (port 8000) start. The adapter will be unhealthy until Baïkal is configured — that's expected.
Open http://localhost:8800 and complete the setup wizard. You'll set an admin password and a few basic options. After the wizard, you're taken to the admin panel.
In the Baïkal admin panel, go to Users and books → + Add user. Fill in a username and password — these are the credentials the adapter will use to talk to Baïkal (not the admin password). Note them down.
Still in the user row, click the address books icon and add an address book. The URL path (slug) shown below the display name is what the API uses — for example, a book called "Contacts" typically gets the slug contacts. You can have multiple books; each becomes a separate namespace in the API.
Open the .env file you copied in step 1 and fill in at minimum:
BAIKAL_USER=the-username-you-created
BAIKAL_PASS=the-password-you-set
API_KEY=any-long-random-string-you-chooseBAIKAL_URL is already set to the internal Docker network address — leave it as-is.
docker compose restart carddav-restcurl http://localhost:8000/api/addressbooks \
-H "X-API-Key: your-api-key"You should see the address book(s) you created listed in the response. The adapter is ready.
docker build -t carddav-rest:latest .
docker compose up -d carddav-restEvery /api/* endpoint requires the X-API-Key header. /health, /docs, and /redoc are public.
curl http://localhost:8000/api/addressbooks \
-H "X-API-Key: your-api-key"A missing or wrong key returns 401 Invalid or missing API key.
Quick reference for integration builders. For full request/response details see:
- Interactive (Swagger UI): http://localhost:8000/docs
- Markdown reference: docs/api-reference.md
| Method | Path | Purpose |
|---|---|---|
GET |
/api/addressbooks |
List available address books |
GET |
/api/addressbooks/{book}/contacts |
List contacts (pagination + q quick search) |
POST |
/api/addressbooks/{book}/contacts/search |
Structured search by name, email, or phone |
POST |
/api/addressbooks/{book}/contacts |
Create a contact |
GET |
/api/addressbooks/{book}/contacts/{uid} |
Get a contact |
GET |
/api/addressbooks/{book}/contacts/{uid}/vcard |
Download raw vCard |
PUT |
/api/addressbooks/{book}/contacts/{uid} |
Full update (replaces all managed fields) |
PATCH |
/api/addressbooks/{book}/contacts/{uid} |
Partial update (only listed fields change) |
POST |
/api/addressbooks/{book}/contacts/{uid}/merge/{other_uid} |
Merge two contacts |
POST |
/api/addressbooks/{book}/contacts/{uid}/move/{target_book} |
Move contact to another book |
DELETE |
/api/addressbooks/{book}/contacts/{uid} |
Delete a contact |
GET |
/api/stats |
Contact counts and timestamps per address book |
GET |
/api/config |
Active runtime configuration |
GET |
/health |
Health check (no API key) |
Phone numbers are normalized to E.164 format (+<countrycode><number>) on every write and search. Numbers without a country code use DEFAULT_COUNTRY_CODE.
06301234567 → +36301234567 (DEFAULT_COUNTRY_CODE=HU)
+36301234567 → +36301234567 (already E.164, unchanged)
An unparseable number returns 422 {"detail": "Invalid phone number: <value>"}.
Pass check_duplicates: true when creating a contact. The adapter searches for an existing contact with the same email address or phone number and returns 409 Conflict if found — instead of creating a duplicate.
Beyond the built-in rule that at least one of firstname or lastname must be present, operators can enforce additional fields via REQUIRED_FIELDS (comma-separated). Applies to create, full update, and partial update (validated against the resulting contact, not the patch body alone).
REQUIRED_FIELDS=emails,phones
A request that violates any required field returns 422 {"detail": "Missing required field(s): emails, org"}.
Controls how the display name (fn) is assembled from structured name parts.
| Value | Format | Example |
|---|---|---|
western |
Prefix Firstname Middlename Lastname Suffix | Dr. Jane Marie Smith PhD |
eastern |
Lastname Firstname | Smith Jane |
eastern_full |
Prefix Lastname Firstname Suffix | Dr. Smith Jane PhD |
| Variable | Required | Default | Description |
|---|---|---|---|
BAIKAL_URL |
yes | — | e.g. http://baikal/dav.php (internal Docker network) |
BAIKAL_USER |
yes | — | Baïkal user the adapter authenticates as |
BAIKAL_PASS |
yes | — | Baïkal password |
API_KEY |
yes | — | Key clients send in X-API-Key |
NAME_FORMAT |
no | western |
See Name format above |
DEFAULT_COUNTRY_CODE |
no | HU |
ISO 3166-1 alpha-2 region for phone normalization |
REQUIRED_FIELDS |
no | (empty) | Comma-separated contact field names that must be non-empty |
python -m venv .venv
.venv\Scripts\pip install -r requirements-dev.txt # Windows
# .venv/bin/pip install -r requirements-dev.txt # macOS/Linux
python -m pytest tests -vThe n8n-nodes-carddav-rest package wraps this adapter for n8n workflows. Install it from Settings → Community Nodes inside n8n, or see the n8n-node/ directory for local development instructions.
The adapter image is published to ghcr.io/oregapam/carddav-rest. The docker-compose.yml pulls this image by default; local builds can override it with ADAPTER_IMAGE=carddav-rest:latest.
One-time setup — generate a Personal Access Token:
GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic) → Generate new token → scopes: write:packages, read:packages
echo "YOUR_TOKEN" | docker login ghcr.io -u oregapam --password-stdinBuild and push:
VERSION=0.1.2 # match the release version
docker build -t ghcr.io/oregapam/carddav-rest:${VERSION} \
-t ghcr.io/oregapam/carddav-rest:latest .
docker push ghcr.io/oregapam/carddav-rest:${VERSION}
docker push ghcr.io/oregapam/carddav-rest:latestMake the package public (first time only): GitHub → Profile → Packages → carddav-rest → Package settings → Change visibility → Public
cd n8n-node
npm run build
npm publish --access public