API reference
Atelier's localhost REST API: authentication, error shapes, and worked examples for the most common endpoints across leads, weddings, vendors, payments, tasks, and reports.
Last updated: 2026-05-10
About this reference. The localhost REST API and the Settings → Local API panel ship with Atelier today. The endpoints below are the curated surface; the in-app
/api/docspage is the live source of truth. New endpoints continue to land alongside new Tauri commands as the API-first parity rule covers each new user action, so check the in-app docs for anything missing here.
This page is the customer-facing API reference. The authoritative version of every endpoint, with full request/response shapes and live response examples, will ship in the running app at http://127.0.0.1:7423/api/docs — open it in any browser when the local API is enabled.
The reference below is the curated, narrative version of the most common endpoints, organized by domain. For an endpoint not covered here, the in-app docs are the source of truth.
Auto-generation status. The end goal for this reference is automatic generation from the Rust handler signatures so it stays exactly in sync with the running API. That work is queued; for v1 the page is hand-curated against the live API surface. If you spot a discrepancy between this page and
/api/docsin the running app, the in-app version is correct — please report the drift to legal@dunamisstudios.net.
Authentication
Every endpoint requires a Bearer token in the Authorization header:
Authorization: Bearer atlr_your_api_key_here
The key is generated from Settings → Local API → Generate API key inside Atelier. It's per-installation; regenerating invalidates the previous key.
The main API binds to 127.0.0.1 only and cannot be exposed on 0.0.0.0 from Settings. The localhost-only default is the right answer for the full API surface; if you want a phone on the same WiFi to view the run-of-show, that's what the phone-friendly day-of view on the LAN listener is for.
Response shape
Every successful response is a JSON object with a single top-level key matching the resource:
{ "weddings": [ ... ] }
For single records, the key is singular:
{ "wedding": { ... } }
Errors return a 4xx or 5xx status with:
{ "error": "Human-readable error message", "field": "optional_field_name" }
The optional field key is present on validation errors and points at the specific field that failed validation (e.g. "field": "wedding_date" on a 400 from POST /api/weddings with a bad date).
Domains
The API is organized into the same domains as the app itself:
- Dashboard — multi-wedding summary
- Leads — CRM pipeline
- Weddings — the core entity
- Vendors — vendor database + per-wedding assignments
- Tasks — per-wedding to-dos
- Payments — money in
- Reports — pre-aggregated summaries
- Calendar — cross-wedding event feed
Dashboard
GET /api/dashboard/summary
Returns a multi-wedding overview suitable for rendering a dashboard or alerting on aggregate state.
Response (200 OK)
{
"summary": {
"active_weddings": 7,
"weddings_this_month": 2,
"weddings_next_30_days": 3,
"outstanding_payments_cents": 47250,
"overdue_tasks": 4,
"milestone_gaps": 2
}
}
The outstanding_payments_cents field aggregates across every active wedding and reflects the live paid_amount cache on every budget line — see the user-guide Budget tab for the maintenance contract.
Leads
GET /api/leads
Lists every lead in the CRM pipeline.
Query parameters
status— filter by pipeline stage (new,contacted,proposal_sent,won,lost)source— filter by lead source (referral,website,instagram, etc.)since— ISO date; only leads created after this date
Response (200 OK)
{
"leads": [
{
"id": "lead_01HX...",
"name": "Robin Patel",
"email": "robin@example.com",
"wedding_date_estimate": "2027-04-18",
"status": "contacted",
"source": "referral",
"assigned_team_member_id": "tm_01HW...",
"created_at": "2026-04-12T09:33:21Z"
}
]
}
POST /api/leads
Create a new lead.
Request body
{
"name": "Robin Patel",
"email": "robin@example.com",
"wedding_date_estimate": "2027-04-18",
"source": "referral",
"notes": "Met at the venue tour at Bayfront last weekend."
}
Response (201 Created) — the full created lead, same shape as in the list response.
Weddings
GET /api/weddings
Lists every wedding the studio is tracking.
Query parameters
status—active,archived,cancelledsince— ISO date; only weddings withwedding_date >= sinceuntil— ISO date; only weddings withwedding_date <= until
Response (200 OK)
{
"weddings": [
{
"id": "wed_01HX...",
"couple_first_names": ["Alex", "Jordan"],
"wedding_date": "2026-09-12",
"venue": "Bayfront Estate",
"status": "active",
"lead_source": "referral",
"assigned_planner_id": "tm_01HW...",
"created_at": "2025-11-04T14:21:33Z"
}
]
}
GET /api/weddings/{id}
Returns the full wedding record, with embedded counts for vendors, guests, tasks, and budget lines.
Response (200 OK)
{
"wedding": {
"id": "wed_01HX...",
"couple_first_names": ["Alex", "Jordan"],
"couple_last_names": ["Lee", "Mendez"],
"wedding_date": "2026-09-12",
"venue": "Bayfront Estate",
"status": "active",
"lead_source": "referral",
"assigned_planner_id": "tm_01HW...",
"vendor_count": 12,
"guest_count": 124,
"task_count": 38,
"tasks_overdue": 2,
"budget_total_cents": 4250000,
"budget_paid_cents": 1700000,
"created_at": "2025-11-04T14:21:33Z",
"updated_at": "2026-04-30T08:15:02Z"
}
}
POST /api/weddings
Create a new wedding.
Request body
{
"couple_first_names": ["Alex", "Jordan"],
"couple_last_names": ["Lee", "Mendez"],
"wedding_date": "2026-09-12",
"venue": "Bayfront Estate",
"lead_source": "referral",
"assigned_planner_id": "tm_01HW..."
}
Response (201 Created) — the created wedding record.
Vendors
GET /api/vendors
Lists every vendor in the studio-wide database.
Response (200 OK)
{
"vendors": [
{
"id": "vnd_01HX...",
"name": "Lila's Florals",
"category": "florist",
"contact_email": "lila@lilasflorals.com",
"contact_phone": null,
"preferred": true,
"do_not_use": false,
"do_not_use_reason": null,
"weddings_used_count": 14
}
]
}
POST /api/vendors
Create a new vendor.
Request body
{
"name": "Lila's Florals",
"category": "florist",
"contact_email": "lila@lilasflorals.com",
"contact_phone": null,
"notes": "Best for fall palettes; bills net-30."
}
Response (201 Created) — the created vendor record.
Tasks
POST /api/tasks
Create a per-wedding task.
Request body
{
"wedding_id": "wed_01HX...",
"title": "Confirm catering count",
"due_date": "2026-08-29",
"owner_team_member_id": "tm_01HW...",
"status": "pending"
}
status is one of pending, in_progress, completed, skipped. Defaults to pending on create.
Response (201 Created) — the created task.
GET /api/weddings/{id}/tasks
Lists every task for a given wedding.
Response (200 OK)
{
"tasks": [
{
"id": "tsk_01HX...",
"wedding_id": "wed_01HX...",
"title": "Confirm catering count",
"due_date": "2026-08-29",
"status": "pending",
"owner_team_member_id": "tm_01HW...",
"completed_at": null
}
]
}
Payments
POST /api/payments
Record a payment received.
Request body
{
"wedding_id": "wed_01HX...",
"amount_cents": 250000,
"received_at": "2026-05-01",
"method": "ach",
"payer": "Lee/Mendez",
"allocations": [
{ "budget_line_id": "bgl_01HX...", "amount_cents": 200000 },
{ "budget_line_id": "bgl_01HY...", "amount_cents": 50000 }
]
}
The allocations array splits the payment across budget lines. The sum of allocations[*].amount_cents must equal amount_cents. Atelier maintains a paid_amount cache on each budget line that tracks the sum of allocations against that line — see the user guide § Budget for the bidirectional contract.
Response (201 Created) — the created payment plus a refreshed snapshot of the affected budget lines so the caller can update its UI without re-fetching.
{
"payment": { ... },
"updated_budget_lines": [ ... ]
}
Reports
GET /api/reports/revenue
Pre-aggregated revenue report.
Query parameters
from— ISO date; report startto— ISO date; report endbucket—month(default),quarter, oryear
Response (200 OK)
{
"report": {
"from": "2026-01-01",
"to": "2026-12-31",
"bucket": "month",
"buckets": [
{ "label": "2026-01", "revenue_cents": 0 },
{ "label": "2026-02", "revenue_cents": 250000 },
{ "label": "2026-03", "revenue_cents": 425000 }
],
"total_cents": 675000
}
}
Calendar
GET /api/calendar/events
Lists every event across every wedding within a date range, suitable for rendering a calendar view.
Query parameters
from— ISO date (required)to— ISO date (required)
Response (200 OK)
{
"events": [
{
"id": "evt_01HX...",
"wedding_id": "wed_01HX...",
"wedding_label": "Lee/Mendez · Sep 12",
"title": "Ceremony",
"starts_at": "2026-09-12T17:00:00-04:00",
"duration_minutes": 30,
"location": "Bayfront Estate — Garden Lawn",
"category": "ceremony"
}
]
}
The wedding_label is pre-formatted for display in the calendar's wedding-color legend; clients should not parse it.
Guests CSV import
The Guests tab's bulk-import flow exposes two POST endpoints. They share a service layer with the Tauri command surface, so a CLI tool can drive the same import the UI does.
POST /api/weddings/{wedding_id}/guests/import-csv
Accepts a pre-parsed array of normalized rows and persists them. The UI does the column-mapping step client-side and posts the mapped result; a script can do the same.
Body
{
"rows": [
{
"first_name": "Alex",
"last_name": "Mendez",
"email": "alex@example.com",
"rsvp_status": "yes",
"dietary_notes": "vegetarian",
"group_name": "Lee family"
}
]
}
Response (200 OK)
{
"import": {
"inserted_guest_ids": ["gst_01HX..."],
"inserted_group_ids": ["grp_01HX..."],
"row_count": 1
}
}
Group rows referenced by group_name are created on the fly when no matching group exists for the wedding.
POST /api/weddings/{wedding_id}/guests/undo-csv-import
Reverses a prior import. The body is the inserted_guest_ids and inserted_group_ids from the response above.
Body
{
"guest_ids": ["gst_01HX..."],
"group_ids": ["grp_01HX..."]
}
Response (200 OK) is an empty body. The undo is best-effort: rows already mutated since the import remain.
PDF payloads
Atelier's print views (Timeline, Seating, Contract) render from a Rust-built payload that bundles the header, body data, and computed labels in one shape. The same payload is exposed over REST so external tools can produce parallel renderings (third-party PDF templates, custom branded variants, etc.) without re-implementing the resolution logic.
GET /api/weddings/{wedding_id}/pdf-payloads/timeline
Returns the bundled run-of-show shape for the Timeline print view.
Response (200 OK)
{
"payload": {
"header": {
"couple_label": "Lee / Mendez",
"wedding_date": "2026-09-12",
"venue_label": "Bayfront Estate"
},
"events": [
{
"id": "evt_01HX...",
"starts_at": "2026-09-12T08:00:00-04:00",
"duration_minutes": 60,
"title": "Hair and makeup",
"location": "Bridal suite",
"vendor_labels": ["Maria's Beauty"],
"notes": "Bride first, then bridesmaids"
}
]
}
}
GET /api/weddings/{wedding_id}/pdf-payloads/seating
Returns the bundled seating chart shape: header, per-table guest buckets, the unassigned list, capacity totals, and any overflow IDs (guests assigned to a table beyond its capacity).
GET /api/contracts/{contract_id}/pdf-payload
Returns the bundled contract shape: header, full contract row, and an alphabetized vendor label list.
Licensing scheduler
GET /api/licensing/heartbeat-scheduler-status
Returns the current state of the daily heartbeat scheduler. Useful for monitoring scripts and for diagnosing why a heartbeat hasn't fired.
Response (200 OK)
{
"running": true,
"last_tick_at": "2026-05-10T14:02:00Z",
"last_outcome": "ok",
"next_tick_at": "2026-05-11T14:02:00Z",
"consecutive_failures": 0
}
last_outcome is one of ok, ok_with_grace_warning, license_revoked, license_refunded, unreachable, deactivated, not_found, or unexpected_response. If the scheduler has not yet completed its first tick, the value is null and last_tick_at is also null. If the scheduler is not running (no activation stored yet, or shutdown in progress), the endpoint returns 503 Service Unavailable.
Online activation
The five endpoints below pair 1:1 with the same-named Tauri commands in commands/activation.rs. Each pair shares one service_* function, so the localhost API and the desktop UI surface identical behavior. Authoritative server responses (slot full, revoked, refunded, license not found, unreachable) flow as 200 OK with a structured { "kind": "...", ... } payload so callers branch on the kind discriminant rather than on HTTP status; only request-validation failures use 400 Bad Request.
POST /api/licensing/activate-online
Activates a license against the activation server and persists the slot locally on success. Verifies the license cryptographically against the embedded public key before the network call so a malformed key short-circuits without round-tripping.
Body
{
"licenseString": "ATLR-...",
"deviceLabel": "Atelier on Studio Desktop"
}
deviceLabel is optional; when omitted, the server-side label defaults to the Windows hostname (COMPUTERNAME), with a final fallback of "Atelier device".
Response (200 OK)
A discriminated union via kind:
{ "kind": "granted", "activation_id": "...", "slot_count": 1, "max_slots": 3, "first_activation": true }
Other kind values: slot_full (carries max_slots + active_devices: PublicSlot[]), license_refunded, license_revoked (carries mode, revoked_at), license_not_found, unreachable (carries message), unexpected_response (carries status, body).
Errors (400 Bad Request)
validation_failedwithfield_errors: { licenseString: "..." }when the license fails signature, format, or major-version checks.- Plain
errormessage whenlicenseStringis missing or empty, or when the hardware fingerprint can't be computed.
POST /api/licensing/heartbeat-now
Manually triggers a heartbeat against the activation server. The daily scheduler covers automatic heartbeats; this endpoint exists for the Settings → License "Reconnect now" button and for integration scripts that want to force a check.
Body: none.
Response (200 OK)
A discriminated union via kind:
{ "kind": "ok", "heartbeat_at": "2026-05-10T14:00:00Z" }
Other kind values: ok_with_grace_warning, deactivated, license_refunded, license_revoked, license_not_found, unreachable, unexpected_response. Each carries the fields appropriate to the outcome (e.g. revoked_at, grace_remaining_days, deactivated_reason).
Errors
503 Service Unavailablewhen no activation row exists (fresh install, post-deactivation, or pre-activation grace).
GET /api/licensing/activation-status
Returns the local cached activation row that the scheduler and lockdown UI read.
Response (200 OK)
{
"activation_id": "...",
"lid": "...",
"first_activated_at": "2026-05-01T18:30:00Z",
"last_heartbeat_at": "2026-05-10T08:00:00Z",
"first_launch_grace_started_at": null,
"revocation_state": "none",
"revoked_at": null,
"grace_remaining_days": null
}
Returns null (not an error) when no activation row exists. revocation_state is one of none, grace_14d, immediate, locked, or refunded.
POST /api/licensing/deactivate-this-device
Self-eviction. Posts to the activation server to release the current install's slot, then clears the local activation + license rows so the next launch returns to License Entry.
Body: none.
Response (200 OK)
{ "kind": "ok" }
Other kind values: forbidden, not_found (treated the same as ok for local cleanup), unreachable, unexpected_response.
Errors
503 Service Unavailablewhen no activation row exists.
POST /api/licensing/deactivate-other-device
Slot-full picker eviction. Used to free a slot held by a different device when activation returns slot_full and the user picks one from the list to kick. Does not touch local state; the caller follows up with activate-online.
Body
{
"licenseString": "ATLR-...",
"activationId": "..."
}
licenseString is verified cryptographically against the embedded public key before the network call so a hostile caller can't drive this surface against an arbitrary license.
Response (200 OK)
Same DeactivateOutcome discriminator as deactivate-this-device.
Errors (400 Bad Request)
validation_failedwithfield_errors: { licenseString: "..." }on signature/format/version failure.- Plain
errorwhenlicenseStringoractivationIdis empty.
Phone-friendly day-of view (LAN listener)
When the local API is enabled, Atelier also runs a second axum listener on 0.0.0.0:{api_port + 1} (default 7424) so a phone on the same WiFi network can pull up a wedding's day-of board. This listener is intentionally narrow: only two endpoints are exposed, and everything else returns 404. The full API stays on 127.0.0.1:7423 and is unreachable from the LAN.
Both listeners share the same Bearer token. The phone view accepts the token either via the Authorization: Bearer ... header or as a ?token=... query parameter, since QR codes carry URLs rather than headers.
GET http://{lan-ip}:{api_port + 1}/weddings/{wedding_id}/dayof
Server-rendered HTML page (not JSON). Phone-optimized layout with the timeline events, vendor list with tel: links, recent incidents, and an inline incident-log form. Auto-refreshes every 30 seconds via meta refresh.
The HTML inlines all CSS and has no external dependencies; it works on any reasonably modern phone browser without internet access.
GET /weddings/wd_01HQ8.../dayof?token=atlr_your_api_key_here
In Atelier, click Open on phone in day-of mode to display a QR code containing exactly this URL.
POST http://{lan-ip}:{api_port + 1}/weddings/{wedding_id}/incidents
Form-encoded POST that creates an incident from the phone's incident-log form. Body fields:
severity:"minor","major", or"critical"summary: required, the incident descriptionresolution: optional notes on how it was handledtoken: the Bearer token (same as the URL query param)
On success, redirects (303) back to the day-of HTML view so the form submission round-trips without breaking the phone-friendly UX.
What's NOT on the LAN listener
- Mutating any resource other than the incident log
- Reading or writing any other wedding's data
- Any endpoint from the main API (
/api/...) - Any admin or licensing surface
If you try to reach http://{lan-ip}:7424/api/weddings, you get a 404. The LAN listener has no such route.
Errors and rate limits
Common status codes:
400 Bad Request— validation error. Body includeserrorandfield.401 Unauthorized— missing or wrong Bearer token.404 Not Found— resource doesn't exist.409 Conflict— write would violate a uniqueness constraint (e.g. creating a vendor with a name that already exists).500 Internal Server Error— unexpected. Worth reporting to legal@dunamisstudios.net with the request that triggered it.
There is no rate limit on the local API — you're calling your own machine. Be reasonable about pollers (5-minute intervals are plenty; 5-second intervals are wasteful).
What's not in this curated reference
- The full catalog of
PUT/PATCH/DELETEendpoints for every resource. They exist; the in-app/api/docsis authoritative. - The full per-resource field list including timestamps, soft-delete flags, and audit columns. The in-app docs include them; we trim the reference here for readability.
- The webhook subscription endpoints. Atelier does not ship outbound webhooks in v1 — see integration examples for the polling-based workaround.
For a complete, autoritative-as-of-the-running-build reference, open http://127.0.0.1:7423/api/docs in any browser while the local API is enabled.