Contracts
Contracts define service agreements between client accounts, where one account acts as a service provider (like an accounting firm) and another as a customer. Contracts are a key part of Snapbooks’ access control system:
- Direct Access: Regular customers have direct access to their own client account through ClientAccountUser associations
- Contract-Based Access: Service providers (like accountants and auditors) gain access to their clients’ accounts through contracts, without needing direct user associations
This dual access model enables:
- Customers to manage their own company directly
- Service providers to efficiently manage multiple client accounts
- Clear separation between direct users and external service providers
Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /contracts | List contracts |
| POST | /contracts | Create a new contract |
| PATCH | /contracts/{id} | Update a contract — approve/reject, terminate, or edit |
| POST | /client-engagements | Atomic onboarding — client account, contract, and optional owner invite in one call (see Client Engagements) |
There is no GET /contracts/{id} endpoint. To look up a single contract, list contracts filtered by client_account_id and pick the matching id.
Query Parameters
The GET /contracts endpoint supports the following query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_account_id | integer/string | No | Filter by customer client account ID(s). Multiple IDs can be separated by comma, semicolon, or space. If not provided and provider_client_account_id is omitted, returns contracts for all eligible client accounts |
| provider_client_account_id | integer | No | Filter to contracts where the caller’s firm is the provider. The caller must be a direct, active member of this firm; contract-derived access does not count. Use this to surface a provider firm’s outgoing PENDING requests |
| approval_status | string | No | Filter by approval status: PENDING, APPROVED, REJECTED, or EXPIRED |
| page | integer | No | Page number for pagination (default: 1) |
| per_page | integer | No | Number of items per page (default: 100) |
Attributes
| Attribute | Type | Description |
|---|---|---|
| id | integer | The unique identifier of the contract |
| created_at | datetime | When the contract was created |
| created_by_id | integer | The ID of the user who created the contract |
| client_account_id | integer | The ID of the customer client account |
| provider_client_account_id | integer | The ID of the provider client account |
| service_provided | string | Service type: ACCOUNTING, AUDITING, or TASK_CONTRIBUTION |
| start_date | date | Start date of the contract |
| end_date | date | End date of the contract. Set when the contract is terminated |
| approval_status | string | Contract approval status: PENDING, APPROVED, REJECTED, or EXPIRED |
| approved_by_id | integer | The ID of the user who approved/rejected the contract |
| approved_at | datetime | When the contract was approved/rejected |
| pending_since | datetime | When the contract entered PENDING (set on creation when approval is required) |
| terminated_by_id | integer | The ID of the user who terminated the contract (set by the termination flow) |
| terminated_at | datetime | When the contract was terminated |
| termination_reason | string | Optional reason captured when the contract was terminated |
| is_active | boolean | Whether the contract is currently active (computed based on approval and dates) |
Relationships
Relationships are opt-in and must be requested via the with query parameter.
| Relationship | Type | Description |
|---|---|---|
| client_account | ClientAccount | The customer client account |
| provider_client_account | ClientAccount | The provider client account |
Example Response
{
"id": 1,
"created_at": "2023-05-10T12:00:00Z",
"created_by_id": 1,
"client_account_id": 2,
"provider_client_account_id": 1,
"service_provided": "ACCOUNTING",
"start_date": "2023-01-01",
"end_date": null,
"approval_status": "APPROVED",
"approved_by_id": 9,
"approved_at": "2023-05-10T12:05:00Z",
"pending_since": null,
"terminated_by_id": null,
"terminated_at": null,
"termination_reason": null,
"is_active": true
}
When requested via with, the response also includes the related client_account and provider_client_account objects.
Error Responses
| Status Code | Description |
|---|---|
| 400 | Invalid request parameters, missing service_provided, unknown client or provider account, or provider and client are the same account |
| 403 | User not authorized to access or modify contract |
| 403 | client_account.contract_core_field_immutable — a PATCH tried to change client_account_id or provider_client_account_id |
| 404 | Contract not found |
Errors use the standard error envelope. Branch on code, not on message:
{
"error": {
"code": "client_account.contract_core_field_immutable",
"message": "Provider cannot be changed.",
"request_id": "a1b2c3d4-5678-90ab-cdef-1234567890ab"
}
}
Creating Contracts
Providers can create contracts with existing client accounts. The server decides the initial approval_status (see Authorization Rules); in most cases the contract starts PENDING and must be approved by the client account’s owner.
POST /contracts
{
"client_account_id": 789,
"provider_client_account_id": 123,
"service_provided": "ACCOUNTING",
"start_date": "2025-01-01"
}
Response:
{
"id": 456,
"client_account_id": 789,
"provider_client_account_id": 123,
"service_provided": "ACCOUNTING",
"start_date": "2025-01-01",
"approval_status": "PENDING",
"is_active": false,
"created_at": "2025-01-01T10:00:00Z",
"created_by_id": 123
}
Contract Approval Workflow
Approval Status Values
- PENDING: Contract awaits approval from client account (default for new contracts)
- APPROVED: Contract has been approved and is active (if within date range)
- REJECTED: Contract has been rejected and will never be active
- EXPIRED: A pending request that was never answered and has been given up on
Who Can Approve/Reject
Only active users with the Client Account Owner role (CA) on the client account can approve or reject contracts. This ensures that only the account’s owners can make approval decisions.
Approving a Contract
PATCH /contracts/456
{
"approval_status": "APPROVED"
}
Response:
{
"id": 456,
"approval_status": "APPROVED",
"approved_by_id": 999,
"approved_at": "2025-01-01T11:00:00Z",
"start_date": "2025-01-01",
"is_active": true
}
start_date here was null before approval and got stamped with today’s date because none was provided.
Rejecting a Contract
PATCH /contracts/456
{
"approval_status": "REJECTED"
}
Response:
{
"id": 456,
"approval_status": "REJECTED",
"approved_by_id": 999,
"approved_at": "2025-01-01T11:00:00Z",
"is_active": false
}
Contract Activity Rules
A contract is considered active (is_active: true) only when:
approval_statusisAPPROVEDstart_dateis set and the current date is on or after it — a contract with nostart_datehas never entered its service period and is never active, no matter its approval status- Current date is on or before
end_date(if specified)
If a contract becomes APPROVED (at creation via auto-approval, or via a PATCH that sets approval_status: APPROVED) without an explicit start_date, the server stamps start_date with the approval date.
Terminating a Contract
Either side can end a live contract by PATCH /contracts/{id} with an end_date (and no approval_status change). The server records terminated_by_id, terminated_at, and an optional termination_reason. Authorization: the caller must be either a direct, active member of the provider firm, or a Client Account Owner on the customer client account.
PATCH /contracts/456
{
"end_date": "2026-06-30",
"termination_reason": "Customer moved to in-house accounting"
}
Response:
{
"id": 456,
"approval_status": "APPROVED",
"end_date": "2026-06-30",
"terminated_by_id": 999,
"terminated_at": "2026-05-18T11:00:00Z",
"termination_reason": "Customer moved to in-house accounting",
"is_active": false
}
Authorization Rules
Contract Creation
- The requester must be a direct, active member of the provider firm (
accounting_client_account_id). Contract-derived access does not count (would be circular). - The provider and customer client accounts must be different.
approval_status,approved_by_id, andapproved_atare server-controlled and ignored if sent in the request body.- The server decides the initial
approval_status. It isAPPROVEDin two cases only:- the provider firm already has contract-granted access to the customer (for example, adding a second service type), or
- the customer client account has never had any users, active or deactivated, and no other provider firm has a
PENDINGorAPPROVEDcontract with it (sole stewardship).
- In every other case the contract is created
PENDINGwithpending_sinceset.
Contract Updates
- Regular updates (e.g.
start_date): provider-side only — caller must have access to the provider client account. - Approval / rejection (
approval_status): only Client Account Owners on the customer client account can approve or reject. - Termination (
end_dateset, noapproval_statuschange): either a direct active member of the provider firm, or a Client Account Owner on the customer client account, can terminate. - Core relationships (provider/client accounts) cannot be changed after creation; trying returns
403withclient_account.contract_core_field_immutable.
Auto-Approval Exception
Contracts created during client account creation (via POST /client-accounts with client_contracts) are automatically approved since the provider is creating both the account and contract.
Example Update Request
PATCH /contracts/456
{
"service_provided": "ACCOUNTING",
"start_date": "2023-01-01",
"end_date": "2024-12-31"
}
Notes
- New contracts on client accounts that are in use require approval from a Client Account Owner.
- Contracts created during client account creation are automatically approved.
- Only users with access to the provider client account can update contract details.
- Only Client Account Owners on the client account can approve/reject contracts.
- Provider access does not end with the service period: an approved contract keeps granting the provider firm access to the client’s books for 5 years after
end_date, so it can support audits.is_activereflects the service period only. - The provider and customer client accounts cannot be changed after contract creation.
- Client account IDs in query parameters must be from the user’s eligible client accounts.
- Multiple client account IDs can be provided in the query using various separators (comma, semicolon, space).
- All dates are returned in ISO 8601 format.
- The created_by_id is automatically set to the authenticated user’s ID.
- The approved_by_id and approved_at are automatically set when approval status changes.
- A contract with no
start_dateis neveris_active: true, regardless ofapproval_status. Ifstart_dateis not provided when a contract becomesAPPROVED, the server sets it to the approval date. - Client account relationships can be included by specifying them in the
withparameter. - Users from the provider account gain access to the customer account through approved contracts.
- Use the has_direct_role filter on /client-accounts to distinguish between direct and contract-based access.
Client Engagements
The client engagement endpoint is a higher-level onboarding flow for provider firms (accountants and auditors). A single request bundles three operations that would otherwise need to be sequenced manually:
- Resolve or create the customer’s client account (from
client_account_idororganization_id). - Create the service contract between the provider firm and the customer.
- Optionally send an invitation email so the customer’s owner can claim the account.
The contract is created APPROVED when the provider already has access to the customer, or when the customer client account has never had any users and no other provider firm is engaged (sole stewardship — the accountant is the only one running the account). Otherwise it is PENDING. This matches the behaviour of POST /contracts.
Endpoint
| Method | Endpoint | Description |
|---|---|---|
| POST | /client-engagements | Onboard a client in one atomic operation |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| provider_client_account_id | integer | Yes | The provider firm’s client account ID. The requester must be a direct active member of this account |
| client_account_id | integer | Conditional | Existing customer client account ID. Provide either this or organization_id, not both |
| organization_id | integer | Conditional | Organization ID to onboard. If no client account exists for this organization, one is created using the provider firm’s accounting_currency |
| service_provided | string | Yes | The service type. One of ACCOUNTING, AUDITING, TASK_CONTRIBUTION |
| invite_owner | boolean | No | Whether to send an invitation email to the customer’s owner. Default: false |
| owner_email | string | Conditional | Email address for the owner invitation. Required when invite_owner is true |
Example Request (existing client account, with owner invite)
POST /client-engagements
{
"provider_client_account_id": 23,
"client_account_id": 789,
"service_provided": "ACCOUNTING",
"invite_owner": true,
"owner_email": "owner@customer.no"
}
Example Request (onboard by organization number)
POST /client-engagements
{
"provider_client_account_id": 23,
"organization_id": 101,
"service_provided": "ACCOUNTING"
}
When organization_id is supplied and no client account exists for that organization, one is created with the organization’s name as the display name. The new client account inherits the provider firm’s accounting currency and is billed via the provider firm (or its billing_client_account_id if set).
Response
Returns 201 Created with a compact result envelope referencing the created (or resolved) entities:
{
"client_account_id": 789,
"contract_id": 456,
"contract_status": "PENDING",
"invitation_id": 12
}
| Field | Type | Description |
|---|---|---|
| client_account_id | integer | The customer client account ID (existing or newly created) |
| contract_id | integer | The created contract ID |
| contract_status | string | APPROVED (provider already has access, or sole stewardship) or PENDING (awaiting owner approval) |
| invitation_id | integer | null | The created invitation ID, or null when invite_owner is false |
Fetch the full contract via GET /contracts (filtered by client_account_id and the returned contract_id) and the invitation via GET /invitations/{id} if you need more detail.
Invitation Behaviour
When invite_owner is true:
- The invitation is created with the
CA(Client Account Owner) role. - Any pre-existing pending invitation for the same email on the same client account is cancelled first to keep one active invitation per email.
- If the contract is
PENDING, the invitation is linked to the contract — accepting it both creates the user’s client-account membership and approves the contract. - If the contract is
APPROVED(sole stewardship), the invitation is a stand-alone “claim your account” invite with no contract side effects. - The invitation email is dispatched after the database commit succeeds; the email language follows the request’s
Accept-Languageheader — Norwegian (nb,nn,no) sends the Norwegian template, anything else sends English.
Error Responses
| Status | Description |
|---|---|
| 400 | provider_client_account_id, service_provided, or owner_email (when invite_owner=true) missing |
| 400 | Both client_account_id and organization_id provided, or neither |
| 400 | Provider and client account are the same |
| 400 | An active or pending contract for the same service already exists |
| 403 | Requester is not a direct active member of the provider firm. Contract-derived access does not count |
| 404 | client_account_id, organization_id, or provider_client_account_id not found |
Notes
- The endpoint runs as a single transaction — if any step fails (validation, contract creation, invitation persistence), nothing is committed and no email is sent.
- Use this endpoint when you want one atomic call. To create the contract and invitation separately (e.g. invite the owner later), use POST /contracts plus POST /invitations instead.
- The
service_providedvalue must be a validServiceTypesenum value. See Contracts for the full list of supported services. - Only “direct” provider-firm members can call this endpoint. Contract-derived access (e.g. an accountant who reaches the provider firm through another contract) is rejected to prevent circular delegation.