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, 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:

  1. approval_status is APPROVED
  2. Current date is on or after start_date (if specified)
  3. 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, and approved_at are server-controlled and ignored if sent in the request body.
  • The server decides the initial approval_status: APPROVED when the customer client account has no active owner users (sole-stewardship case), otherwise PENDING.

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_date set, no approval_status change): 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:

  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 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-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.