API
The web app, the CLI and any integration use the same versioned REST API.
Base URL: https://work.betterthangood.xyz/api/v1
Authentication
Create a personal access token at Settings → API tokens, owner-only and send it as a bearer token:
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
| Field | Notes |
|---|---|
| Name | A label, for example “Deploy script” |
| Scopes | read, write or both |
| Expires | Defaults 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
| Scope | Allows |
|---|---|
read | Everything the issuing user can see |
write | Everything the issuing user can change. Includes read |
Revoking
Tokens are revoked rather than deleted, so audit log entries keep a valid reference. A revoked token stops working immediately.
Resources
| Resource | Endpoints |
|---|---|
| Contacts | GET/POST /contacts · GET/PATCH/DELETE /contacts/{id} |
| Companies | GET/POST /companies · GET/PATCH/DELETE /companies/{id} |
| Deals | GET/POST /deals · GET/PATCH/DELETE /deals/{id} |
| Projects | GET/POST /projects · GET/PATCH/DELETE /projects/{id} |
| Tasks | GET/POST /tasks · GET/PATCH/DELETE /tasks/{id} |
| Invoices | GET/POST /invoices · GET /invoices/{id} |
| Invoice actions | POST /invoices/{id}/send · POST /invoices/{id}/payments |
| Payments | GET /payments · GET /payments/{id} |
Invoices have no update or delete endpoint. Sent invoices are immutable, and drafts are edited in the app.
Payments are created against an invoice at POST /invoices/{id}/payments.
Task access follows roles: owners can list and assign any task; members see and change only their own, and a task created by a member 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.
Creating an invoice
POST /api/v1/invoices
{ "invoice": {
"company_id": 7, "contact_id": 12,
"issue_on": "2026-08-27", "due_on": "2026-09-26",
"lines": [
{ "description": "Design retainer", "quantity": "1", "unit_price": "1200.50"
}
] } }
lines is required. An invalid line rolls back the whole request.
Line unit_price is submitted in dollars ("1200.50"). Amounts in responses are in cents.
POST /invoices/{id}/sendemails the client and freezes the figures. It returns 422 if the invoice is already sent, has no line items or has no recipient.POST /invoices/{id}/paymentstakesamountin dollars,paid_onandreference. It is refused on drafts.
Both return the full invoice.
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." } }
| Status | Meaning |
|---|---|
| 401 | Token missing, unknown, revoked or expired, or its user has left the account |
| 402 | The account's subscription is not active |
| 403 | The token lacks the required scope |
| 404 | No such record in this account, or no such route |
| 422 | The save failed. details names the fields |
| 429 | Rate 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.