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, or REJECTED |
| 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_relations 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_relations, the response also includes the related client_account and provider_client_account objects.
Error Responses
| Status Code | Description |
|---|---|
| 400 | Invalid request parameters, invalid contract dates, or invalid service details |
| 403 | User not authorized to access or modify contract |
| 404 | Contract not found |
Creating Contracts
Providers can create contracts with existing client accounts. New contracts are created with PENDING approval status and require approval from the client account.
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
Who Can Approve/Reject
Only users with role_id 3 on the client account can approve or reject contracts. This ensures that only authorized client account administrators 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",
"is_active": true
}
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_statusisAPPROVED- Current date is on or after
start_date(if specified) - Current date is on or before
end_date(if specified)
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 role-3 user 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:APPROVEDwhen the customer client account has no active owner users (sole-stewardship case), otherwisePENDING.
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 users with role_id 3 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 role-3 user on the customer client account, can terminate. - Core relationships (provider/client accounts) cannot be changed after creation.
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 existing client accounts require approval from client account users with role_id 3.
- Contracts created during client account creation are automatically approved.
- Only users with access to the provider client account can update contract details.
- Only users with role_id 3 on the client account can approve/reject contracts.
- 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.
- Client account relationships can be included by specifying them in the with_relations parameter.
- 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 customer client account has no active owner users (sole-stewardship case — the accountant is the only one running the account) and PENDING otherwise — matching 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 (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.