Skip to content

Tenant API v1 — Contacts

Base path: /api/tenant/v1

Middleware: auth:tenant-api, tenant.user, verified, module:contacts, plus permission middleware / policies.

Assignee scoping: without contacts.assign (and not superadmin), list/stats/view/update only include contacts where assigned_to is the current user.

Stats

GET /contacts/stats

Same filters as list (minus pagination/sort). Payload includes:

total_contacts, my_contacts, unassigned, with_email, created_this_week, on_boarded, off_boarded, scope (org|mine).

Contacts CRUD

GET /contacts

Query: search, company, company_id, lifecycle_status (on_boarded | off_boarded), assigned_to (unassigned or user id), my_contacts, trashed, sort, direction, page, per_page.

List items include lifecycle_status, latest_note — most recent note (id, body, author, timestamps) or null. May include company_id and linked_company when the relationship is loaded.

POST /contacts

Body: name (required), email, phone, company (legacy free-text), company_id (optional FK to Companies), job_title, source, lifecycle_status (default on_boarded), assigned_to.

When company_id is set and the company exists, the legacy company string is synced to that Company’s name.

GET /contacts/{id}

Includes assignee, creator, notes, activities. May include company_id and linked_company (id, uuid, name) when the relationship is loaded. Embedded notes and activities are newest-first (created_at DESC, then id DESC).

PUT /contacts/{id}

Partial update of contact fields (including assigned_to, company, company_id).

DELETE /contacts/{id}

Soft delete. Permission: contacts.delete.

POST /contacts/{id}/restore

Restore a soft-deleted contact. Permission: contacts.restore.

DELETE /contacts/{id}/force

Permanently delete a soft-deleted contact (must already be trashed). Permission: contacts.force.delete (owner by default).

Actions

POST /contacts/{id}/assign

{ "assigned_to": number|null }

POST /contacts/{id}/notes

{ "body": string }

GET /contacts/{id}/timeline

Contact activity timeline entries.

Billing summary & statement

Requires Contacts view. Invoice/payment/credit-note lines appear only when those modules are entitled.

GET /contacts/{id}/billing-summary

Returns { currencies: [{ currency, invoice_count, total_invoiced, total_paid, balance_due }] } for non-draft, non-cancelled invoices. Empty currencies when Invoices is not entitled or there is no data.

GET /contacts/{id}/statement

Query: from, to (optional YYYY-MM-DD; defaults cover a sensible workspace range). Chronological lines (invoice | payment | credit_note) with date, number, description, amount, currency, plus period totals, opening_balance (outstanding before from), and balance_due (closing as of to) per currency. Credit notes appear only when applied (draft/issued/refunded/void excluded), matching aged receivables treatment.

GET /contacts/{id}/statement.pdf

Same query as statement; returns a branded PDF download.

Lead conversion

POST /leads/{lead}/convert (see tenant-v1-leads.md) creates a Contact and sets contact_id / contact on the returned lead when the contacts module is entitled for the workspace. conversion_meta.stub is false in that case; it is true when Contacts is not installed (status-only conversion).

Official documentation for the EloSync SaaS Platform.