Skip to content

Tenant API v1 — Contracts ​

Base path: /api/tenant/v1

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

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

Stats ​

GET /contracts/stats ​

Same filters as list (minus pagination/sort). Counts include sent alongside draft, active, expired, and terminated.

Contracts CRUD ​

GET /contracts ​

Query: search, status, opportunity_id, assigned_to (unassigned or user id), my_contracts, trashed, sort, direction, page, per_page.

List items include status, opportunity, quotation (when linked), invoice_count, assignee/creator refs, latest_note, plus acceptance fields when present: has_acceptance_link, acceptance_link_expires_at, accepted_at, accepted_by_name, accepted_by_email, accepted_by_phone, acceptance_ip, acceptance_signature_method, has_acceptance_signature, has_acceptance_id_document, acceptance_id_document_type, acceptance_id_document_original_name (file paths are never exposed).

POST /contracts ​

Body: opportunity_id (required), quotation_id (optional — only valid when the Quotations module is entitled, the quotation is not soft-deleted, the actor may access the quotation, and the quotation belongs to the same opportunity; see LinkableQuotation), title (required), party_name, start_date (required), end_date, value, currency (3-letter, default USD), description (HTML memo, sanitized server-side, max 50000), notes (HTML memo, sanitized server-side, max 50000), assigned_to.

GET /contracts/{id} ​

Includes opportunity, quotation (when linked), related invoices (id/number/status), quotation_already_invoiced (true when the linked quotation already has any invoice), assignee, creator, description / notes HTML memos, comments (contract_notes), timeline activities, and acceptance metadata (same fields as list, including evidence flags). Embedded contract_notes and timeline/domain activities are newest-first (created_at DESC, then id DESC).

GET /contracts/{id}/acceptance-signature ​

Permission: contracts.view (assignee-scoped). Streams the stored signature image when has_acceptance_signature is true; otherwise 404. Records acceptance_evidence_downloaded on the timeline.

GET /contracts/{id}/acceptance-id-document ​

Permission: contracts.view (assignee-scoped). Streams the uploaded ID/passport file when has_acceptance_id_document is true; otherwise 404. Records acceptance_evidence_downloaded on the timeline.

GET /contracts/{id}/pdf ​

Permission: contracts.view (assignee-scoped). Extra limiter throttle:contracts-pdf. Returns application/pdf attachment. Branded layout matches other sales documents (logo, button color, company profile). Includes party, dates, value, sanitized description/notes HTML, and — when accepted — signer metadata plus the signature image (ID files are not embedded).

PUT /contracts/{id} ​

Partial update of draft contracts only. Non-draft contracts return 422 on status (Only draft contracts can be edited.). Assignment after send/activate uses POST /contracts/{id}/assign. Status changes use POST /contracts/{id}/status (or dedicated send/accept actions).

DELETE /contracts/{id} ​

Soft delete. Permission: contracts.delete. Keeps acceptance evidence files so restore can recover them.

POST /contracts/{id}/restore ​

Permission: contracts.restore.

DELETE /contracts/{id}/force ​

Permanently delete a soft-deleted contract. Permission: contracts.force.delete. Purges signature and ID evidence objects from storage.

Actions ​

POST /contracts/{id}/assign ​

{ "assigned_to": number|null }

Permission: contracts.assign.

POST /contracts/{id}/send ​

Transitions draft → sent. Permission: contracts.send (assignee-scoped unless the actor has contracts.assign or is superadmin). Status-only — does not email or generate a PDF. Issues a hashed customer acceptance token (plaintext returned only from POST …/acceptance-link or embedded in email). Records signature_requested.

POST /contracts/{id}/email ​

{ "to"?: string[], "cc"?: string[], "bcc"?: string[], "subject": string, "message": string, "attach_pdf"?: boolean }

Permission: contracts.send (assignee-scoped unless the actor has contracts.assign or is superadmin). Throttle: billing-document-email (10/min per user).

Requires the contract to already be sent — draft returns 422 on status. Allowed for sent, active, expired, and terminated.

When to is omitted, resolves the recipient from the linked opportunity’s contact email, then company email. If no address is found, returns 422 on to.

Queues a branded email via the tenant mailer (optional PDF attachment from ContractPdfService). For Sent contracts, rotates the acceptance token and appends the accept URL to the message body. Records an emailed timeline entry and a tenant email log row (notification_type: contract.emailed).

POST /contracts/{id}/acceptance-link ​

Permission: contracts.send (assignee-scoped). Requires status sent. Rotates the acceptance token and returns { url, expires_at, has_acceptance_link, acceptance_link_expires_at }.

POST /contracts/{id}/accept ​

Transitions sent → active. Permission: contracts.accept (assignee-scoped unless the actor has contracts.assign or is superadmin). Clears any outstanding acceptance token.

Public customer accept (unauthenticated) ​

Requires tenancy (domain / X-Tenant-Domain) and Contracts entitlement. Throttle: contract-acceptance (20/min per IP).

  • GET /public/contracts/accept/{token} — summary (title, party, dates, value, workspace name, description, expiry, acceptance_requirements: { phone, signature_image, id_document }). Invalid/expired/used → 404.
  • POST /public/contracts/accept/{token} — multipart or JSON. Always: accepted_by_name, accepted_by_email. When requirements demand: accepted_by_phone; signature file + signature_method (drawn|uploaded); id_document file + id_document_type (national_id|passport|other). Marks active, stores signer + IP (+ user agent / evidence files), invalidates token, records signed activity. Uploads honor workspace Storage quota and upload policy. Enabling signature/ID Settings requires Storage entitlement.

POST /contracts/{id}/status ​

{ "status": "draft"|"sent"|"active"|"expired"|"terminated" }

Permission: contracts.update. Rejects disallowed transitions (including re-entering the same status) with a 422 validation error on status. Records a status_changed timeline entry. Prefer dedicated POST …/send and POST …/accept for send/activate-from-sent (correct permission gates). Draft → active supports Activate without signature. Expire/terminate clear outstanding acceptance links.

Allowed transitions:

  • draft → sent | active | terminated
  • sent → active | expired | terminated
  • active → expired | terminated

POST /contracts/{id}/convert ​

Creates a draft CustomerInvoice from an active contract (repeatable — progress billing):

  • Requires the Invoices module to be entitled (soft, call-time check — no module_dependencies row). Returns 422 if Invoices is not installed.
  • Copies linked quotation lines when present; otherwise a single line from value. Rejects if there is no value and no quotation lines.
  • Sets customer_invoices.contract_id and copies quotation_id when the contract is linked to a quotation
  • Does not hard-block when the quotation is already invoiced — the confirm UI warns; the API allows another invoice
  • Second and later invoices for the same contract require { "acknowledge_repeat_billing": true } (422 on acknowledge_repeat_billing otherwise). The SPA confirm dialog always sends this flag.
  • Records a converted activity (Created invoice {number})

Permission: contracts.convert (assignee-scoped unless the actor has contracts.assign or is superadmin). Rejects with a 422 if the contract is not active. Returns the created invoice (CustomerInvoiceResource) with HTTP 201.

POST /contracts/{id}/notes ​

{ "body": string }

Permission: contracts.update.

GET /contracts/{id}/timeline ​

Domain timeline entries (created, updated, assigned, status_changed, converted, note_added, deleted, restored, emailed, signature_requested, signed).

Official documentation for the EloSync SaaS Platform.