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:

  1. Direct Access: Regular customers have direct access to their own client account through ClientAccountUser associations
  2. 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:

  1. approval_status is APPROVED
  2. start_date is set and the current date is on or after it — a contract with no start_date has never entered its service period and is never active, no matter its approval status
  3. 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, and approved_at are server-controlled and ignored if sent in the request body.
  • The server decides the initial approval_status. It is APPROVED in 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 PENDING or APPROVED contract with it (sole stewardship).
  • In every other case the contract is created PENDING with pending_since set.

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_date set, no approval_status change): 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 403 with client_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_active reflects 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_date is never is_active: true, regardless of approval_status. If start_date is not provided when a contract becomes APPROVED, the server sets it to the approval date.
  • Client account relationships can be included by specifying them in the with 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:

  1. Resolve or create the customer’s client account (from client_account_id or organization_id).
  2. Create the service contract between the provider firm and the customer.
  3. 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-Language header — 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_provided value must be a valid ServiceTypes enum 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.