WORK Docs
Go to WORK →

API

The web app, the CLI and any integration use the same versioned REST API.

Settings, API tokens

Base URL: https://work.betterthangood.xyz/api/v1

Authentication

Create a personal access token at Settings → API tokens and send it as a bearer token. Admins see and can revoke every token in the account; members and contractors manage only their own:

curl https://work.betterthangood.xyz/api/v1/contacts \
  -H "Authorization: Bearer work_pat_…"

The header is the only accepted method. Tokens are not read from query parameters or cookies.

A token acts as the user who created it, on the account it was created in, with that user’s access.

Creating a token

FieldNotes
NameA label, for example “Deploy script”
Scopesread, write or both
ExpiresDefaults to 30 days out

The token is displayed once, at creation. WORK stores a hash of it and cannot display it again. Replace a lost token by revoking it and creating another.

Scopes

ScopeAllows
readEverything the issuing user can see
writeEverything the issuing user can change. Includes read

Reading is open on every plan. Writing needs Pro: a free account cannot issue a write token, and a token issued while the account was on Pro is refused with 402 once it is not (see Billing & plan).

Revoking

Tokens are revoked rather than deleted, so audit log entries keep a valid reference. A revoked token stops working immediately.

Resources

ResourceEndpoints
ContactsGET/POST /contacts · GET/PATCH/DELETE /contacts/{id}
CompaniesGET/POST /companies · GET/PATCH/DELETE /companies/{id}
DealsGET/POST /deals · GET/PATCH/DELETE /deals/{id}
ProjectsGET/POST /projects · GET/PATCH/DELETE /projects/{id}
TasksGET/POST /tasks · GET/PATCH/DELETE /tasks/{id}
InvoicesGET /invoices · GET /invoices/{id} · PUT /invoices/{source}/{external_id}
BillsGET /bills · GET /bills/{id}

Invoices are imported records of what was billed, so there is no create, update or delete endpoint. Bring one in with a CSV import, or sync it from the accounting system you keep your books in.

Syncing an invoice in

PUT /api/v1/invoices/xero/9c1
{ "invoice": { "number": 91, "status": "sent", "currency": "USD",
               "issue_on": "2026-01-02", "due_on": "2026-02-01",
               "subject": "January retainer",
               "customer": { "external_id": "cus_7", "name": "Widgets Intl" },
               "lines": [ { "description": "Design sprint", "quantity": 2,
                            "unit_price_cents": 150000 } ] } }

The invoice is addressed by the accounting system it was billed in — quickbooks, xero or freeagent — and the id that system knows it by, so sending it twice updates the same invoice rather than adding a second: 201 the first time, 200 after. Lines are replaced whole each time, and a body with no lines leaves the ones already there.

The customer is matched by its own external_id, never by name; one you have not billed before is created from the name you send. A synced invoice stays read-only in the web app, like an imported one, and carries source, external_id, external_url and synced_at when you read it back.

Invoices only. Bills are money you owe out, so you enter them in the web app or bring them in by CSV import — nothing syncs them.

Bills are also read-only through the API. A bill response includes its stored open or paid status, derived overdue display status, reference, project, and computed total. Creating and paying bills remain deliberate actions in the web app.

Task access follows roles: admins and members can list and assign any task; contractors see and change only their own, and a task created by a contractor is assigned to them.

Responses

Responses use one envelope. Lists return an array in the same data key.

{ "data": { "id": 42, "first_name": "Ada", "created_at": "2026-08-27T09:15:00Z" } }

Every record carries id, created_at and updated_at.

Money in responses is an integer number of cents, in value_cents and amount_cents fields, with a currency field alongside.

Reading an invoice

GET /api/v1/invoices/42
{ "data": {
    "id": 42, "number": 1001, "status": "sent", "display_status": "overdue",
    "currency": "USD", "issue_on": "2026-01-05", "due_on": "2026-02-04",
    "subject": "January retainer", "notes": "Thank you!", "sent_at": "2026-01-05T09:00:00Z",
    "total_cents": 385000, "company_id": 7, "contact_id": 12 } }

total_cents is computed from the invoice’s lines. display_status reflects an overdue sent invoice; status is the stored value.

Pagination

GET /api/v1/contacts?page=2&per_page=50

Default 25 per page, maximum 100. Values outside the range fall back to the default. There is no next-page marker; a page shorter than per_page is the last one.

Errors

{ "error": { "code": "forbidden", "message": "This token does not have the write scope." } }
StatusMeaning
401Token missing, unknown, revoked or expired, or its user has left the account
402The action needs the Pro plan — the account is on Free, or a token issued under Pro outlived it
403The token lacks the required scope
404No such record in this account, or no such route
422The save failed. details names the fields
429Rate limit exceeded

A record in another account returns 404 rather than 403, so that IDs cannot be tested for existence.

Rate limits

1,000 requests per hour per token, across all endpoints. Exceeding it returns 429.

Audit

API writes are recorded in the audit log with the token’s user and the request IP address.

Versioning

  • New fields are additive and may appear without notice. Clients should ignore unrecognized keys.
  • Breaking changes ship as a new version at /api/v2, alongside v1.
  • v1 would then be deprecated on a published timeline. Response shapes are not changed in place.

Not currently available

Webhooks and OAuth apps are planned. Integrations poll the API and authenticate with personal access tokens.

The specification

The OpenAPI specification is generated from the server’s routes and contract-tested against them. Use it to generate a client.

© 2026 BTG Space Exploration and Cattle LLC. All rights reserved.