REST API

Call every Continual operation over HTTP: authentication, routes, Actions, paging, errors, and changes.

Every operation your organization can use is an HTTP route under /api/v1 on your Continual address. This page covers what every organization's API shares. The examples keep your address in CONTINUAL_URL and an API key in CONTINUAL_API_KEY.

Your organization's API

Your organization's API depends on the apps it has installed, including any apps of its own. Two places describe it exactly:

  • GET /api/openapi returns an OpenAPI document. With your API key, it describes your organization's model. Without credentials, it describes only Continual's apps.
  • In the web app, Settings › Domain model shows your organization's record types, their fields, and their operations.
Shell
curl "$CONTINUAL_URL/api/openapi" -H "Authorization: Bearer $CONTINUAL_API_KEY"

The standard record types are listed in Standard record types.

Authentication

A request authenticates in one of two ways.

CredentialHow to send itWho it is
API keyAuthorization: Bearer cos_…A service account of one organization
Login sessionThe company_session cookieA person signed in to the web app

Use an API key for code and agents.

  1. Create a service account

    An administrator opens Settings › Service accounts and adds one. They name it, choose its kind, Integration or Agent, and give it a role. Adding it issues its first key.

  2. Copy the key

    The key starts with cos_. It is shown once, and Continual stores only its hash. Keep it secret.

  3. Revoke the key when you no longer need it

    On the same page, administrators can issue more keys for an account and revoke any of them. Anything that uses a revoked key loses access at once.

An administrator can do the same through the API, with serviceAccounts.create and serviceAccounts.issueKey. A key may have an expiry.

Choose the organization

Each request works in one organization.

  • An API key works only in its own organization, so its requests need no organization header.
  • A signed-in person names the organization with the x-company-organization header.
  • A change made with a session cookie must come from the web app's own origin.

To check who a credential is, call me. It returns the caller, its kind, its organization, its permissions there, and the installed apps.

Shell
curl "$CONTINUAL_URL/api/v1/me" -H "Authorization: Bearer $CONTINUAL_API_KEY"

Act for a person

An agent-kind service account can name a person it acts for with the x-company-on-behalf-of header, set to that person's user ID. The person must be an active member of the account's organization. The request then reaches exactly what that person may reach, and its changes record both.

Only an account that holds a system role allowing platform.members.actAs gets any access this way, such as Continual's agent.delegate or an app endpoint's platform.appDelegate. An agent account an admin creates holds no such role, so a request it sends with x-company-on-behalf-of gets no grants.

An agent can also name the record its writes are for, such as its session, with x-company-cause.

Routes

Routes follow the operation IDs in the OpenAPI document. A record operation takes the record's ID in the path. Where an Object has handles, such as INV-1042, a handle works in place of the ID.

OperationRoute
companies.listGET /api/v1/companies
companies.getGET /api/v1/companies/{id}
companies.createPOST /api/v1/companies
companies.updatePATCH /api/v1/companies/{id}
companies.deleteDELETE /api/v1/companies/{id}
finance.invoices.issuePOST /api/v1/finance.invoices/{id}:issue
records.searchPOST /api/v1/records:search
records.activityPOST /api/v1/records/{id}:activity
meGET /api/v1/me

Request and response bodies are JSON.

Actions

Create a company:

Continual sets the record's organization to the organization the request works in. The field is output only, so you never send it.

Every Action takes two optional fields beside its input:

  • requestId: an ID you choose for the write. A retry with the same requestId and input returns the first result and does not write again.
  • validateOnly: run every check and return the result the Action would produce, without committing.

A record Action can also name the revision it read. If the record has changed since, the Action fails with ABORTED, so you never overwrite a change you did not see.

Every Action answers with the same shape:

FieldWhat it holds
resultThe Action's result, such as the record it created
recordsEach record the transaction created or changed that you can read
deletedThe IDs of the records it deleted
positionThe commit log position of the commit; null when nothing changed

Issue a draft invoice by its handle:

To run several Actions in one transaction, use records.run (POST /api/v1/records:run).

Lists, filters, and paging

Lists, search, revisions, and activity page the same way:

  • Send pageSize, at most 100. With none or zero, you get 50.
  • Send the previous response's nextPageToken as pageToken to get the next page.
  • The response holds items and nextPageToken. Lists also return totalSize.

A list's filter and sort, and an aggregate's filter and sum, travel as JSON in their query parameters. A field filter names a field, an operator, and a value. Operators include eq, in, gt, lt, contains, startsWith, and isNull, and filters combine with and, or, and not.

Each Object's filter schema is in the OpenAPI document. A list also takes query, a text search across the Object's search fields and record names. Lists and searches return only the records the caller can reach.

Errors

Every error has the same body:

JSON
{
  "status": "FAILED_PRECONDITION",
  "reason": "APPROVAL_REQUIRED",
  "message": "…",
  "details": {
    "record": "…",
    "action": "finance.invoices.issue",
    "policy": "…"
  }
}

status is the canonical code, and it sets the HTTP status. reason is the stable cause to match in code. Validation errors list violations in details, each with the field's path.

StatusHTTPCommon causes
INVALID_ARGUMENT400The input or a business rule fails validation
FAILED_PRECONDITION400A document is final, or a policy holds or refuses the call
UNAUTHENTICATED401No credential, or one that does not verify
PERMISSION_DENIED403The caller's roles do not allow it, or it is not admitted
NOT_FOUND404No such record or route
ABORTED409The record moved past the revision you sent
ALREADY_EXISTS409A record already holds a unique value

When a policy holds a call, the reason is APPROVAL_REQUIRED. Ask a person with approvals.request, naming the held call. When a policy refuses a call, the reason is POLICY_DENIED. See Approvals.

Every response carries its request's ID in the x-request-id header. Include it when you report a problem.

Follow changes

GET /api/v1/events returns the events the caller can see, in commit order. An event is one record's part of a commit, named for what happened: finance.invoice.issued when an invoice is issued, or created, updated, or deleted after the record type for a plain write. The x-event of an operation in the OpenAPI document gives the name its Action makes. Events of one Action share their commit and position, and type keeps one kind, such as type=finance.invoice.issued. object keeps one record type's events, and with filter, a filter over that type's own fields as JSON, only those whose record matches it before or after, so you see a record arrive, change, and leave what the filter describes.

Each page returns a nextCursor. Send it back as cursor to continue, even after an empty page. Send cursor=now to start at the current head.