Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: WorkWing Custom Fields API
description: 'REST API for WorkWing field service management. Designed as an **integration point** for CRM, ERP, and dispatch systems. Admin configuration (webhooks, roles, job types, integrations) is managed via the web dashboard. Every resource is documented under its own section in the sidebar. ## Authentication
Send your API key as a bearer token on every request:
```
Authorization: Bearer ww_sk_live_. ```
### Scopes
An API key carries a list of `action:resource` scopes. Every operation publishes the ones it requires as `x-required-scopes`, an array on the operation object; a key must hold all of them, or the wildcard scope `*`, and one that does not is refused with `403 E_AP_1102`. The two operations that declare `security: []` need no key at all: the health check, and the integration callback, which authenticates with the signed token in its own path. ## Querying collections
List endpoints (jobs, customers, resources, and so on) accept query parameters for filtering, sorting, field selection, and pagination:
- `?status=eq.T_PENDING` - filter by exact match
- `?priority=in.(T_HIGH,T_URGENT)` - match any of several values
- `?created_at=gte.2024-01-01` - range comparisons (`gt`, `gte`, `lt`, `lte`)
- `?select=id,status,customer:customers(name)` - choose columns and embed related records. Only the resources listed here, the assignments and users they reference, and the foreign keys between them can be embedded; anything else is refused with `E_AP_6003`
- `?order=created_at.desc` - sort (append `.asc` / `.desc`)
- `?limit=25&offset=50` - paginate (maximum 1000 rows per page)
### Columns not available over the API
Some columns are never returned: `tenant_id` on every resource (a key is bound to one tenant), `internal_notes` on jobs (staff-only; it can be set with `PATCH`, and a job''s note timeline is at `GET /v1/jobs/{jobId}/notes`), and the raw PostGIS geography columns `customers.location`, `locations.location` and `resources.current_location`. Read a position from `latitude`/`longitude` on locations and `current_latitude`/`current_longitude` on resources instead. `select=*`, at the top level or inside an embed, returns every other column. Naming one of these columns in `select`, a filter or `order` is refused with `E_AP_6003`. To read, update or delete one record, address it by id: `GET`, `PATCH` or `DELETE /v1/jobs/{jobId}` (likewise customers, resources and locations). `PATCH` and `DELETE` on a collection change every row matching its filters, e.g. `PATCH /v1/jobs?status=eq.T_PENDING`; they are listed under Workflows. ### Counting and paging
Send `Prefer: count=exact` on a list request to get the total row count back in the `Content-Range` response header (`0-24/312`). `count=planned` and `count=estimated` are cheaper approximations for large tables. ## Idempotency
**Guarantee.** Within 24 hours, a write sent with a given `Idempotency-Key` is applied at most once. If you did not receive a response, retrying with the same key and the same request is always safe. **Core rule.** A key is consumed only when the write is actually applied. The key and the write commit in the same database transaction, so they succeed or fail together. Every write request (`POST`, `PATCH`, `PUT`, `DELETE`) must send an `Idempotency-Key` header. | Situation | Response | Write applied?'
version: 1.0.0
servers:
- url: https://api.workwing.io
description: Production
security:
- bearerAuth: []
tags:
- name: Custom Fields
description: 'Custom field definitions: the schema (keys, labels, types) for job, customer and resource custom fields. Read them for discovery, create and update them to define the fields an integration fills in. Set values via each entity''s custom-fields endpoint.'
paths:
/v1/custom-field-definitions:
get:
operationId: listCustomFieldDefinitions
summary: List custom field definitions
tags:
- Custom Fields
description: 'List active custom field definitions (the schema: key, label, type, and whether required) for your organisation. Filter with `?entity_type=T_JOB` (or `T_CUSTOMER` / `T_RESOURCE`).
Set values with `PATCH /v1/jobs/{jobId}/custom-fields` (and the equivalent customer and resource endpoints), which reference these definitions by `field_key`.'
parameters:
- schema:
type: string
enum:
- T_JOB
- T_CUSTOMER
- T_RESOURCE
in: query
name: entity_type
required: false
description: Filter to one entity type
security:
- bearerAuth: []
x-required-scopes:
- read:custom_field_definitions
responses:
'200':
description: Default Response
content:
application/json:
schema:
type: object
required:
- data
- meta
properties:
data:
type: array
items:
type: object
required:
- id
- entity_type
- template_id
- field_key
- field_label
- field_description
- field_type
- field_config
- display_order
- display_section
- display_width
- is_required
- is_active
- is_searchable
- is_filterable
- created_at
- updated_at
- created_by_user_id
properties:
id:
type: string
format: uuid
entity_type:
type: string
enum:
- T_JOB
- T_CUSTOMER
- T_RESOURCE
template_id:
type:
- string
- 'null'
format: uuid
description: Set when the field belongs to one job type (defined in the form builder). Null for tenant-wide fields, which is every field created over the API.
field_key:
type: string
field_label:
type: string
field_description:
type:
- string
- 'null'
field_type:
type: string
enum:
- T_TEXT
- T_TEXTAREA
- T_NUMBER
- T_DATE
- T_DATETIME
- T_BOOLEAN
- T_SELECT
- T_MULTISELECT
- T_URL
- T_EMAIL
- T_PHONE
- T_SCANNER
field_config:
type:
- object
- 'null'
additionalProperties: true
description: 'Type-specific settings: minLength/maxLength/pattern for text, min/max/step/unit for numbers, minDate/maxDate for dates, options for select and multiselect, allowedBarcodeTypes/scannerHint for scanner.'
display_order:
type:
- integer
- 'null'
display_section:
type:
- string
- 'null'
display_width:
type:
- string
- 'null'
enum:
- T_FULL
- T_HALF
- T_THIRD
- null
is_required:
type:
- boolean
- 'null'
is_active:
type:
- boolean
- 'null'
is_searchable:
type:
- boolean
- 'null'
is_filterable:
type:
- boolean
- 'null'
created_at:
type:
- string
- 'null'
format: date-time
updated_at:
type:
- string
- 'null'
format: date-time
created_by_user_id:
type:
- string
- 'null'
format: uuid
meta:
type: object
required:
- request_id
- timestamp
properties:
request_id:
type: string
timestamp:
type: string
format: date-time
post:
operationId: createCustomFieldDefinition
summary: Create a custom field definition
tags:
- Custom Fields
description: 'Define a new custom field on jobs, customers or resources for your whole organisation. Requires `create:custom_field_definitions`.
`entity_type`, `field_key` and `field_type` are fixed once created. `is_required` defaults to false; `is_searchable` and `is_filterable` default to true. The new field appears in the layout and form builder palettes; placing it on a form is done in the web app.
Returns 409 when a field with the same key already exists on that entity.'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- entity_type
- field_key
- field_label
- field_type
additionalProperties: true
properties:
entity_type:
type: string
enum:
- T_JOB
- T_CUSTOMER
- T_RESOURCE
field_key:
type: string
pattern: ^[a-z0-9_]+$
description: Stable key the value is stored against (lowercase letters, digits, underscores)
field_type:
type: string
enum:
- T_TEXT
- T_TEXTAREA
- T_NUMBER
- T_DATE
- T_DATETIME
- T_BOOLEAN
- T_SELECT
- T_MULTISELECT
- T_URL
- T_EMAIL
- T_PHONE
- T_SCANNER
field_label:
type: string
field_description:
type:
- string
- 'null'
display_section:
type:
- string
- 'null'
is_required:
type: boolean
is_searchable:
type: boolean
is_filterable:
type: boolean
field_config:
type: object
additionalProperties: true
description: 'Type-specific settings: minLength/maxLength/pattern for text, min/max/step/unit for numbers, minDate/maxDate for dates, options for select and multiselect, allowedBarcodeTypes/scannerHint for scanner.'
security:
- bearerAuth: []
x-required-scopes:
- create:custom_field_definitions
responses:
'201':
description: Default Response
content:
application/json:
schema:
type: object
required:
- data
- meta
properties:
data:
type: object
required:
- id
- entity_type
- template_id
- field_key
- field_label
- field_description
- field_type
- field_config
- display_order
- display_section
- display_width
- is_required
- is_active
- is_searchable
- is_filterable
- created_at
- updated_at
- created_by_user_id
properties:
id:
type: string
format: uuid
entity_type:
type: string
enum:
- T_JOB
- T_CUSTOMER
- T_RESOURCE
template_id:
type:
- string
- 'null'
format: uuid
description: Set when the field belongs to one job type (defined in the form builder). Null for tenant-wide fields, which is every field created over the API.
field_key:
type: string
field_label:
type: string
field_description:
type:
- string
- 'null'
field_type:
type: string
enum:
- T_TEXT
- T_TEXTAREA
- T_NUMBER
- T_DATE
- T_DATETIME
- T_BOOLEAN
- T_SELECT
- T_MULTISELECT
- T_URL
- T_EMAIL
- T_PHONE
- T_SCANNER
field_config:
type:
- object
- 'null'
additionalProperties: true
description: 'Type-specific settings: minLength/maxLength/pattern for text, min/max/step/unit for numbers, minDate/maxDate for dates, options for select and multiselect, allowedBarcodeTypes/scannerHint for scanner.'
display_order:
type:
- integer
- 'null'
display_section:
type:
- string
- 'null'
display_width:
type:
- string
- 'null'
enum:
- T_FULL
- T_HALF
- T_THIRD
- null
is_required:
type:
- boolean
- 'null'
is_active:
type:
- boolean
- 'null'
is_searchable:
type:
- boolean
- 'null'
is_filterable:
type:
- boolean
- 'null'
created_at:
type:
- string
- 'null'
format: date-time
updated_at:
type:
- string
- 'null'
format: date-time
created_by_user_id:
type:
- string
- 'null'
format: uuid
meta:
type: object
required:
- request_id
- timestamp
properties:
request_id:
type: string
timestamp:
type: string
format: date-time
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
/v1/custom-field-definitions/{id}:
patch:
operationId: updateCustomFieldDefinition
summary: Update a custom field definition
tags:
- Custom Fields
description: 'Update the label, description, section, flags or type-specific settings of a definition, or deactivate it with `{"is_active": false}` (stored values are kept). Requires `update:custom_field_definitions`.
`entity_type`, `field_key` and `field_type` cannot be changed; a body naming them is rejected with 400.'
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: true
properties:
field_label:
type: string
field_description:
type:
- string
- 'null'
display_section:
type:
- string
- 'null'
is_required:
type: boolean
is_searchable:
type: boolean
is_filterable:
type: boolean
field_config:
type: object
additionalProperties: true
description: 'Type-specific settings: minLength/maxLength/pattern for text, min/max/step/unit for numbers, minDate/maxDate for dates, options for select and multiselect, allowedBarcodeTypes/scannerHint for scanner.'
is_active:
type: boolean
description: false deactivates the field and hides it from forms and the list; values are kept
parameters:
- schema:
type: string
format: uuid
in: path
name: id
required: true
- $ref: '#/components/parameters/IdempotencyKey'
security:
- bearerAuth: []
x-required-scopes:
- update:custom_field_definitions
responses:
'200':
description: Default Response
content:
application/json:
schema:
type: object
required:
- data
- meta
properties:
data:
type: object
required:
- id
- entity_type
- template_id
- field_key
- field_label
- field_description
- field_type
- field_config
- display_order
- display_section
- display_width
- is_required
- is_active
- is_searchable
- is_filterable
- created_at
- updated_at
- created_by_user_id
properties:
id:
type: string
format: uuid
entity_type:
type: string
enum:
- T_JOB
- T_CUSTOMER
- T_RESOURCE
template_id:
type:
- string
- 'null'
format: uuid
description: Set when the field belongs to one job type (defined in the form builder). Null for tenant-wide fields, which is every field created over the API.
field_key:
type: string
field_label:
type: string
field_description:
type:
- string
- 'null'
field_type:
type: string
enum:
- T_TEXT
- T_TEXTAREA
- T_NUMBER
- T_DATE
- T_DATETIME
- T_BOOLEAN
- T_SELECT
- T_MULTISELECT
- T_URL
- T_EMAIL
- T_PHONE
- T_SCANNER
field_config:
type:
- object
- 'null'
additionalProperties: true
description: 'Type-specific settings: minLength/maxLength/pattern for text, min/max/step/unit for numbers, minDate/maxDate for dates, options for select and multiselect, allowedBarcodeTypes/scannerHint for scanner.'
display_order:
type:
- integer
- 'null'
display_section:
type:
- string
- 'null'
display_width:
type:
- string
- 'null'
enum:
- T_FULL
- T_HALF
- T_THIRD
- null
is_required:
type:
- boolean
- 'null'
is_active:
type:
- boolean
- 'null'
is_searchable:
type:
- boolean
- 'null'
is_filterable:
type:
- boolean
- 'null'
created_at:
type:
- string
- 'null'
format: date-time
updated_at:
type:
- string
- 'null'
format: date-time
created_by_user_id:
type:
- string
- 'null'
format: uuid
meta:
type: object
required:
- request_id
- timestamp
properties:
request_id:
type: string
timestamp:
type: string
format: date-time
components:
parameters:
IdempotencyKey:
name: Idempotency-Key
in: header
required: true
description: Unique per write, 1-256 printable ASCII characters. Retrying with the same key and the same request replays the original response for 24 hours. See Idempotency.
schema:
type: string
minLength: 1
maxLength: 256
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: API Key
description: 'Use your WorkWing API key (format: `ww_sk_live_<tokenId>.<secret>`). The scopes an operation requires are published on it as `x-required-scopes`.'