# Worksome API Documentation — Full Content > This file contains the complete text of all documentation pages for AI ingestion. > Last updated: 2026-08-13 --- ## / # Worksome We support two methods for working with data in the [Worksome][worksome] platform: a GraphQL-based public API and webhooks. The [GraphQL API][graphql] is the primary way for third-party applications to read and write to the [Worksome][worksome] platform. All parts of the Worksome platform will be available through the API. The [Webhooks][webhook] are a way to receive real-time updates about events happening in the [Worksome][worksome] platform, so that you can update your systems with new or updated information. ## Quick links - [Basics of Authentication](/authentication) Learn about how to generate tokens and authenticate with the GraphQL API. - [Getting Started with the GraphQL API](/graphql) Learn the basics of using the GraphQL API, the required endpoints for access, and make your first request. - [Getting Started with Webhooks](/webhooks) Learn the essentials of webhooks and what we need from you to get it set up for you. - [Integration Options](/integrations) Explore all the ways to connect with Worksome — from no-code Zapier automation to full programmatic access. - [Error Reference](/errors) Look up error codes and resolution steps. - [Troubleshooting and support](/support) Learn about troubleshooting the GraphQL API, and how to get support if needed. ## Guides - [Introduction to GraphQL](/graphql/guides/introduction) An introduction to GraphQL and the terminology surrounding it. - [Introduction to Webhooks](/webhooks/guides/introduction) An introduction to Webhooks and the terminology surrounding them. ## Integrations Connect Worksome with your existing tools and workflows. - [Integration Options](/integrations) — Choose the right method for your use case - [CLI](/integrations/cli) — Full API access from the command line - [Zapier](/integrations/zapier) — No-code automation with 6,000+ apps - [MCP Server](/integrations/mcp-server) — AI assistant integration [graphql]: https://graphql.org [webhook]: https://en.wikipedia.org/wiki/Webhook [worksome]: https://worksome.com --- ## /authentication # Authentication ## OAuth [OAuth 2.0][oauth2] is the industry-standard protocol for authorization. It allows your application to authenticate with Worksome on behalf of other users. This is the preferred way for you to interact with the Worksome API. ### Creating an OAuth Client OAuth clients can be created and managed via our [API clients page][api-clients]. Worksome supports setting a client name, which can be used to identify your client within your list, and a [redirect URL (or callback URL)](https://oauth.com/oauth2-servers/redirect-uris). The redirect URL is used to specify which URL is supported for redirection as part of the OAuth flow. Redirect URLs ensure that Worksome will redirect the user back to the correct application, after authorization. As an example, if your domain was `example.org`, you might have the redirect URL as `https://example.org/callback`. Upon authorization, the user would be redirected back to that redirect URL. ### The OAuth Flow 1. **Users are redirected to request their Worksome identity** Users should be redirected to the following URL from your application, this is usually via an authorization link or button on your website. ```http GET https://use.worksome.com/oauth/authorize ``` The authorization endpoint takes the following input parameters: | Parameter name | Type | Description | |-----------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `client_id` | `string` | **Required**. The client ID you received from Worksome when registering. | | `response_type` | `string` | **Required**. This should be set to `code`. | | `redirect_uri` | `string` | **Required**. The URL in your application where users will be sent after authorization. | | `state` | `string` | This is an unguessable random string. It is used to protect against cross-site request forgery (CSRF) attacks. | | `prompt` | `string` | One of `consent` (the authorization approval screen will always be shown) or `login` (the user will always be prompted to log in, even if they have a session), or if excluded, the user will only be prompted for authorization if they have not done so previously. | 2. **Users are redirected back to your site by Worksome** If the authorization request is accepted by the user, Worksome redirects back to your site using the Redirect URL, with a temporary `code`, as well as the `state` if provided. The temporary code will expire after 10 minutes. If the states do not match, then a third party created the request, and you should abort the process. The `code` should now be exchanged for an access token. This should be done within your application's backend, and should not be visible to the user (as it contains your client secret). ```http POST https://use.worksome.com/oauth/token ``` The token endpoint takes the following input parameters: | Parameter name | Type | Description | |-----------------|----------|-------------------------------------------------------------------------------------| | `client_id` | `string` | **Required**. The client ID you received from Worksome when registering. | | `client_secret` | `string` | **Required**. The client secret you received from Worksome when registering. | | `code` | `string` | **Required**. The code you received from Worksome as part of the previous step. | | `grant_type` | `string` | **Required**. The grant type should be `authorization_code`. | | `redirect_uri` | `string` | **Required**. The URL in your application where users are sent after authorization. | The data will be included as JSON in the response. For example: ```json { "token_type": "Bearer", "expires_in": "...", "access_token": "...", "refresh_token": "..." } ``` 3. **Your application accesses the API with the user's access token** The access token allows you to make requests to the API on behalf of a user. ```http POST https://api.worksome.com/graphql ``` For example, in curl you can set the Authorization header like this: ```shell $ curl -H "Authorization: Bearer OAUTH-TOKEN" https://api.worksome.com/graphql ``` ### Refreshing OAuth Tokens Your OAuth token can be refreshed by making another `POST` request to the token URL (`/oauth/token`). This requires a `grant_type` of `refresh_token`, the refresh token (`refresh_token`), and your `client_id` and `client_secret`. ```shell curl -X POST https://use.worksome.com/oauth/token \ -H 'Authorization: Bearer {access_token}' \ -d 'refresh_token={refresh_token}' \ -d 'grant_type=refresh_token' \ -d 'client_id={client_id}' \ -d 'client_secret={client_secret}' ``` ## Personal Access Tokens Personal access tokens are tokens for a specific user. These allow you to generate a token without using the OAuth flow. These tokens are useful when you are testing the API. You can manage the personal access tokens for your account via our [API tokens page][api-tokens]. ## Authenticating with the API All API calls that require authentication must provide a standard `Authorization` header using the [`Bearer` authentication scheme][mdn-bearer-authentication-scheme]. To make a test request via [curl][], run the command below with your token: ```bash curl -H "Authorization: Bearer ${WORKSOME_API_TOKEN}" \ -H "Content-Type: application/json" \ -X POST \ -d '{"query": "query { viewer { name } }"}' \ https://api.worksome.com/graphql ``` > [!WARNING] > The string value of "query" must escape newline characters or the schema will not parse it properly. For the POST body, use outer double quotes and escaped inner double quotes. ## Token expiration By default, generated API tokens are valid for 6 months from creation. Upon reaching your token's expiration date, the token is automatically revoked. ## Revoking a token Tokens can be manually revoked via the [API tokens page][api-tokens]. This is useful for when the token is no longer necessary, or for security purposes. ## Revoking an OAuth client Clients can be manually revoked via the [API clients page][api-clients]. This will prevent the creation of new tokens with this client, and also prevent any tokens generated by this client from authenticating. [api-clients]: https://use.worksome.com/integrations/api-clients [api-tokens]: https://use.worksome.com/integrations/api-tokens [curl]: https://curl.se [mdn-bearer-authentication-scheme]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#bearer [oauth2]: https://oauth.net/2 --- ## /graphql/index # Getting Started The [GraphQL API][graphql] is the primary way for third-party applications to read and write to the [Worksome][] platform. **New to APIs?** Start with [What is an API?](/graphql/guides/introduction#what-is-an-api) for a beginner-friendly overview. **Coming from REST?** See [Coming from REST?](/graphql/guides/introduction#coming-from-rest) for a quick comparison. For GraphQL terminology, read the full [Introduction to GraphQL][introduction-to-graphql] guide. To get started, you'll need an API access token, you can learn about and generate one from [the Authentication page][authentication]. Once you've generated a token, carry on reading to learn how to use the API. We also have extensive documentation for the API available on [Apollo Studio](https://studio.apollographql.com/public/Worksome/home?variant=production), which is searchable and may be easier to use. ## The GraphQL Endpoint The GraphQL API has a single endpoint: [`https://api.worksome.com/graphql`][api-graphql] The endpoint is constant no matter what operation you perform. This endpoint also functions as a GraphQL explorer, using [the official GraphiQL interface][graphiql]. This can be used to test GraphQL queries or use [introspection][] to discover the API. ## Making your first request GraphQL requests to our API are made over HTTP, via the POST request method. All data is sent in [JavaScript Object Notation (JSON)][json] format. We'll be creating a GraphQL API request using the [Laravel HTTP Client][laravel-http] for this example. ```php use Illuminate\Support\Facades\Http; $apiToken = env('WORKSOME_API_TOKEN'); // The query is a GraphQL structured request specifying what is needed. $query = <<post('https://api.worksome.com/graphql', [ 'query' => $query, 'variables' => $variables, ]) ->json(); dd($response); ``` ```shell curl -X POST https://api.worksome.com/graphql \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "query": "{ viewer { id name } }", "variables": {} }' ``` ### JavaScript ```javascript const response = await fetch('https://api.worksome.com/graphql', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'Content-Type': 'application/json', }, body: JSON.stringify({ query: '{ viewer { id name } }', variables: {}, }), }); const { data, errors } = await response.json(); ``` ## Discover your accounts Most API operations require a company context. After authenticating, query the `viewer` for your user info and the top-level `accounts` field for the accounts you can act on. `accounts` returns an `Account` interface — use a fragment (e.g., `... on Company`) to read company-specific fields: ```graphql { viewer { id name email } accounts { id name ... on Company { market } } } ``` ```json { "data": { "viewer": { "id": "VXNlcjoxMjM0", "name": "Jane Smith", "email": "jane@example.com" }, "accounts": [ { "id": "Q29tcGFueTox", "name": "Acme Corp", "market": "UK" }, { "id": "Q29tcGFueToy", "name": "Acme Corp (Staging)", "market": "UK" } ] } } ``` Use the account `id` as the company identifier when creating hires, jobs, or other company-scoped operations. The `Account` interface is implemented by `Company`, `Organisation`, `Partner`, `StaffingAgency`, and `Worker` — pass the right kind of ID for the operation you are calling (most company-context operations require a `Company` ID). [graphql]: https://graphql.org [worksome]: https://worksome.com [authentication]: /authentication [introduction-to-graphql]: /graphql/guides/introduction [api-graphql]: https://api.worksome.com/graphql [graphiql]: https://github.com/graphql/graphiql/tree/main/packages/graphiql#readme [introspection]: https://graphql.org/learn/introspection [json]: https://json.org [laravel-http]: https://laravel.com/docs/http-client --- ## /graphql/guides/introduction # Introduction to GraphQL ## What is an API? An API (Application Programming Interface) is a way for software systems to talk to each other. When you use the Worksome web app, your browser communicates with Worksome's servers behind the scenes. The API is the same interface, but designed for your own software to use directly. With the Worksome API, your applications can do things like: - **Read data** — list active hires, check invoice statuses, view compliance requirements - **Create records** — draft new hires, create job postings - **React to changes** — receive notifications when a contract is accepted or a hire is updated (via [Webhooks](/webhooks)) You send a **request** (what you want), and the API returns a **response** (the data or confirmation). All communication happens over HTTPS, so it works from any programming language or tool that can make web requests. If you're not a developer, you don't need to use the API directly. [Zapier](/integrations/zapier) lets you connect Worksome to other tools without writing code. ## Coming from REST? If you've worked with REST APIs before, here are the key differences with GraphQL: | | REST (illustrative) | Worksome GraphQL | |---|---|---| | **Endpoints** | Many (e.g., `/hires`, `/hires/123`, `/contracts`) | One: `https://api.worksome.com/graphql` | | **Data fetching** | Fixed response shape per endpoint | You specify exactly which fields you need | | **Over-fetching** | Common — endpoints return all fields | Never — you only get what you ask for | | **Fetching related data** | Requires multiple round-trips (get hire, then get contract, then get worker) | One request can fetch related data across entities | | **HTTP methods** | `GET`, `POST`, `PUT`, `DELETE` | Always `POST` | | **Documentation** | OpenAPI / Swagger | Self-documenting schema you can introspect | > [!WARNING] > The REST column above is a generic illustration only. Worksome does **not** publish a REST API: there are no `/hires`, `/contracts`, etc. endpoints. Every operation goes through the single GraphQL endpoint `https://api.worksome.com/graphql`. ### A practical example A REST API might split fetching a hire with its contract and worker into three requests: ``` GET /hires/123 GET /contracts/456 GET /workers/789 ``` In GraphQL, you do it in one: ```graphql { hire(id: "SGlyZToxMjM0") { id activeStatus latestContract { rate currency startDate } worker { name email } } } ``` You get exactly the fields you asked for — no more, no less. This makes responses smaller and faster, especially when you only need a few fields from each entity. ### Reading vs writing - **Queries** are the GraphQL equivalent of `GET` — they read data. - **Mutations** are the equivalent of `POST`/`PUT`/`DELETE` — they create or change data. Both use the same endpoint and the same `POST` method. The operation type (`query` or `mutation`) tells the API what you intend to do. ## GraphQL terminology You will probably encounter some new terminology in the Worksome GraphQL API [reference docs][graphql]. ## Schema A schema defines a GraphQL APIs type system. It describes the complete set of possible data (objects, fields, relationships, everything) that a client can access. Calls from the client are [validated][graphql-validation] and [executed][graphql-execution] against the schema. A client can find information about the schema via [introspection](#discovering-the-graphql-api). A schema resides on the GraphQL API server. For more information, see "[Discovering the GraphQL API](#discovering-the-graphql-api)." ## Field A field is a unit of data you can retrieve from an object. As the [official GraphQL docs][graphql-schema] say: "The GraphQL query language is basically about selecting fields on objects." The [official spec][graphql-spec] also says about fields: > All GraphQL operations must specify their selections down to fields which return scalar values to ensure an unambiguously shaped response. This means that if you try to return a field that is not a scalar, schema validation will throw an error. You must add nested subfields until all fields return scalars. ## Argument An argument is a set of key-value pairs attached to a specific field. Some fields require an argument. [Mutations][mutations] require an input object as an argument. ## Implementation A GraphQL schema may use the term _implements_ to define how an object inherits from an [interface][interfaces]. Here's a contrived example of a schema that defines interface `X` and object `Y`: ```graphql interface X { some_field: String! other_field: String! } type Y implements X { some_field: String! other_field: String! new_field: String! } ``` This means object `Y` requires the same fields/arguments/return types that interface `X` does, while adding new fields specific to object `Y`. (The `!` means the field is required.) In the reference docs, you'll find that: * Each [object](/graphql/reference/objects) lists the interface(s) _from which it inherits_ under **Implements**. * Each [interface](/graphql/reference/interfaces) lists the objects _that inherit from it_ under **Implementations**. ## Connection Connections let you query related objects as part of the same call. With connections, you can use a single GraphQL call where you would have to use multiple calls to a REST API. It's helpful to picture a graph: dots connected by lines. The dots are nodes, the lines are edges. A connection defines a relationship between nodes. ## Node _Node_ is a generic term for an object. You can look up a node directly, or you can access related nodes via a connection. If you specify a `node` that does not return a [scalar](/graphql/reference/scalars), you must include subfields until all fields return scalars. ## Discovering the GraphQL API GraphQL is [introspective][graphql-introspection]. This means you can query a GraphQL schema for details about itself. - Query `__schema` to list all types defined in the schema and get details about each: ```graphql query { __schema { types { name kind description fields { name } } } } ``` - Query `__type` to get details about any type: ```graphql query { __type(name: "Company") { name kind description fields { name } } } ``` You can also run an _introspection query_ of the schema as a regular `POST` request: ```shell curl -H "Authorization: Bearer ${WORKSOME_API_TOKEN}" \ -H "Content-Type: application/json" \ -X POST \ -d '{"query": "{ __schema { types { name kind } } }"}' \ https://api.worksome.com/graphql ``` > [!WARNING] > If you get a response containing a `"message": "Unauthenticated."` error, check that you are using a valid token. For more information, see "[Authentication][authentication]." The results are in JSON, so we recommend pretty-printing them for easier reading and searching. You can use a command-line tool like [jq][] or parse the results using a language-specific pretty-printer for this purpose. > Every GraphQL request to Worksome goes through `POST /graphql` with `Content-Type: application/json`. The gateway rejects `GET` requests with `BAD_REQUEST` (a CSRF guard). If you genuinely need `GET` (e.g. for caching), send the `apollo-require-preflight: true` header. [graphql-schema]: https://graphql.org/learn/schema [graphql-spec]: https://spec.graphql.org/October2021/#sec-Language.Fields [graphql-validation]: https://graphql.org/learn/validation [graphql-execution]: https://graphql.org/learn/execution [graphql-introspection]: https://graphql.org/learn/introspection [jq]: https://jqlang.org [authentication]: /authentication [graphql]: /graphql [interfaces]: /graphql/reference/interfaces [mutations]: /graphql/reference/mutations --- ## /graphql/guides/pagination # Pagination & Filtering The Worksome GraphQL API returns collections as **paginated lists**. Instead of fetching every record at once, you request a page of results and navigate forward or backward through the full set. This keeps responses fast and predictable regardless of how much data exists. Worksome uses [Lighthouse](https://lighthouse-php.com/)'s offset-based pagination model. Paginated queries return a `data` array of results alongside a `paginatorInfo` object that tells you where you are in the result set. ## Basic Pagination Every paginated query accepts a `first` argument that controls the page size. The response includes a `data` array with the requested items and a `paginatorInfo` object with metadata about the result set. ```graphql query { hires(first: 10) { data { id worker { name } company { name } activeStatus } paginatorInfo { currentPage lastPage total hasMorePages } } } ``` The response looks like this: ```json { "data": { "hires": { "data": [ { "id": "SGlyZTox", "worker": { "name": "Jane Doe" }, "company": { "name": "Acme Corp" }, "activeStatus": "ACTIVE" } ], "paginatorInfo": { "currentPage": 1, "lastPage": 5, "total": 48, "hasMorePages": true } } } } ``` ### Understanding `paginatorInfo` | Field | Type | Description | |---|---|---| | `currentPage` | `Int!` | The current page number (1-based). | | `lastPage` | `Int!` | The last available page number. | | `total` | `Int!` | Total number of records matching the query. | | `count` | `Int!` | Number of records returned on this page. | | `hasMorePages` | `Boolean!` | Whether there are more pages after the current one. | | `perPage` | `Int!` | The number of items requested per page. | | `firstItem` | `Int` | The index of the first item on this page. | | `lastItem` | `Int` | The index of the last item on this page. | ## Navigating Pages Use the `page` argument to jump to a specific page in the result set. Combined with `first`, this gives you offset-based pagination. ```graphql query { hires(first: 10, page: 2) { data { id activeStatus } paginatorInfo { currentPage lastPage total hasMorePages } } } ``` A typical pagination flow: 1. Fetch the first page with `first: 10` (page defaults to `1`). 2. Check `paginatorInfo.hasMorePages` to see if more results exist. 3. Request the next page by incrementing `page`. 4. Repeat until `hasMorePages` is `false`. > [!TIP] > Use `paginatorInfo.lastPage` to show the user how many pages are available, and `total` to display the overall count of matching records. ## Filtering Most paginated queries accept filter arguments that narrow down the results **before** pagination is applied. Filters vary by resource but follow a consistent pattern. ### Filtering Hires The `hires` query supports several filters: ```graphql query { hires( first: 10 activeStatus: [ACTIVE] search: "Jane" startDateRange: { from: "2025-01-01", to: "2025-12-31" } ) { data { id worker { name } activeStatus startDate } paginatorInfo { total hasMorePages } } } ``` Available filter arguments for `hires`: | Argument | Type | Description | |---|---|---| | `activeStatus` | `[HireActiveStatus!]` | Filter by one or more hire statuses (e.g., `[ACTIVE]`, `[OFFERED, ACTIVE]`). | | `accounts` | `[ID!]` | Filter by one or more company IDs (the company side of the hire). | | `search` | `String` | Free-text search across hire fields. | | `startDateRange` | `DateRangeInput` | Filter by start date range (`from`/`to`). | | `endDateRange` | `DateRangeInput` | Filter by end date range (`from`/`to`). | | `orderBy` | `[HiresOrderByClauseInput!]` | Sort results. Each clause is `{ field: HireOrderByColumn!, order: SortOrder! }` (see [Sorting](#sorting)). | ### Filtering Contracts ```graphql query { contracts( first: 20 statuses: [ACTIVE, DRAFT] currencies: [USD, EUR] ) { data { id status currency } paginatorInfo { total hasMorePages } } } ``` Available filter arguments for `contracts`: | Argument | Type | Description | |---|---|---| | `accounts` | `[ID!]` | Filter by account IDs. | | `currencies` | `[Currency!]` | Filter by currency enum values (e.g., `[USD, EUR]`). | | `statuses` | `[ContractStatus!]` | Filter by contract statuses. Valid values: `DRAFT`, `ACTIVE`, `ARCHIVED`. | | `locationPreferences` | `[LocationPreference!]` | Filter by location preferences. | ### Filtering Invoices ```graphql query { invoices( first: 15 status: [UNPAID] search: "INV-2025" currency: [USD] ) { data { id number currency grossAmount date } paginatorInfo { total hasMorePages } } } ``` Available filter arguments for `invoices`: | Argument | Type | Description | |---|---|---| | `accounts` | `[ID!]` | Filter by account IDs. | | `status` | `[InvoiceStatus!]` | Filter by one or more invoice statuses. Valid values: `PAID`, `UNPAID`, `OVERDUE`, `CREDITED`. | | `search` | `String` | Free-text search across invoice fields. | | `currency` | `[Currency!]` | Filter by one or more currency enum values. | | `orderBy` | `[QueryInvoicesOrderByOrderByClause!]` | Sort results. Each clause is `{ column: InvoicesOrderByColumn!, order: SortOrder! }` (see [Sorting](#sorting)). | ## Sorting Queries that support an `orderBy` argument let you control the sort order of results. The `orderBy` argument takes an array of sort clauses, each specifying a column and an `order` direction. The exact key for the column varies between input types — most use `field`, but a few (e.g. `QueryInvoicesOrderByOrderByClause`) use `column` — so check the input type for the query you are calling. ```graphql query { hires(first: 10, orderBy: [{ field: CREATED_AT, order: DESC }]) { data { id activeStatus createdAt } paginatorInfo { total } } } ``` You can sort by multiple clauses. The first entry is the primary sort, the second is the tiebreaker, and so on: ```graphql query { invoices( first: 20 orderBy: [ { column: DATE, order: DESC } { column: NUMBER, order: ASC } ] ) { data { id number date } paginatorInfo { total } } } ``` > [!TIP] > Check the schema reference for each query to see which columns are available for sorting. The sortable columns are defined as enums (e.g., `HireOrderByColumn`, `InvoicesOrderByColumn`). Valid `InvoicesOrderByColumn` values are `DATE`, `DUE_DATE`, `NUMBER`, and `TOTAL_AMOUNT`. ## Combining Pagination with Filters Filters and pagination work together naturally. Filters narrow the result set first, and then pagination splits the filtered results into pages. Here is a practical example that fetches the second page of active hires for a specific company, sorted by start date: ```graphql query { hires( first: 10 page: 2 activeStatus: [ACTIVE] accounts: ["Q29tcGFueTox"] orderBy: [{ field: START_DATE, order: DESC }] ) { data { id worker { name } company { name } activeStatus startDate } paginatorInfo { currentPage lastPage total hasMorePages } } } ``` > [!WARNING] > When building paginated UIs, always apply your filters and sort order **consistently** across page requests. Changing filters between pages will produce unexpected results since the underlying result set changes. ## Best Practices ### Choose an appropriate page size - Start with a page size of **10-25** items for most use cases. - The maximum allowed page size is typically **100**. Requesting more may result in an error. - Smaller pages mean faster responses and lower memory usage on both client and server. ### Avoid deep pages - Requesting high page numbers (e.g., `page: 500`) can be slow because the server still has to count through all preceding records. - If you need to process a large dataset, iterate sequentially from page 1 rather than jumping to arbitrary pages. - For bulk data exports, consider using smaller page sizes and processing pages sequentially. ### Use filters to reduce result sets - The more specific your filters, the smaller the result set and the faster the query. - Apply filters server-side rather than fetching all records and filtering on the client. ### Request only the fields you need - GraphQL lets you request exactly the fields you need. Avoid selecting large nested objects when you only need an ID or name. - Requesting fewer fields reduces response size and server processing time. ### Cache total counts carefully - The `total` field in `paginatorInfo` requires counting all matching records, which can be expensive for large datasets. - If you only need to know whether more results exist, use `hasMorePages` instead. - Cache the total count on the client side and avoid re-fetching it on every page request when the underlying data has not changed. > [!WARNING] > For large datasets, prefer sequential page-by-page iteration over jumping to arbitrary page numbers. This produces more predictable performance and avoids timeouts. --- ## /graphql/guides/error-handling # Error Handling Unlike REST APIs that use HTTP status codes to signal errors, the Worksome GraphQL API **returns HTTP 200 for almost every request**, with errors reported inside the response body in the `errors` array. The exceptions are HTTP 400 (e.g. when the request shape is wrong, such as missing the `Content-Type: application/json` header). Your client code must always inspect the response payload rather than relying on status codes alone. The Worksome GraphQL endpoint is fronted by an [Apollo Federation](https://www.apollographql.com/docs/federation/) gateway in front of the Lighthouse-PHP `platform` subgraph. Errors fall into two layers: those raised by the gateway itself (parsing, validation, transport) and those forwarded from the platform (auth, business validation, downstream failures). Both layers follow the same [GraphQL error specification](https://spec.graphql.org/October2021/#sec-Errors) and add an `extensions` object that carries the error code and additional metadata. ## Error response format Every error in the response follows the standard GraphQL error structure: ```json { "errors": [ { "message": "Human-readable error description", "locations": [ { "line": 2, "column": 3 } ], "path": ["mutationName"], "extensions": { "code": "DOWNSTREAM_SERVICE_ERROR", "serviceName": "platform" } } ], "data": null } ``` | Field | Type | Description | |---|---|---| | `message` | `String` | A human-readable description of the error. | | `locations` | `[Location]` | The line and column in the query where the error originated. | | `path` | `[String \| Int]` | The path to the field in the response that caused the error. | | `extensions` | `Object` | Structured metadata about the error. Always contains `code`. May also contain `serviceName`, a `validation` map, and `guards`. | > [!TIP] > Use `extensions.code` to determine the type of error programmatically. Do not parse the `message` string — wording may change without notice. ## Error codes The `extensions.code` field tells you which broad failure mode the error falls into. Each code maps to a class of problems with a distinct handling strategy. | Code | Source | Retryable | Description | |---|---|---|---| | `GRAPHQL_VALIDATION_FAILED` | Gateway | No | The query references a field, argument, or type that does not exist on the schema. | | `GRAPHQL_PARSE_FAILED` | Gateway | No | The GraphQL document could not be parsed (syntax error). | | `BAD_REQUEST` | Gateway | No | The HTTP request itself is malformed (missing `Content-Type`, invalid JSON, etc.). | | `DOWNSTREAM_SERVICE_ERROR` | Platform via gateway | Sometimes | The query is syntactically valid but the platform rejected the operation. Discriminate further using `extensions.validation`, `extensions.guards`, and the operation `path`. | `DOWNSTREAM_SERVICE_ERROR` covers most of what you would think of as "auth" and "business logic" failures, so the most useful work happens inside that bucket. ### Schema validation errors When the query references something that does not exist on the schema, the gateway returns `GRAPHQL_VALIDATION_FAILED` before the platform even sees the request. This is the most common error to encounter while developing. ```graphql query { hires(first: 10) { data { id nonExistentField } } } ``` ```json { "errors": [ { "message": "Cannot query field \"nonExistentField\" on type \"Hire\".", "locations": [ { "line": 5, "column": 7 } ], "extensions": { "code": "GRAPHQL_VALIDATION_FAILED" } } ] } ``` Common causes: - Typos in field or type names. - Querying a field that was removed or renamed in a schema update. - Missing required arguments on a field. - Wrong argument types (e.g., passing a `String` where `[Currency!]` is required). > [!TIP] > Validate queries against the published schema during development — either with `__schema`/`__type` introspection or by exploring the [Reference](/graphql/reference). Catching this class of error before deploy means it never appears in production. ### Bad request errors If the HTTP request itself is malformed (missing `Content-Type`, invalid JSON body, GET without an `apollo-require-preflight` header), the gateway returns HTTP 400 with `code: "BAD_REQUEST"` before any GraphQL processing. ```json { "errors": [ { "message": "This operation has been blocked as a potential Cross-Site Request Forgery (CSRF). Please either specify a 'content-type' header (with a type that is not one of application/x-www-form-urlencoded, multipart/form-data, text/plain) or provide a non-empty value for one of the following headers: x-apollo-operation-name, apollo-require-preflight", "extensions": { "code": "BAD_REQUEST" } } ] } ``` Make sure every request includes: - `Authorization: Bearer {token}` - `Content-Type: application/json` - A POST body containing `{ "query": "...", "variables": { ... } }`. ### Validation errors Input that fails the platform's business rules surfaces as `DOWNSTREAM_SERVICE_ERROR` with an `extensions.validation` map of dot-notation field paths to error messages. The presence of `validation` is the signal that this error came from the input layer rather than from auth or business logic. ```graphql mutation { createDraftHire(input: { company: "Q29tcGFueTox" name: "" startDate: "not-a-date" }) { id } } ``` ```json { "errors": [ { "message": "Validation failed for the field [createDraftHire].", "locations": [ { "line": 2, "column": 3 } ], "path": ["createDraftHire"], "extensions": { "validation": { "input.name": ["The name field is required."], "input.startDate": ["The start date is not a valid date."] }, "serviceName": "platform", "code": "DOWNSTREAM_SERVICE_ERROR" } } ], "data": { "createDraftHire": null } } ``` The keys in `extensions.validation` correspond to the input field paths using dot notation (e.g., `input.name`). Each key maps to an array of error messages for that field. > [!TIP] > Use `extensions.validation` to display inline field-level errors in your UI. Map the dot-notation keys back to your form fields for a smooth user experience. ### Authentication errors A missing or invalid bearer token comes back as `DOWNSTREAM_SERVICE_ERROR` with `guards: ["api"]` in `extensions`. This is the GraphQL equivalent of an HTTP 401. ```json { "errors": [ { "message": "Unauthenticated.", "extensions": { "guards": ["api"], "serviceName": "platform", "code": "DOWNSTREAM_SERVICE_ERROR" } } ], "data": null } ``` Common causes: - The `Authorization` header is missing or malformed. - The API token has expired (Personal Access Tokens are valid for 6 months) or been revoked. - The token does not have the required scopes. > [!TIP] > Discriminate this case from generic platform errors by checking for `extensions.guards` containing `"api"` plus the message `"Unauthenticated."`. ### Authorization errors Authorization errors occur when the authenticated user does not have permission to perform the requested operation. They also surface as `DOWNSTREAM_SERVICE_ERROR`, with no `validation` map and no `guards` — the operation `path` and message indicate which mutation/field was rejected. ```graphql mutation { terminateHire(input: { hire: "SGlyZTox" reason: PROJECT_COMPLETED_EARLY date: "2026-04-01" }) { id } } ``` ```json { "errors": [ { "message": "You are not authorized to perform this action.", "locations": [ { "line": 2, "column": 3 } ], "path": ["terminateHire"], "extensions": { "serviceName": "platform", "code": "DOWNSTREAM_SERVICE_ERROR" } } ], "data": { "terminateHire": null } } ``` Common causes: - The authenticated user does not have the required role on the company that owns the resource. - The resource belongs to a different company or workspace. - A field-level restriction is in effect (the field returns `null` in `data` and an entry shows up in `errors` — see [Partial responses](#partial-responses)). > [!WARNING] > Authorization errors are not retryable with the same credentials. Verify that the API token belongs to a user with the correct role and permissions for the operation. ### Server errors If the platform itself fails (timeout, unhandled exception), the gateway forwards a `DOWNSTREAM_SERVICE_ERROR` with `serviceName: "platform"` and a generic message. These are typically transient — wait briefly and retry. ```json { "errors": [ { "message": "Internal server error.", "extensions": { "serviceName": "platform", "code": "DOWNSTREAM_SERVICE_ERROR" } } ], "data": null } ``` > [!CAUTION] > If a server error persists after multiple retries, contact Worksome support with the full error response and the query you were executing. ## Handling errors in code Here is an example of a robust error-handling pattern in TypeScript. It switches on `extensions.code` and uses the additional metadata (`validation`, `guards`) to discriminate the platform-side codes. ```typescript interface GraphQLError { message: string; locations?: { line: number; column: number }[]; path?: (string | number)[]; extensions?: { code?: string; serviceName?: string; guards?: string[]; validation?: Record; }; } interface GraphQLResponse { data: T | null; errors?: GraphQLError[]; } async function executeQuery(query: string, variables?: Record): Promise { const response = await fetch("https://api.worksome.com/graphql", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${API_TOKEN}`, }, body: JSON.stringify({ query, variables }), }); const result: GraphQLResponse = await response.json(); if (result.errors && result.errors.length > 0) { for (const error of result.errors) { const code = error.extensions?.code; switch (code) { case "GRAPHQL_VALIDATION_FAILED": case "GRAPHQL_PARSE_FAILED": // Query references something that does not exist on the schema. // This is a developer error — fix the query. console.error("GraphQL query error:", error.message, error.locations); throw new QueryError(error.message); case "BAD_REQUEST": // The HTTP request itself was malformed. throw new BadRequestError(error.message); case "DOWNSTREAM_SERVICE_ERROR": // Discriminate using the additional extensions fields. if (error.extensions?.validation) { throw new ValidationError(error.message, error.extensions.validation); } if (error.extensions?.guards?.includes("api") && error.message === "Unauthenticated.") { throw new AuthenticationError(error.message); } // Otherwise it is an authorization or business-rule failure. throw new PlatformError(error.message, error.path); default: throw new Error(error.message); } } } return result.data as T; } ``` A corresponding validation error class that makes field errors convenient to work with: ```typescript class ValidationError extends Error { fieldErrors: Record; constructor(message: string, fieldErrors: Record) { super(message); this.fieldErrors = fieldErrors; } /** * Get errors for a specific input field. * Pass the full dot-notation path, e.g. "input.name". */ getFieldErrors(field: string): string[] { return this.fieldErrors[field] ?? []; } /** * Check whether a specific field has validation errors. */ hasFieldError(field: string): boolean { return this.getFieldErrors(field).length > 0; } } ``` ## Partial responses GraphQL can return **both data and errors** in the same response. This happens when some fields resolve successfully while others fail. The `data` object will contain `null` for the fields that errored, with corresponding entries in the `errors` array. ```graphql query { hire(id: "SGlyZTox") { id activeStatus rate worker { name email } } } ``` ```json { "errors": [ { "message": "You are not authorized to access this field.", "path": ["hire", "rate"], "extensions": { "serviceName": "platform", "code": "DOWNSTREAM_SERVICE_ERROR" } } ], "data": { "hire": { "id": "SGlyZTox", "activeStatus": "ACTIVE", "rate": null, "worker": { "name": "Jane Doe", "email": "jane@example.com" } } } } ``` In this example, the query succeeded overall and returned the hire, worker name, and email, but the authenticated user did not have permission to view the rate. The `rate` field is `null` and the reason is explained in the `errors` array. > [!WARNING] > Always check the `errors` array even when `data` is present. A non-null `data` object does not guarantee that the entire query succeeded. ## Retry strategy Not all errors are retryable. Use the error code (and the discriminators inside `DOWNSTREAM_SERVICE_ERROR`) to decide whether to retry a failed request. | Failure mode | Retryable | Action | |---|---|---| | `GRAPHQL_VALIDATION_FAILED` / `GRAPHQL_PARSE_FAILED` | No | Fix the query. This is a developer error. | | `BAD_REQUEST` | No | Fix the request shape (headers, body). | | `DOWNSTREAM_SERVICE_ERROR` with `validation` map | No | Fix the input and resubmit. | | `DOWNSTREAM_SERVICE_ERROR` with `guards: ["api"]` (`Unauthenticated.`) | No | Refresh or regenerate your API token. | | `DOWNSTREAM_SERVICE_ERROR` (authorization) | No | Verify user permissions. Do not retry with the same credentials. | | `DOWNSTREAM_SERVICE_ERROR` (transient platform error) | Yes | Retry with exponential backoff. | For retryable errors, use exponential backoff with jitter to avoid thundering-herd problems: ```typescript async function executeWithRetry( query: string, variables?: Record, maxRetries: number = 3, ): Promise { for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await executeQuery(query, variables); } catch (error) { if (error instanceof PlatformError && attempt < maxRetries) { const baseDelay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s const jitter = Math.random() * 1000; await new Promise((resolve) => setTimeout(resolve, baseDelay + jitter)); continue; } throw error; } } throw new Error("Max retries exceeded"); } ``` > [!CAUTION] > Never retry validation, authentication, or authorization errors automatically. They will fail with the same result every time and may trigger rate limiting. ## Best practices ### Always check the `errors` array Every response from the Worksome GraphQL API may contain an `errors` array. Check it on every request, even when `data` is present. ### Handle partial responses gracefully When you receive both `data` and `errors`, use the data that resolved successfully and handle the errors for the fields that failed. Do not discard the entire response because of a single field error. ### Switch on `extensions.code`, not on `message` Route your error-handling logic based on `extensions.code` (and the secondary discriminators within `DOWNSTREAM_SERVICE_ERROR`) rather than parsing error messages. Codes are stable; messages may be rephrased. ### Log error details for debugging Include the full error object — `message`, `path`, `locations`, and `extensions` — in your application logs. The `path` field is especially useful for pinpointing which part of a complex query failed. ### Display user-friendly messages The `message` field is intended as a developer-facing description. Map error codes to user-friendly messages in your UI rather than showing raw API errors to end users. The exception is validation errors, where `extensions.validation` messages are suitable for display. ### Validate queries during development Use schema introspection (`__schema`/`__type`) and the [Reference](/graphql/reference) to validate your queries before deploying them. Catching `GRAPHQL_VALIDATION_FAILED` errors locally means they never reach production. --- ## /graphql/guides/rate-limiting # Rate Limiting The Worksome GraphQL API enforces rate limits to ensure fair usage and maintain platform stability for all consumers. Every authenticated request counts toward your rate limit quota. ## Rate Limit Tiers Rate limits are applied **per API token**. Each token is allocated a fixed number of requests per minute within a rolling window. | Tier | Requests per Minute | |-------------|---------------------| | Default | 60 | > [!WARNING] > The exact rate limit for your integration may vary depending on your plan and agreement. Contact your Worksome account representative if you need a higher throughput allocation. Unauthenticated requests (where permitted) are rate-limited by IP address at a lower threshold. > [!WARNING] > The federation gateway in front of the platform does not currently propagate `X-RateLimit-*` response headers. Build your rate-limit awareness around your own request count and the `429`/error response described below, not around per-response headers. ## Query Complexity Because GraphQL allows clients to request arbitrarily nested data in a single call, the API also evaluates **query complexity** to protect against expensive operations. Each field in a query has an associated cost. Deeply nested queries, or queries that request large collections of related objects, consume more complexity budget than basic field lookups. **How to keep complexity low:** - Request only the fields you need — avoid selecting every field on a type. - Limit the depth of nested relationships (e.g., avoid chaining `company → workers → contracts → invoices` in a single query). - Use pagination with reasonable page sizes (`first: 25` rather than `first: 100`). ```graphql # Good — focused query with limited depth query { hires(first: 25) { data { id worker { id name email } } paginatorInfo { hasMorePages } } } ``` ```graphql # Avoid — deeply nested query with excessive data query { hires(first: 100) { data { id worker { id name hires(first: 100) { data { latestContract { id files { id name url } } } } } } } } ``` > [!WARNING] > If a query exceeds the maximum allowed complexity, the API will reject it with an error before execution. Simplify your query or split it into multiple smaller requests. ## Handling Rate Limit Errors When you exceed the rate limit, the platform returns HTTP `429 Too Many Requests` with a `Retry-After` header. The federation gateway forwards this as a `DOWNSTREAM_SERVICE_ERROR` in the GraphQL `errors` array (HTTP 200 at the gateway, even though the underlying response was 429): ```json { "errors": [ { "message": "Too Many Requests", "extensions": { "serviceName": "platform", "code": "DOWNSTREAM_SERVICE_ERROR" } } ] } ``` Treat repeated `DOWNSTREAM_SERVICE_ERROR` responses with a "Too Many Requests" message as the rate-limit signal and back off. See [Error Handling](/graphql/guides/error-handling) for the full error envelope. ### Exponential Backoff The recommended strategy is to implement **exponential backoff** — wait progressively longer between retries to avoid flooding the API: ```javascript async function fetchWithRetry(query, variables, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { const response = await fetch('https://api.worksome.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_TOKEN', }, body: JSON.stringify({ query, variables }), }); const result = await response.json(); const isThrottled = result.errors?.some( (e) => e.extensions?.code === 'DOWNSTREAM_SERVICE_ERROR' && /too many requests/i.test(e.message), ); if (isThrottled) { const backoff = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s const jitter = Math.random() * 1000; await new Promise((resolve) => setTimeout(resolve, backoff + jitter)); continue; } return result; } throw new Error('Rate limit exceeded after retries'); } ``` > [!WARNING] > Retrying immediately or in a tight loop may result in extended throttling. Use exponential backoff with jitter. ## Best Practices - **Batch related data into fewer queries.** GraphQL lets you request multiple resources in a single call — use that to your advantage instead of making many small requests. - **Cache responses** where the data does not change frequently. Avoid polling the same query repeatedly when a webhook or less frequent interval would suffice. - **Paginate wisely.** Use Lighthouse pagination (`data { ... }` plus `paginatorInfo`) and stop iterating once `paginatorInfo.hasMorePages` is `false`. See [Pagination & Filtering](/graphql/guides/pagination). - **Request only needed fields.** Smaller responses are faster to process and reduce complexity costs. - **Spread requests over time.** If you have a batch job, distribute requests evenly across the rate limit window rather than bursting them all at once. ## Monitoring Your Usage Because the gateway does not currently expose rate-limit headers, track your usage on the client side. The pattern below counts successful calls per minute for a single token and slows down before hitting the documented limit: ```javascript const PER_MINUTE_LIMIT = 60; const window = []; // timestamps of recent successful calls function recordCall() { const now = Date.now(); while (window.length && now - window[0] > 60_000) window.shift(); window.push(now); } async function rateLimitedFetch(query, variables) { if (window.length >= PER_MINUTE_LIMIT - 5) { const wait = 60_000 - (Date.now() - window[0]); await new Promise((resolve) => setTimeout(resolve, wait)); } const response = await fetch('https://api.worksome.com/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_TOKEN', }, body: JSON.stringify({ query, variables }), }); const result = await response.json(); recordCall(); return result; } ``` > [!TIP] > Build rate limit awareness into your integration from the start. Proactively slowing down before you hit the limit is far better than reacting to throttle errors. --- ## /webhooks/index # Getting Started Webhooks are a way to enable real-time notifications to third-party systems, for various events within the Worksome platform. This feature facilitates synchronizing your systems with our platform and ensures immediate updates. When an event occurs in Worksome, we send a JSON payload with relevant data to the URL you provide. In order for you to receive this webhook, you must supply an endpoint URL to your system, and we will agree on a secret token, that we use to sign the request. > You can also manage webhooks programmatically using the [CLI](/integrations/cli): `worksome webhooks create`, `worksome webhooks list`, and `worksome webhooks delete`. ## Example We have an event called `contractAccepted` that triggers when a contract is accepted by a worker. You have supplied an endpoint URL of https://api.example.com/worksome-webhook to us, and we have agreed on a secret `exampleSecret`. We store this into the Worksome platform, along with information that you want to receive a webhook call when `contractAccepted` occurs. Playing out the example, a worker now accepts the contract. This is what happens next: - The Worksome platform immediately makes an HTTP POST request to https://api.example.com/worksome-webhook containing a payload with a minimal set of information, which identifies the contract, the worker and relevant custom fields, if any. The request is signed using the secret and the signature is placed in a `Signature` header of the request. The signature can be used to verify that it is in fact Worksome who sent the webhook. - Your system receives our request at your endpoint and responds with an HTTP 200 code. You should verify it using the signature and secret, and then take any action you need at your end, such as storing it in your database and triggering any events you need. We provide a minimum of information in the webhook itself, so an action on your part might include calling our API for more information that you might need. - In case your system _does not_ respond with an HTTP 2XX code, we will back off and retry the call later. We use an exponential backing off strategy. For a step-by-step implementation guide including signature verification and event routing, see [Handle Webhooks](/webhooks/guides/handle-webhooks). ### Payload example The payload could look something like this: ```json { "event": "contractAccepted", "data": { "contract": { "id": "Q29udHJhY3Q6MTIzNA==", "hireId": "SGlyZToxNTY4NTA=", "hireStatus": "ready", "startDate": "2023-11-20", "endDate": "2024-02-20" }, "worker": { "id": "V29ya2VyOjIzNDU2", "name": "Peter Bishop", "email": "peterbishop@example.com", "phone": "+16955024" }, "trustedContact": { "id": "VHJ1c3RlZENvbnRhY3Q6Nzg5", "accountId": "Q29tcGFueToxMjM=", "externalIdentifier": "d05fc13d-156e-4cc6-b325-a42e20f721c5", "status": "active" }, "customFieldValues": [ { "id": "Q3VzdG9tRmllbGQ6MTI=", "customFieldTitle": "Manager", "displayValue": "Olivia Dunham" }, { "id": "Q3VzdG9tRmllbGQ6Nw==", "customFieldTitle": "Client", "displayValue": "Massive Dynamic" } ] } } ``` Breaking down this payload, we see the `event` field, which contains the event type that triggered the webhook. Your system may subscribe to several different events, so it must look at this field first, to handle the payload `data` correctly. The `data` object contains the information relevant for the event, including hire status information within the contract object. For a `contractAccepted` event, we send basic information about the contract, the worker hired and custom fields tied to the hire. The [`contract`](/graphql/reference/objects#contract) object contains the `id` of the contract, the `hireId` which uniquely identifies the engagement (and remains the same even when contracts are revised), the `hireStatus` which provides the current status of the hire at the time the webhook was triggered, along with the start and end date. The `hireStatus` field is included in all hire-related webhook events and provides a consistent way to track hire status. Possible values are `draft`, `offered`, `ready`, `active`, `ended`, `cancelled`, and `terminated` — the same [`HireActiveStatus`](/graphql/reference/enums) enum the GraphQL API uses, which exposes it in upper case (`DRAFT`, `OFFERED`, and so on). Compare the two case-insensitively if you match webhook payloads against API responses. > **Important**: The `hireId` field provides a stable reference to the overall engagement. When contract details are revised, a new contract is created with a new `id`, but the `hireId` remains the same. Using the `hireId` as the primary reference in your integrations helps maintain continuity across contract changes. The [`worker`](/graphql/reference/objects#worker) object contains the `id` of the worker in the Worksome platform, along with the name and email of the worker. The `trustedContact` object contains the `id` of the trusted contact, the `accountId` of the associated company, the (optional) `externalIdentifier` which is an identifier for the trusted contact in your system, and the `status` of the invitation to the talent pool. Finally, there is an array of custom field value objects, each with the `id` of the related [`customField`](/graphql/reference/objects#customfield), the custom field title, and the display value. Note that the `id` references the `CustomField` definition (not a `CustomFieldValue` instance), which is the identifier you use when calling the GraphQL API to update or query custom fields. The `id` fields are the same as in the GraphQL API, so if you call the API for more information, these are the IDs you must use in your queries. --- ## /webhooks/guides/introduction # Introduction to Webhooks ## Webhook terminology You may not be familiar with the terminology of webhooks, so here we will introduce the relevant terms as they pertain to the Worksome platform. ## Webhook A webhook is an outgoing HTTP request to your system, containing a payload of interest. Such a request is called "sending a webhook" from the Worksome side and "receiving a webhook" from your side. Typically, a webhook is sent when a particular event happens within the sending application (in our case the Worksome platform). Such an event may be of interest to a third party (receiving) system (such as yours), which have a need to be updated quickly after the event happens in the sending system. A webhook is normally sent as a POST HTTP request, but could be another method. _Worksome webhooks are always sent as POST requests_. To be able to receive a webhook, you must create an endpoint for it in your system, and tell us the URL of it. ## Secret The secret is a token made from a string of characters. This is exchanged between you and Worksome, and only known by those two parties. The secret is stored on both yours and Worksome systems. It is used for signing the payload, such that you can verify that the webhook was sent from Worksome and nobody else. The longer the secret the more secure it is. We support secrets up to 255 characters long. ## Payload A webhook payload is usually in JSON format, but could be in another format such as XML. _Worksome webhooks are always in JSON format_. ## Headers A widely used way of authenticating a webhook is through the use of a `Signature` header. Worksome uses HMAC signatures, like GitHub, Stripe and Facebook. ## Signature The HMAC signature is created by taking the HMAC `sha256` hash of the payload with the secret used as the key. To verify the authenticity of the payload, you would take the received payload and apply the same method, using the secret, and compare your result with the contents of the `Signature` header. If they match, you know that Worksome was the sender. Here is an example in PHP: ```php $secret = 'exampleSecret'; $payload = [ 'event' => 'contractAccepted', 'data' => [ 'what' => [ 'id' => 42, ], ], ]; $payloadJson = json_encode($payload); $signature = hash_hmac('sha256', $payloadJson, $secret); // Signature is now: 2c25330460c6dd4af652b1c0714b5a98894aef94112b8f1e6dbd5f9830ddc766 ``` ## Responses When a webhook is received, the receiving system should respond with a 200 (or any other 2XX) HTTP code to signal that it was received without errors. If we receive any 2XX HTTP code in response to sending a webhook, we consider the webhook completely delivered. Typical non-2XX codes might be: - 401 if the Signature header did not match the payload. - 403 if Worksome is otherwise not authorized to send to your system. - 404 if your system does not recognize the endpoint URL that we try to deliver to. - 422 in case parts of the payload failed validation, such as a worker being unknown in your system. - 5XX if your server crashed or is otherwise unable to handle the request. This is usually temporary. We do not need a body in case of a 2XX code, but in any other case, we would appreciate a body in JSON format with an error message detailing the reason why it could not be received. This makes it easier for customer service to help you. ## Backing off and retrying If your system for any reason responds with other than 2XX, or the call takes longer than a timeout of 60 seconds, we start the "backing off" retry process to give your system more time to recover from whatever reason it could not receive it. The backing off process starts by waiting 10 seconds, then tries again for a second attempt. If your system still does not respond with 2XX, or times out, then we increase the waiting time exponentially, so the third try is 100 seconds after the second, the fourth try is 1000 seconds after the third and finally the fifth attempt is 10000 seconds after the fourth. If the fifth attempt still was not successful, we stop trying. We log every attempt and will be able to restart the webhook sending for a particular event that ended not being successfully sent and received. Contact customer service if you suspect that you are missing a webhook. ## Summary In summary: - An event that you have interest in, happens in the Worksome platform. - Worksome creates a payload with relevant information about the event. - Worksome signs the payload using the agreed secret. - Worksome makes an HTTP POST request to the agreed endpoint at your system, with a Signature header and the payload as a JSON formatted body. - You receive the request and verify the payload using the secret and comparing with the Signature header. - When verified, perform any validations to make sure the contents fit your system. - If all is well, respond with a 200 HTTP code and proceed with processing the payload in your system. This might involve making a callback to our API to obtain more information. - Otherwise, respond with any other (non 2XX) code and preferably a body in JSON format with an error message. - Worksome retries sending the webhook up to a total of 5 times if your system does not respond with a 2XX code. --- ## /webhooks/guides/handle-webhooks # Handle Webhooks This guide walks through implementing a webhook receiver for Worksome events. By the end, you will have a working endpoint that verifies webhook signatures, processes event payloads, and responds correctly. For an overview of webhook concepts (payloads, signatures, retries), see the [Introduction to Webhooks](/webhooks/guides/introduction). ## Setting up your endpoint Create an HTTP endpoint that accepts POST requests. Worksome sends all webhooks as `POST` requests with a JSON body and a `Signature` header. Your endpoint must: 1. Read the raw request body (do not parse it before verifying the signature). 2. Verify the HMAC signature using your shared secret. 3. Parse the JSON payload and route it by event type. 4. Respond with a `2XX` status code to acknowledge receipt. ## Verifying the signature Every webhook includes a `Signature` header containing an HMAC-SHA256 hash of the raw request body, signed with your shared secret. Always verify this before processing the payload. ### PHP ```php function verifyWebhookSignature(string $payload, string $signature, string $secret): bool { $expected = hash_hmac('sha256', $payload, $secret); return hash_equals($expected, $signature); } // In your controller: $payload = file_get_contents('php://input'); $signature = $_SERVER['HTTP_SIGNATURE'] ?? ''; $secret = getenv('WORKSOME_WEBHOOK_SECRET'); if (! verifyWebhookSignature($payload, $signature, $secret)) { http_response_code(401); echo json_encode(['error' => 'Invalid signature']); exit; } $event = json_decode($payload, true); ``` ### Node.js ```javascript const crypto = require('crypto'); function verifyWebhookSignature(payload, signature, secret) { if (typeof signature !== 'string' || signature.length === 0) { return false; } const expected = crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); const expectedBuffer = Buffer.from(expected); const receivedBuffer = Buffer.from(signature); // timingSafeEqual throws on length mismatch — guard explicitly so the // handler fails closed instead of crashing on a malformed signature. if (expectedBuffer.length !== receivedBuffer.length) { return false; } return crypto.timingSafeEqual(expectedBuffer, receivedBuffer); } // In your Express handler: app.post('/webhooks/worksome', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['signature']; const secret = process.env.WORKSOME_WEBHOOK_SECRET; // express.raw() gives a Buffer. Decode it once and verify the signature // against the exact bytes received, before parsing anything. const payload = req.body.toString('utf8'); if (!verifyWebhookSignature(payload, signature, secret)) { return res.status(401).json({ error: 'Invalid signature' }); } const event = JSON.parse(payload); // Process the event... res.sendStatus(200); }); ``` ### Python ```python import hmac import hashlib def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool: expected = hmac.new( secret.encode(), payload, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature) ``` > [!WARNING] > Always use constant-time comparison functions (`hash_equals` in PHP, `timingSafeEqual` in Node.js, `compare_digest` in Python) to prevent timing attacks. ## Routing events After verifying the signature, parse the JSON and route based on the `event` field. ```php $event = json_decode($payload, true); switch ($event['event']) { case 'contractAccepted': handleContractAccepted($event['data']); break; case 'hireUpdated': handleHireUpdated($event['data']); break; case 'hireCancelled': handleHireCancelled($event['data']); break; case 'hireEnded': handleHireEnded($event['data']); break; case 'hireTerminated': handleHireTerminated($event['data']); break; case 'trustedContactUpdated': handleTrustedContactUpdated($event['data']); break; default: // Log unknown events for monitoring error_log("Unknown webhook event: " . $event['event']); break; } ``` > [!TIP] > Always return `200` even for unknown event types. This prevents Worksome from retrying events your system does not handle and allows us to add new event types without breaking your integration. ## Processing the payload Each event's `data` object contains the relevant entities. For example, the `contractAccepted` event includes the `contract`, `worker`, and `trustedContact` objects, plus a `customFieldValues` array. ```php function handleContractAccepted(array $data): void { $contract = $data['contract']; $worker = $data['worker']; // Sync the contract to your system $yourContract = YourContract::updateOrCreate( ['worksome_id' => $contract['id']], [ 'job_title' => $contract['jobName'], 'start_date' => $contract['startDate'], 'end_date' => $contract['endDate'], 'rate' => $contract['rate'], 'currency' => $contract['currency'], 'status' => $contract['hireStatus'], ] ); // Sync worker details $yourWorker = YourWorker::updateOrCreate( ['worksome_id' => $worker['id']], [ 'name' => $worker['firstName'] . ' ' . $worker['lastName'], 'email' => $worker['email'], ] ); } ``` See the [Event Reference](/webhooks/reference) for full payload schemas and field descriptions for each event type. ## Responding correctly Worksome expects a `2XX` response to confirm delivery. Any other status triggers the retry process. | Response | Worksome behavior | |---|---| | `200`–`299` | Delivery confirmed. No retries. | | `401` | Signature mismatch. Retries will follow. | | `4XX` (other) | Client error. Retries will follow. | | `5XX` | Server error. Retries will follow. | | Timeout (>60s) | Treated as failure. Retries will follow. | > [!TIP] > Respond quickly — ideally within a few seconds. If your processing takes longer, acknowledge the webhook with `200` immediately and process it asynchronously (e.g., via a queue). ## Handling retries If your endpoint fails to respond with `2XX`, Worksome retries with exponential backoff: | Attempt | Delay after previous | |---|---| | 2nd | 10 seconds | | 3rd | 100 seconds | | 4th | 1,000 seconds (~17 minutes) | | 5th | 10,000 seconds (~2.8 hours) | After 5 failed attempts, Worksome stops retrying. Contact support if you suspect missed webhooks. **Idempotency:** Your handler should be idempotent — processing the same webhook twice should produce the same result. Use the entity IDs in the payload to detect duplicates. ## Security best practices - **Verify every request.** Never process a webhook without checking the signature first. - **Use HTTPS.** Your endpoint URL should always use HTTPS to protect the payload in transit. - **Keep your secret safe.** Store the webhook secret in environment variables, not in source code. - **Rotate secrets periodically.** Coordinate with Worksome support to rotate your webhook secret on a schedule. - **Restrict access.** If possible, allowlist Worksome's IP ranges at the network level. ## Testing locally During development, use a tunneling service to expose your local server to the internet: ```shell # Using ngrok ngrok http 8080 # Then provide the ngrok URL to Worksome as your webhook endpoint: # https://abc123.ngrok.io/webhooks/worksome ``` > [!WARNING] > Local tunneling tools are for development only. Always use a production-grade endpoint for live integrations. ## Complete example (Laravel) ```php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Http\Response; class WorksomeWebhookController extends Controller { public function handle(Request $request): Response { $payload = $request->getContent(); $signature = $request->header('Signature', ''); $secret = config('services.worksome.webhook_secret'); // 1. Verify signature $expected = hash_hmac('sha256', $payload, $secret); if (! hash_equals($expected, $signature)) { return response()->json(['error' => 'Invalid signature'], 401); } // 2. Parse and route $event = json_decode($payload, true); match ($event['event']) { 'contractAccepted' => $this->onContractAccepted($event['data']), 'hireUpdated' => $this->onHireUpdated($event['data']), 'hireCancelled' => $this->onHireCancelled($event['data']), 'hireEnded' => $this->onHireEnded($event['data']), 'hireTerminated' => $this->onHireTerminated($event['data']), 'trustedContactUpdated' => $this->onTrustedContactUpdated($event['data']), default => logger()->info("Unhandled webhook: {$event['event']}"), }; // 3. Acknowledge return response()->noContent(); } private function onContractAccepted(array $data): void { // Example: sync the new contractor to your HRIS $worker = $data['worker']; $contract = $data['contract']; $trustedContact = $data['trustedContact']; YourHRIS::createContractorRecord([ 'external_id' => $trustedContact['externalIdentifier'] ?? $contract['id'], 'name' => $worker['firstName'] . ' ' . $worker['lastName'], 'email' => $worker['email'], 'job_title' => $contract['jobName'], 'start_date' => $contract['startDate'], 'end_date' => $contract['endDate'], 'rate' => $contract['rate'], 'currency' => $contract['currency'], ]); } private function onHireUpdated(array $data): void { // Process hire update... } private function onHireCancelled(array $data): void { // Process hire cancellation... } private function onHireEnded(array $data): void { // Process hire end... } private function onHireTerminated(array $data): void { // Example: close the contractor assignment in your HRIS $contract = $data['contract']; $externalId = $data['trustedContact']['externalIdentifier'] ?? $contract['id']; YourHRIS::closeAssignment($externalId, [ 'termination_date' => $contract['endDate'], 'reason' => $data['terminatedReason'], ]); } private function onTrustedContactUpdated(array $data): void { // Process trusted contact update... } } ``` Remember to register the route without CSRF protection, since webhooks come from an external source: ```php // routes/api.php Route::post('/webhooks/worksome', [WorksomeWebhookController::class, 'handle']); ``` ## Real-world integration patterns Webhooks are the backbone of real-time integrations with Worksome. Here are the most common patterns: ### HRIS sync (contract accepted → create contractor record) The most common webhook integration is pushing contractor records into an HR system when a contract is accepted. This eliminates manual data entry and ensures your workforce management system reflects current engagements. **What to sync on `contractAccepted`:** - Worker name, email, and contact details from the `worker` and `trustedContact` objects - Contract terms (job title, start/end dates, rate, currency) from the `contract` object - Your internal reference from `trustedContact.externalIdentifier` (if you set one when creating the hire) **What to sync on `hireTerminated` or `hireEnded`:** - Close the assignment record in your HRIS - Update the termination/end date **What to sync on `hireUpdated`:** - Detect contract extensions (end date changes) - Detect rate changes for updated contracts ### Compliance and document workflows Use `contractAccepted` to trigger compliance workflows — for example, sending an NDA or background check request as soon as a contractor signs. The webhook payload includes the worker's details, so you can pre-fill document templates automatically. ### Finance system reconciliation While invoices don't have a dedicated webhook, you can combine the `hireUpdated` event with periodic API polling to keep your finance system up to date — query the `invoices` field on the API for recent records. --- ## /webhooks/reference/index # Webhook Events Worksome offers a selection of webhooks intended to allow you to keep your systems in sync with your data in the Worksome platform. We intentionally provide a minimum of information in the webhook payloads themselves. If you need more, we refer to calling the [GraphQL Public API](/graphql) after you receive the webhook. The payload includes IDs for objects that can be looked up in the Public API. These are the events currently available: | Webhook Event Name | Description | |-------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------| | [Contract Accepted](/webhooks/reference/events/contract-accepted) | Triggered when a worker has accepted a contract. | | [Hire Cancelled](/webhooks/reference/events/hire-cancelled) | Triggered when the job/hire for a worker is cancelled. | | [Hire Ended](/webhooks/reference/events/hire-ended) | Triggered when the job/hire for a worker is ended. | | [Hire Terminated](/webhooks/reference/events/hire-terminated) | Triggered whenever a hire is terminated. | | [Hire Updated](/webhooks/reference/events/hire-updated) | Triggered whenever a hire or its related objects are updated. | | [Trusted Contact Updated](/webhooks/reference/events/trusted-contact-updated) | Triggered when a trusted contact is updated. This includes things like contact information, skills etc. | --- ## /webhooks/reference/events/contract-accepted # Contract Accepted This event is triggered when a worker has accepted a contract. The payload includes a selection of objects with fields that allow you to identify the contract, the worker and relevant custom fields, so that you can store this in your own system. ## Webhook payload The payload contains the following fields and objects: ```json { "event": "contractAccepted", "data": { "contract": {}, "worker": {}, "trustedContact": {}, "customFieldValues": [] } } ``` ### Event Identifier The `event` field contains `contractAccepted` as the identifier for this webhook. ### Event Data The `data` object contains the `contract`, `worker`, and `trustedContact` objects, plus the `customFieldValues` array for the event. The hire status information is included in the `contract` object as the `hireStatus` field, which contains the current status of the hire. For this event, it will typically be `"ready"` (when the contract is accepted but the contract period hasn't started yet) or `"active"` (when the contract period has already started). This field provides a consistent way to track hire status across all hire-related webhook events. ## Payload example ```json { "event": "contractAccepted", "data": { @include('webhooks/reference/payloads/examples/contract') @include('webhooks/reference/payloads/examples/worker') @include('webhooks/reference/payloads/examples/trusted-contact') @include('webhooks/reference/payloads/examples/custom-field-value') } } ``` --- ## /webhooks/reference/events/hire-cancelled # Hire Cancelled This event is triggered when the job/hire for a worker is cancelled. The payload includes a selection of objects with fields that allow you to identify the contract and the worker, so that you can update their status in your own system. ## Webhook payload The payload contains the following fields and objects: ```json { "event": "hireCancelled", "data": { "contract": {}, "worker": {}, "trustedContact": {}, "customFieldValues": [], "cancelReason": {} } } ``` ### Event Identifier The `event` field contains `hireCancelled` as the identifier for this webhook. ### Event Data The `data` object contains the `contract`, `worker`, `trustedContact`, and `cancelReason` objects, plus the `customFieldValues` array for the event. The hire status information is included in the `contract` object as the `hireStatus` field, which contains the current status of the hire. For this event, it will always be `"cancelled"`. This field provides a consistent way to track hire status across all hire-related webhook events. ### Cancel Reason The `cancelReason` object contains details about why the hire was cancelled. | `CancelReason` fields | |:----------------------------------------------------------------------------| | `reason` always contains the string `custom` for cancelled hires. | | `reason_message` contains the cancellation message provided by the user. | ## Payload example ```json { "event": "hireCancelled", "data": { @include('webhooks/reference/payloads/examples/contract') @include('webhooks/reference/payloads/examples/worker') @include('webhooks/reference/payloads/examples/trusted-contact') @include('webhooks/reference/payloads/examples/custom-field-value') "cancelReason": { "reason": "custom", "reason_message": "Project requirements have changed significantly." } } } ``` --- ## /webhooks/reference/events/hire-ended # Hire Ended This event triggers when a hire comes to a natural end (i.e., reaches its planned end date). The payload includes a selection of objects with fields that allow you to identify the contract and the worker, so that you can update their status in your own system. ## Webhook payload The payload contains the following fields and objects: ```json { "event": "hireEnded", "data": { "contract": {}, "worker": {}, "trustedContact": {}, "customFieldValues": [] } } ``` ### Event Identifier The `event` field contains `hireEnded` as the identifier for this webhook. ### Event Data The `data` object contains the `contract`, `worker`, and `trustedContact` objects, plus the `customFieldValues` array for the event. The hire status information is included in the `contract` object as the `hireStatus` field, which contains the current status of the hire. For this event, it will always be `"ended"`. This field provides a consistent way to track hire status across all hire-related webhook events. ## Payload example ```json { "event": "hireEnded", "data": { @include('webhooks/reference/payloads/examples/contract') @include('webhooks/reference/payloads/examples/worker') @include('webhooks/reference/payloads/examples/trusted-contact') @include('webhooks/reference/payloads/examples/custom-field-value') } } ``` --- ## /webhooks/reference/events/hire-terminated # Hire Terminated This event triggers when a hire is terminated early (i.e., before its planned end date). This webhook provides detailed information about the terminated contract, including the reason for termination as provided by the client. This webhook is triggered in addition to the `hireUpdated` webhook. The payload includes a selection of objects with fields that allow you to identify the contract and the worker, so that you can update their status in your own system. ## Webhook payload The payload contains the following fields and objects: ```json { "event": "hireTerminated", "data": { "contract": {}, "worker": {}, "trustedContact": {}, "customFieldValues": [], "terminatedReason": "mutual_agreement_to_terminate" } } ``` ### Event Identifier The `event` field contains `hireTerminated` as the identifier for this webhook. ### Event Data The `data` object contains the `contract`, `worker`, `trustedContact`, and `terminatedReason` objects, plus the `customFieldValues` array for the event. The hire status information is included in the `contract` object as the `hireStatus` field, which contains the current status of the hire. For this event, it will always be `"terminated"`. This field provides a consistent way to track hire status across all hire-related webhook events. ### Terminated Reason The top-level `terminatedReason` field on `data` contains the reason for early termination. Possible values are `worker_unavailability`, `project_completed_early`, `mutual_agreement_to_terminate`, `budget_constraints`, `change_in_project_scope`, `performance_issues`, `communication_issues`, `personal_reasons`, `legal_or_compliance_issues`, `violation_of_contract_terms`, `unforeseen_circumstances`, `dissatisfaction_with_quality_of_work`, `conflict_of_interest`, and `other`. ## Payload example ```json { "event": "hireTerminated", "data": { @include('webhooks/reference/payloads/examples/contract') @include('webhooks/reference/payloads/examples/worker') @include('webhooks/reference/payloads/examples/trusted-contact') @include('webhooks/reference/payloads/examples/custom-field-value') "terminatedReason": "mutual_agreement_to_terminate" } } ``` --- ## /webhooks/reference/events/hire-updated # Hire Updated This event has a general purpose and triggers whenever a hire or its related objects are updated. The following changes on a hire and related objects will trigger the event: - The job offer is accepted by the worker - A contract is accepted by the worker - The contract type on the hire is changed - The hire is terminated - A staffing agency is attributed to the hire - The hire changes status from "draft" to "active" - The worker information was updated (see [Trusted Contact Updated](/webhooks/reference/events/trusted-contact-updated)) - Custom field values on the related job were added or updated ## Webhook payload The payload contains the following fields and objects: ```json { "event": "hireUpdated", "data": { "contract": {}, "worker": {}, "trustedContact": {}, "customFieldValues": [] } } ``` ### Event Identifier The `event` field contains `hireUpdated` as the identifier for this webhook. ### Event Data The `data` object contains the `contract`, `worker`, and `trustedContact` objects, plus the `customFieldValues` array for the event. The hire status information is included in the `contract` object as the `hireStatus` field, which contains the current status of the hire at the time of the update. This field provides a consistent way to track hire status across all hire-related webhook events. ## Payload example ```json { "event": "hireUpdated", "data": { @include('webhooks/reference/payloads/examples/contract') @include('webhooks/reference/payloads/examples/worker') @include('webhooks/reference/payloads/examples/trusted-contact') @include('webhooks/reference/payloads/examples/custom-field-value') } } ``` --- ## /webhooks/reference/events/trusted-contact-updated # Trusted Contact Updated This event is triggered when a trusted contact is updated. This includes things like contact information, skills etc. The payload includes a selection of objects with fields that allow you to identify the worker and trusted contact relationship. To update your system when this event happens, you should make an API call to fetch the information you need. The payload does explicitly not include any information about _what_ was changed about the trusted contact. ## Webhook payload The payload contains the following fields and objects: ```json { "event": "trustedContactUpdated", "data": { "trustedContact": {}, "worker": {} } } ``` ### Event Identifier The `event` field contains `trustedContactUpdated` as the identifier for this webhook. ### Event Data The `data` object contains the `trustedContact` and `worker` objects for the event. ## Payload example ```json { "event": "trustedContactUpdated", "data": { @include('webhooks/reference/payloads/examples/trusted-contact') @include('webhooks/reference/payloads/examples/worker') } } ``` --- ## /webhooks/reference/payloads/contract-fields #### Contract Object The `contract` object has fields identifying the contract and the most important details. Fields named here are always present. | `Contract` fields | |:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `id` is the ID that refers to the contract in the Worksome platform, and you can [query the API](/graphql/reference/objects/#contract) for the full details using this ID. | | `hireId` is the ID that refers to the hire (or engagement) in the Worksome platform. This ID remains constant even when contract details are revised or updated, providing a stable reference for the overall engagement. | | `accountId` is the ID that refers to the company account in the Worksome platform, and you can [query the API](/graphql/reference/objects/#company) for the full details using this ID. | | `hireStatus` is the current status of the hire at the time the webhook was triggered. Possible values are `draft` (hire has pending approval requests), `offered` (hire is not yet accepted by freelancer), `ready` (hire is accepted but contract period hasn't started), `active` (contract period has started and hire is ongoing), `ended` (hire has completed naturally), `cancelled` (hire was cancelled), and `terminated` (hire was terminated early). This field provides a consistent way to track hire status across all hire-related webhook events. | | `startDate` is the date (YYYY-MM-DD) that the contract starts or started. This field can be `null` if a start date is not known. The field has no time part. | | `endDate` is the date (YYYY-MM-DD) that the contract ends or ended. This field can be `null` if the end date is not known. The field has no time part. | | `workerAcceptedAt` is the time (`YYYY-MM-DD HH:mm:ss`) for when the contract is accepted by the hired worker. This field is `null` until the contract is accepted. | | `currency` is the currency value for the rate on the contract. The values follow [ISO 4217 format](https://www.iso.org/iso-4217-currency-codes.html). | | `rate` is the offered pay rate for the worker on the contract. | | `rateType` defines the payment frequency that applies to the rate. Values are `hourly`, `daily`, `weekly`, `monthly`, `fixed`, or `unknown`. | | `status` is the lifecycle status of the contract itself. Possible values are `draft`, `active`, and `archived`. (Distinct from `hireStatus`, which describes the engagement.) | | `owners` is a list of users that are assigned as owners on the related job, each with `name` and `email`. This list will be empty until a job owner is added. | | `jobOwners` is the legacy alias for `owners` and is kept for backwards compatibility. Prefer `owners` going forward. | | `jobId` is the ID that refers to the job in the Worksome platform, and you can [query the API](/graphql/reference/objects/#job) for the full details using this ID. | | `jobName` is name of the job as agreed on for the contract. Note that this can be different from the related original job post. | | `jobDescription` is the full job description and scope for the contract. Note that this can be different from the related original job post. | | `purchaseOrderNumber` is purchase order number (PO) that will be applied to all invoices from the hire. | | `locationPreference` is the location preference of the contract. Possible values are `onsite-only`, `onsite-some`, or `remote-only`. | --- ## /webhooks/reference/payloads/custom-field-value-fields #### Custom Field Value Object The `customFieldValue` object has fields for the custom field values. | `CustomFieldValue` object fields | | :-------------------------------------------------------------------------------------------------------------------- | | `id` is the ID of the [custom field](/graphql/reference/objects/#customfield) the value is for. Use this ID when calling the GraphQL API to read or update custom-field values for the entity (it identifies the field definition, not a specific value instance). | | `slug` is a technical identifier for the field, based on the field title. This may be `null` if no slug has been set. | | `customFieldTitle` is the title of the [custom field](/graphql/reference/objects/#customfield). | | `displayValue` is the human-readable representation of the custom field value chosen for this event. | The array is always present, but may be empty. Objects in the array always have the named fields. --- ## /webhooks/reference/payloads/trusted-contact-fields #### Trusted Contact Object The `trustedContact` object has fields with the worker's data in relation to the client. Fields named here are always present. | `Trusted Contact` fields | | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` is the ID that refers to the trusted contact in the Worksome platform, and you can [query the API](/graphql/reference/objects/#trustedcontact) for the full details using this ID. | | `accountId` is the ID that refers to the company account in the Worksome platform, and you can [query the API](/graphql/reference/objects/#company) for the full details using this ID. | | `externalIdentifier` is an identifier associated with the [trusted contact](/graphql/reference/objects/#trustedcontact) from an external system. This may be `null` if no external identifier is defined. | | `status` is the status of the invitation to the talent pool. Possible values are `invited`, `active`, `declined`, `applied`, and `application_declined`. | --- ## /webhooks/reference/payloads/worker-fields #### Worker Object The `worker` object has fields identifying the worker and most important details. Fields named here are always present. | `Worker` fields | | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` is the ID that refers to the worker in the platform. You can [query the API](/graphql/reference/objects/#worker) for the full details using this ID. | | `name` is the full name of the worker. | | `firstName` is the first name of the worker. | | `lastName` is the last name of the worker. | | `middleName` is the middle name of the worker. | | `email` is the email for the worker. | | `phone` is the phone number of the worker. | | `location` is the location of the Worker as an object. The object includes `name`, `address`, `city`, `zipcode`, `country`, `county`, `state`. | | `businessEntity` is details for the Worker's business entity as an object. The object includes `entity_type`, `label`, `company_no`, `company_name` and `tax_no`. | --- ## /integrations/index # Integration Options Worksome offers multiple ways to integrate with the platform, from no-code automation to full programmatic API access. Choose the method that fits your technical resources and use case. ## Choosing an integration method | Method | Best for | Technical level | API coverage | Auth method | |---|---|---|---|---| | [GraphQL API](/graphql) | Full programmatic access, custom integrations | Developer | Full | OAuth 2.0 or PAT | | [Webhooks](/webhooks) | Real-time event notifications when data changes | Developer | Outbound events only | Shared secret (HMAC) | | [CLI](/integrations/cli) | Command-line access, scripting, AI agent tooling | Developer / AI agent | Broad (170+ operations) | PAT | | [PHP SDK](/integrations/php-sdk) | PHP applications with convenient helpers | PHP developer | Partial (wrapper over GraphQL) | PAT | | [Zapier](/integrations/zapier) | No-code automation connecting Worksome to 6,000+ apps | Non-technical | Limited | OAuth 2.0 | | [MCP Server](/integrations/mcp-server) | AI assistant integration (Claude, ChatGPT, custom LLMs) | AI agent | Early access — request access | OAuth 2.0 or PAT | | [AI-Ready Docs](/integrations/ai-agents) | Feed documentation directly into LLMs and AI agents | AI agent / Developer | Documentation only | None | ## Quick start by use case ### "I want to sync Worksome data with another tool" If the other tool is on Zapier, start with [Zapier](/integrations/zapier). If not, or if you need more control, use the [GraphQL API](/graphql) with [Webhooks](/webhooks) for real-time updates. ### "I want to build a custom integration" Use the [GraphQL API](/graphql) directly. Start with [Authentication](/authentication) to get a Personal Access Token, then follow the [Getting Started guide](/graphql). ### "I want to automate workflows with scripts" The [CLI](/integrations/cli) provides full API access from the command line. It supports JSON output, piping, and is designed for scripting and automation. ### "I want to connect an AI agent to Worksome" Three options are available depending on what your agent needs: - **[MCP Server](/integrations/mcp-server)** — Purpose-built for AI assistants like Claude and ChatGPT. Provides structured tools for querying and mutating Worksome data via natural language. - **[CLI](/integrations/cli)** — Full API coverage with JSON output mode, designed for AI agent tooling and scripting. - **[AI-Ready Documentation](/integrations/ai-agents)** — Machine-readable documentation files (`llms.txt` and `llms-full.txt`) that can be fed directly into an LLM's context window for grounded answers about the Worksome API. ### "I have a PHP application" The [PHP SDK](/integrations/php-sdk) provides a convenient wrapper over the GraphQL API with helper methods for common operations. ## Integration methods ### GraphQL API The core of all Worksome integrations. A single endpoint (`https://api.worksome.com/graphql`) for querying and mutating data — hires, contracts, invoices, workers, compliance, and more. See [Getting Started with GraphQL](/graphql). ### Webhooks Receive real-time HTTP callbacks when events occur — contracts accepted, hires updated, invoices created. See [Getting Started with Webhooks](/webhooks). ### CLI A Go-based command-line tool with broad API coverage (60+ resource groups, 170+ operations). Supports JSON output, piping, dry-run mode, and multiple authentication profiles. See [CLI](/integrations/cli). ### PHP SDK An open-source PHP package for the Worksome API with helper methods for authentication, viewer info, and raw GraphQL queries. See [PHP SDK](/integrations/php-sdk). ### Zapier No-code automation connecting Worksome to Slack, Google Sheets, HubSpot and thousands more. The available triggers and actions are listed on the Zapier app page. See [Zapier](/integrations/zapier). ### MCP Server A Model Context Protocol server that lets AI assistants interact with Worksome through natural language. Tools map one-to-one to public GraphQL API operations and cover hires, jobs, candidates, payments, invoices, timesheets, contracts, approvals, webhooks, and more. See [MCP Server](/integrations/mcp-server). ### AI-Ready Documentation Machine-readable documentation files designed for LLMs and AI agents. These follow the emerging `llms.txt` convention and can be loaded directly into an AI agent's context window. See [AI Agents](/integrations/ai-agents). ## What can you integrate? | Data | Read | Write | Webhooks | |---|---|---|---| | Hires | Query hires by status, company, worker | Create draft hires | Contract accepted, hire updated, cancelled, ended, terminated | | Contracts | Query contracts by status, currency | Created via draft hire flow | Contract accepted | | Workers | Query workers through hires | — | Trusted contact updated | | Invoices | Query invoices by status, date, currency | — | — | | Jobs | Query jobs | Create jobs (API, Zapier) | New job (Zapier) | | Compliance | Query compliance status, gates, classification | — | — | ## Need help? If you're unsure which integration method fits your needs, contact your Worksome account representative. For technical support, visit the [Support](/support) page. --- ## /integrations/php-sdk # PHP SDK We've created an open source PHP package that allows you to communicate with [the Worksome API](/graphql). It provides some convenient methods to get started. ## Getting started We offer [a PHP SDK package](https://github.com/worksome/sdk-php) that is ready for use. It's open source, and we accept contributions to it. > **Note:** The PHP SDK for Worksome requires PHP 8.4 or later. To install via Composer, you'll need a [PSR-17](https://packagist.org/providers/psr/http-factory-implementation) and [PSR-18](https://packagist.org/providers/psr/http-client-implementation) implementation. We suggest using Guzzle. ```shell composer require worksome/sdk guzzlehttp/guzzle:^7.5 http-interop/http-factory-guzzle:^1.2 ``` Next, create an instance of the SDK, and authenticate. This takes your API token as a parameter for the `authenticate()` method. If you don't have an API token yet, you can [read up on how to create one first](/authentication). ```php $apiToken = '...'; $worksome = new \Worksome\Sdk\Client(); $worksome->authenticate($apiToken); ``` Once you have initialised the SDK, you can call any of the methods, or directly call the Worksome API. ## User Info You can retrieve your authenticated users' global id using the convenient `viewer()` helpers. Behind the scenes, this uses the [`viewer` query](/graphql/reference/queries#viewer). ```php $viewerId = $worksome->viewer()->id(); ``` You can also retrieve a list of the accounts that your API token has access to. ```php $accounts = $worksome->viewer()->accounts(); ``` This will return an array of arrays with an `id` and a `name` value. ```php foreach ($accounts as $account) { var_dump($account['id']); // The global id of the account var_dump($account['name']); // The name of the account } ``` ## GraphQL You can also call any GraphQL queries or mutations through the `graphql()` helpers. For example, to retrieve the same id as in the `viewer()->id()` call, you could run the following: ```php $result = $worksome->graphql()->execute( <<<'GQL' { viewer { id } } GQL ); ``` The SDK also supports passing variables for the GraphQL query via the second `$variables` parameter. For example: ```php $result = $worksome->graphql()->execute('...', [ 'id' => '...', ]); ``` The SDK also supports loading a GraphQL query from a file. This is useful for calling long queries. You can do this using the following: ```php $result = $worksome->graphql()->fromFile(__DIR__.'/custom-query.graphql'); ``` ## Other integrations - [GraphQL API](/graphql) — Full programmatic access for custom integrations. - [Webhooks](/webhooks) — Real-time event notifications. - [CLI](/integrations/cli) — Command-line interface with broad API coverage. - [Zapier](/integrations/zapier) — No-code automation with triggers and actions. - [MCP Server](/integrations/mcp-server) — AI agent integration via Model Context Protocol. - [AI Agents](/integrations/ai-agents) — AI-ready documentation and agent integration guide. --- ## /integrations/cli # CLI The Worksome CLI is a command-line tool for interacting with the Worksome GraphQL API. It provides broad API coverage across 60+ resource groups and 170+ operations, and is designed for both human users and AI agents. ## Installation Pre-built binaries for macOS, Linux, and Windows are attached to each [release](https://github.com/worksome/worksome-cli/releases). While the repository is private, downloading them requires repository access. Install Go 1.26+ if you want to build from source. ```shell # Build from source (requires Go 1.26+ and access to the worksome-cli repo) go install github.com/worksome/worksome-cli/cmd/worksome@latest ``` > [!WARNING] > The `worksome-cli` repository is currently private. External integrators should request access from their Worksome contact, or contact [Support](/support). Run `worksome version` to check the installed version. ## Authentication The CLI uses **Personal Access Tokens** (PATs). See [Authentication](/authentication) for how to create one. Token resolution order: 1. `--token` flag (highest priority) 2. `WORKSOME_API_TOKEN` environment variable 3. Config file (`~/.worksome/config.yaml`) The endpoint and profile resolve the same way, via `--endpoint`/`WORKSOME_ENDPOINT` and `--profile`/`WORKSOME_PROFILE`. ### Setting up authentication ```shell # Interactive setup — saves token to config file (under the `default` profile unless --profile is given) worksome auth login # Check current auth status worksome auth status # List configured profiles worksome auth list # Switch to another profile you have created worksome auth switch stage # Remove a profile and its stored credentials worksome auth logout stage ``` ### Multiple profiles Use `--profile` to manage different accounts or environments. Profile names are arbitrary — create the ones you need with `worksome auth login --profile `. ```shell worksome --profile stage hires list worksome --profile default hires list ``` ## Usage The CLI uses a `resource action` pattern: ```shell worksome [flags] ``` ### Querying data ```shell # Get a single resource worksome hires get # List resources (paginated by default) worksome hires list # Fetch all pages worksome hires list --all # Custom page size worksome hires list --first 50 # Specific page worksome hires list --page 3 # Re-run and refresh the list every 5 seconds worksome hires list --watch # Filter and search worksome hires list --active-status ACTIVE --search "john" ``` ### Creating and modifying resources ```shell # Create via flags worksome jobs create --company --name "Backend Engineer" # Create via JSON file (use `-` to read from stdin) worksome jobs create --input job.json # Mix both (flags override file values) worksome hires terminate --input base.json --reason "PROJECT_COMPLETED_EARLY" # Preview the operation and variables without executing worksome jobs create --company --name "Test" --dry-run ``` ### Output formats The CLI auto-detects your terminal. When output is piped, it defaults to JSON; in an interactive terminal, it uses human-friendly formatting. ```shell # Force JSON output worksome hires list --output json # Force table output worksome hires list --output table # Pipe-friendly worksome hires list --output json | jq '.[] | .id' ``` ## Available resources The CLI covers the full Worksome API. Some commonly used resource groups: | Resource | Examples | |---|---| | `hires` | `list`, `get`, `create-draft`, `terminate`, `cancel` | | `jobs` | `list`, `get`, `create`, `update` | | `contracts` | `list`, `get` | | `invoices` | `list`, `get` | | `worker` | `get`, `update` | | `projects` | `list`, `get`, `create`, `update` | | `timesheets` | `list`, `get` | | `webhooks` | `list`, `get`, `create`, `delete` | | `approvals` | `list`, `get`, `create`, `update` | Run `worksome --help` for the full list, or `worksome --help` for available actions on a resource. ## Shell completion ```shell # Bash source <(worksome completion bash) # Zsh source <(worksome completion zsh) # Fish worksome completion fish | source # PowerShell worksome completion powershell | Out-String | Invoke-Expression ``` ## Global flags | Flag | Description | |---|---| | `--token`, `-t` | API token (overrides config and environment) | | `--endpoint` | Custom API endpoint URL | | `--profile`, `-p` | Config profile name | | `--output`, `-o` | Output format: `json` or `table` | | `--columns` | Comma-separated list of columns to display (table output) | | `--fields` | Comma-separated list of fields to include (e.g., `id,name,worker.name`) | | `--filter` | Key=value filter pairs on list commands (e.g., `"status=ACTIVE,currency=DKK"`) | | `--timeout` | Request timeout in seconds (default 30) | | `--verbose`, `-v` | Show request and response details | | `--no-color` | Disable colored output | | `--dry-run` | Preview the operation and variables without executing | ## Support For issues with the CLI or to request features, contact [Support](/support). ## Other integrations - [GraphQL API](/graphql) — Full programmatic access for custom integrations. - [Webhooks](/webhooks) — Real-time event notifications. - [PHP SDK](/integrations/php-sdk) — Official PHP SDK for the Worksome API. - [Zapier](/integrations/zapier) — No-code automation with triggers and actions. - [MCP Server](/integrations/mcp-server) — AI agent integration via Model Context Protocol. - [AI Agents](/integrations/ai-agents) — AI-ready documentation and agent integration guide. --- ## /integrations/zapier # Zapier The [official Worksome Zapier integration](https://zapier.com/apps/worksome/integrations) lets you connect Worksome to thousands of other apps, with no code required. Typical Zaps sync new hires into an HRIS, notify a Slack channel when a contract is accepted, or publish a job from an approved requisition in your ATS. Zapier hosts the reference for the integration itself. The available triggers and actions, and the fields each one exposes, are listed on the [Worksome app page](https://zapier.com/apps/worksome/integrations) and always reflect what the integration currently supports, so start there when building a Zap. You authorize the connection with your Worksome account when Zapier prompts for it. > [!NOTE] > Zapier triggers poll for new data rather than receiving it as it happens. If your workflow needs immediate delivery, use [Webhooks](/webhooks) instead. ## Support For help with the Zapier integration, contact your Worksome account representative or visit the [Support](/support) page. ## Other integrations - [GraphQL API](/graphql) — Full programmatic access for custom integrations. - [Webhooks](/webhooks) — Real-time event notifications. - [PHP SDK](/integrations/php-sdk) — Official PHP SDK for the Worksome API. - [CLI](/integrations/cli) — Command-line interface with broad API coverage. - [MCP Server](/integrations/mcp-server) — AI agent integration via Model Context Protocol. - [AI Agents](/integrations/ai-agents) — AI-ready documentation and agent integration guide. --- ## /integrations/mcp-server # MCP Server The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) is an open standard for connecting AI assistants to external systems. A Worksome MCP server would let agents such as Claude and ChatGPT work with Worksome data through the same [public GraphQL API](/graphql) every other integration uses. > [!NOTE] > The MCP Server is in **early access** and not yet generally available. [Request access](https://docs.google.com/forms/d/1STlHLunHJzxQ8EASk3kJgX6aBu56RYuALGFy2f6lLac/viewform) or contact your Worksome account representative, and we will follow up with onboarding details, including the endpoint and the operations available to you. Which operations to expose to an agent is a deliberate decision rather than a straight mirror of the API, so the tool reference is not published here yet. If you have a use case in mind, the form above is the most useful place to describe it — what people want to automate is what shapes the surface we build. ## Support To request access, or for help once you have it: - [Request access via this form](https://docs.google.com/forms/d/1STlHLunHJzxQ8EASk3kJgX6aBu56RYuALGFy2f6lLac/viewform) - Contact your Worksome account representative - Visit the [Support](/support) page for general API support ## Other integrations - [GraphQL API](/graphql) — Full programmatic access for custom integrations. - [Webhooks](/webhooks) — Real-time event notifications. - [PHP SDK](/integrations/php-sdk) — Official PHP SDK for the Worksome API. - [CLI](/integrations/cli) — Command-line interface with broad API coverage. - [Zapier](/integrations/zapier) — No-code automation with triggers and actions. - [AI Agents](/integrations/ai-agents) — AI-ready documentation you can load into an agent today. --- ## /integrations/ai-agents # AI Agents Worksome provides multiple ways for AI agents and LLMs to interact with the platform — from structured API tools to machine-readable documentation that can be loaded directly into context windows. ## Machine-readable documentation We publish two documentation files following the emerging [`llms.txt` convention](https://llmstxt.org/) for AI-friendly documentation: ### `llms.txt` — Overview **URL:** [`https://docs.worksome.com/llms.txt`](/llms.txt) A concise summary of the Worksome API: capabilities, entity model, available queries/mutations, webhook events, and links to all documentation pages. Ideal for giving an AI agent a quick overview of what the API can do. **Use this when:** Your agent needs to understand the API's capabilities and decide which documentation to read next. ### `llms-full.txt` — Full documentation **URL:** [`https://docs.worksome.com/llms-full.txt`](/llms-full.txt) The complete text of all documentation pages concatenated into a single file — guides, webhook references, integration docs, and more. This file is large (6,000+ lines) but fits within the context window of most modern LLMs. **Use this when:** Your agent needs to answer detailed questions about the API, write integration code, or understand specific workflows end to end. ### Example: loading docs into an AI agent ```python import httpx # Load the full documentation into your agent's context docs = httpx.get("https://docs.worksome.com/llms-full.txt").text # Use it as context for your LLM messages = [ {"role": "system", "content": f"You are a Worksome API assistant.\n\n{docs}"}, {"role": "user", "content": "How do I create a hire and wait for contract acceptance?"}, ] ``` ```javascript // Load the overview for quick capability checks const overview = await fetch('https://docs.worksome.com/llms.txt').then(r => r.text()); // Or the full docs for detailed answers const fullDocs = await fetch('https://docs.worksome.com/llms-full.txt').then(r => r.text()); ``` ## Structured API access for agents For AI agents that need to **read and write** Worksome data (not just answer questions), use one of these tools: ### MCP Server The [MCP Server](/integrations/mcp-server) implements the [Model Context Protocol](https://modelcontextprotocol.io/), a standard for connecting AI assistants to external tools. It is in early access; request access from the [MCP Server](/integrations/mcp-server) page to get the endpoint and the operations available to you. **Best for:** AI assistants like Claude, ChatGPT, or custom agents that support MCP. ### CLI with JSON output The [CLI](/integrations/cli) provides broad API coverage (170+ operations) with a `--output json` flag that outputs structured JSON — ideal for AI agents that can invoke shell commands. ```shell # An AI agent can run CLI commands and parse the JSON output worksome hires list --active-status ACTIVE --output json worksome contracts get Q29udHJhY3Q6MTIzNA== --output json ``` **Best for:** AI agents that can execute shell commands, or automation pipelines where the CLI is already available. ## Choosing the right approach | Need | Recommended approach | |---|---| | Answer questions about the API | Load `llms.txt` or `llms-full.txt` into context | | Query Worksome data in real time | [MCP Server](/integrations/mcp-server) or [CLI](/integrations/cli) | | Create hires, manage payment requests, mutate jobs | [MCP Server](/integrations/mcp-server) or [CLI](/integrations/cli) | | Build a custom AI integration | Combine `llms-full.txt` for knowledge + MCP Server or GraphQL API for actions | ## Other integrations - [GraphQL API](/graphql) — Full programmatic access for custom integrations. - [Webhooks](/webhooks) — Real-time event notifications. - [PHP SDK](/integrations/php-sdk) — Official PHP SDK for the Worksome API. - [CLI](/integrations/cli) — Command-line interface with broad API coverage. - [Zapier](/integrations/zapier) — No-code automation with triggers and actions. - [MCP Server](/integrations/mcp-server) — AI agent integration via Model Context Protocol. --- ## /integrations/timesheet-integration # Timesheet Integration ## What this integration is for This integration is for Worksome clients whose workers register their time in an external system — custom internal tooling, a time-tracking product, or similar — and who want to avoid duplicating that data by hand into Worksome. The client captures timesheet data externally and pushes it to Worksome via the Custom API. Worksome then: 1. Creates Worksome timesheet registrations for the workers on their hires. 2. Automatically creates payment requests for the workers based on those registrations. 3. Invoices the client for those payment requests through the normal Worksome billing flow. It is **assumed that timesheets have been approved on the client side before being pushed to Worksome.** Payment requests created by this integration are auto-approved by default. The worker gets paid and the client gets invoiced without any further manual review inside Worksome. The mutation for submitting timesheet data is intended for clients with **large volumes** of externally-captured timesheet data who want to automate both the capture of that data in Worksome and the subsequent approval and payout. ### Not a complete standalone integration This integration covers **pushing timesheet data into Worksome.** For it to be useful, the client's external system also needs the hire and contract context from Worksome — specifically, the Global ID of each hire and its contract period — so timesheet data sent over can be matched to the correct worker and contract. An additional layer of integration is therefore expected, using the Worksome [GraphQL API](/graphql) and/or [webhooks](/webhooks), to capture hire and contract data from Worksome into the external system. See [Hire Global ID](#hire-global-id) below for how to obtain the IDs you will need. ### Working with Worksome This part of the API is expected to be built upon in collaboration with Worksome. Your Worksome customer success manager can help with sandbox access, payment and billing configuration for your use case, and clarifying how timesheets and payment requests are managed inside Worksome once data starts flowing. ## Authentication and API access For authentication and general GraphQL API usage, see: - [Authentication](/authentication) - [Introduction to GraphQL](/graphql/guides/introduction) ## Testing The timesheet endpoint and data processing can be tested in the **sandbox environment.** Submitting registrations via the API will create timesheets, but processing those timesheets into payment requests requires coordination with your Worksome customer success manager, as this trigger is not currently available automatically in the sandbox. ## GraphQL mutation ```graphql mutation CreateCustomTimesheet($input: CreateCustomTimesheetInput!) { createCustomTimesheet(input: $input) { providedRegistrations successfulRegistrations rejectedRegistrations { externalId reason message } } } ``` ### Input | Field | Type | Description | |----------|-----------|-------------| | `schema` | `String!` | Must be `"default-json"` | | `data` | `String!` | JSON string containing the registration payload (see [Data format](#data-format)) | ### Response | Field | Type | Description | |---------------------------|--------|-------------| | `providedRegistrations` | `Int!` | Total number of registrations detected in the payload. | | `successfulRegistrations` | `Int!` | Number of registrations that passed all validation and were queued for processing. | | `rejectedRegistrations` | `[RejectedCustomTimesheetRegistration!]!` | One entry per registration that was **not** accepted, with the reason and a human-readable message. Empty when everything was accepted. | Each `RejectedCustomTimesheetRegistration` has: | Field | Type | Description | |--------------|-----------------------------------|-------------| | `externalId` | `String` | The `externalId` from the submitted registration (may be null if that field was the one missing). | | `reason` | `CustomTimesheetRejectionReason!` | Machine-readable rejection reason (see below). | | `message` | `String!` | Human-readable explanation, suitable for your own logs or UI. | ### Rejection reasons | `reason` | When it fires | How to fix | |-------------------------------|---------------|------------| | `MISSING_REQUIRED_FIELD` | One or more required fields (`hireId`, `reportedDate`, `hours`, `externalId`) are missing, empty, or invalid. Includes unparseable dates. | Fix the payload on your side and resubmit. The `message` names the offending field. | | `HIRE_NOT_FOUND` | The submitted `hireId` does not resolve to a hire the authenticated account can submit timesheets for. This covers both "hire does not exist" and "hire exists but your account has no access" — the two cases are surfaced identically so that access scopes are not leaked. | Verify the `hireId` is correct and belongs to one of your Worksome-connected companies. See [Hire Global ID](#hire-global-id). | | `DATE_OUTSIDE_CONTRACT_PERIOD`| `reportedDate` falls before the hire's contract `startDate` or after its `endDate`. | Check the contract dates on the hire. Fix the date or stop submitting for the hire once it has ended. See the [US-payroll exception](#us-payroll-exception) below. | Registrations that pass all validation are accepted and counted in `successfulRegistrations`. Registrations that fail stop at the first failure — you only get one rejection reason per registration. > **Partial-success semantics.** A single payload may produce both accepted and rejected registrations. This is always a `200` response with structured errors in the body; there is no top-level GraphQL error. Process the `rejectedRegistrations` array the same way you would any business-level response. #### US-payroll exception For hires on a US payroll scheme, `DATE_OUTSIDE_CONTRACT_PERIOD` is **not** raised. Contract dates can change after a registration has already been submitted, so for US-payroll hires Worksome accepts the registration and handles any date-vs-contract discrepancy on its side — you do not need to filter these registrations out client-side. Out-of-contract dates on non-US-payroll hires are rejected as above. ### Recommended: submit in large batches We strongly recommend submitting timesheet data in **large, combined payloads** — many registrations per mutation call — rather than making one mutation call per registration or per worker. Large batches are easier to debug, monitor, re-send, and reprocess if something needs to be corrected. Sending thousands of small individual mutations makes it much harder to reason about failures, retries, and state. ## Data format The `data` field is a JSON string containing either a single registration object or an array of registration objects. A formal JSON Schema is published at: - [https://docs.worksome.com/schemas/timesheet-registration.json](/schemas/timesheet-registration.json) You can use the schema to validate outgoing payloads with any JSON Schema validator, generate type definitions for your integration code, or build transformation pipelines from non-JSON sources (CSV exports, spreadsheets, legacy systems) into the expected format. Business rules — for example whether the `hireId` resolves to a real hire — are validated by Worksome after submission. ### Registration object | Field | Type | Required | Description | |----------------|----------|----------|-------------| | `hireId` | `String` | Yes | Worksome Global ID of the hire (e.g. `SGlyZToxMjM0`). See [Hire Global ID](#hire-global-id). | | `reportedDate` | `String` | Yes | Date of the registration in `YYYY-MM-DD` format. Must fall within the hire's contract start/end dates. | | `hours` | `Number` | Yes | Number of hours worked. | | `externalId` | `String` | Yes | Unique identifier from the external system for this registration (see [Updating registrations](#updating-registrations-external-id)). | | `reference` | `String` | No | Free-text reference field (e.g. project code, cost centre, purchase order number). | | `isPayable` | `Boolean`| No | Whether this registration is billable. Defaults to `true`. | | `meta` | `Object` | No | Arbitrary key-value metadata to store with the registration. | ### Hire Global ID The `hireId` must be a Worksome Global ID — a base64-encoded identifier (e.g. `SGlyZToxMjM0`) that uniquely references a hire across the Worksome platform. This is different from the numeric internal ID visible in some URLs. Two ways to obtain hire Global IDs for your integration: - **Webhooks** — subscribe to [`contractAccepted`](/webhooks/reference/events/contract-accepted) to capture the `hireId` (at `data.contract.hireId`) as soon as a worker accepts the contract, and persist it alongside your internal worker/job records. Additional hire lifecycle events exist for keeping your records in sync; see the [Webhooks event reference](/webhooks/reference) for the full list. - **GraphQL query** — poll the `hires` query to list hires on demand. Pick whichever matches your integration style — webhooks require no polling and are lower-latency, while the GraphQL API is simpler for ad-hoc lookups or scheduled syncs. #### Example: list recent hires ```graphql query { hires(first: 25, orderBy: [{ field: CREATED_AT, order: DESC }]) { data { id number startDate endDate worker { name } job { name } } paginatorInfo { total currentPage lastPage } } } ``` The `id` returned is the Global ID you need for the `hireId` field when submitting timesheets. The `hires` query supports pagination, free-text search, and a range of filters — see the [`hires` query reference](/graphql/reference/queries) for all arguments. Notably useful for this integration are `activeStatus`, `startDateRange`/`endDateRange`, and `externalIdentifiers`. #### Look up a single hire by Global ID ```graphql query { hire(id: "SGlyZToxMjM0") { id number startDate endDate worker { name } job { name } } } ``` ### Example: single registration The `data` field is a JSON string — the actual registration payload — wrapped inside the outer GraphQL variables object. You have to **escape the inner double quotes** so the whole thing is still valid JSON: ```json { "input": { "schema": "default-json", "data": "[{\"hireId\": \"SGlyZToxMjM0\", \"reportedDate\": \"2026-04-07\", \"hours\": 8, \"externalId\": \"TS-001\"}]" } } ``` When the outer wrapping is parsed, the `data` value decodes to this valid JSON payload: ```json [ { "hireId": "SGlyZToxMjM0", "reportedDate": "2026-04-07", "hours": 8, "externalId": "TS-001" } ] ``` > JSON requires double quotes for property names and string values. Most HTTP clients and GraphQL tooling (Postman, Insomnia, etc.) can serialize the inner payload automatically if you pass it as an object; if you're writing the request by hand, escape with backslashes as shown above. ### Example: a work week for one worker ```json [ { "hireId": "SGlyZToxMjM0", "reportedDate": "2026-04-07", "hours": 8, "externalId": "TS-001", "reference": "PROJ-42" }, { "hireId": "SGlyZToxMjM0", "reportedDate": "2026-04-08", "hours": 7.5, "externalId": "TS-002" }, { "hireId": "SGlyZToxMjM0", "reportedDate": "2026-04-09", "hours": 8, "externalId": "TS-003", "isPayable": true, "meta": { "department": "Engineering" } } ] ``` ### Example: non-billable registration ```json [ { "hireId": "SGlyZToxMjM0", "reportedDate": "2026-04-09", "hours": 4, "externalId": "TS-004", "isPayable": false, "meta": { "reason": "Training day" } } ] ``` ## Processing behaviour ### Asynchronous processing Registrations are processed asynchronously. The mutation returns immediately with validation counts; actual timesheet creation happens in the background. ### Timesheet grouping Registrations are grouped into weekly timesheets (Monday-Sunday) per hire. If a timesheet already exists for the hire and week, registrations are added to it. ### Registration types Each registration creates two records: - A **line registration** preserving the original payload (for audit/timeline). - A **day registration** aggregating hours for the date (used for billing calculations). ### Updating registrations (External ID) The `externalId` field is the key for updates. When you send a registration with an `externalId` that already exists for the same date and timesheet, the previous line registration is **replaced** with the new one. The day registration is then recalculated from all current line registrations for that date. This means you can correct a registration by sending the same `externalId` with updated hours: ```json { "hireId": "SGlyZToxMjM0", "reportedDate": "2026-04-07", "hours": 6, "externalId": "TS-001" } ``` If `TS-001` was previously submitted with 8 hours, it is now replaced with 6 hours. ### Deleting registrations Deletion of individual registrations is **not currently supported** via the API. To effectively zero out a registration, send an update with `0` hours or `"isPayable": false` using the same `externalId`. ### Custom fields If the hire's company has custom fields defined on timesheets, payload keys matching custom field slugs are automatically applied to registrations. ## Validation All validation happens **at submission time** and is surfaced immediately in the response via `rejectedRegistrations`. See [Rejection reasons](#rejection-reasons) for the full list of rejection types and how to address each one. The top-level `data` string must be valid JSON. If it is not, the mutation responds with a top-level GraphQL error instead of per-registration rejections. Registrations that pass submission-time validation are queued for processing and become visible as Worksome timesheets shortly after. Contracts can change after submission — if a hire's end date is shortened after you submitted a registration for a now-out-of-contract date, that registration will be handled correctly at payment-request-creation time (accepted + isolated for US-payroll hires; otherwise ignored for billing). You do not need to manage this on the client side. ## Payment request creation Payment requests are automatically created from completed timesheets on a schedule. A timesheet is eligible for processing when: - It has a resolved hire and worker. - The timesheet week has ended (end date is on or before the previous Sunday). - It has not been previously processed (or its duration has changed since the last payment request was created). - It has not been held back by additional validation by the Worksome team. > The Worksome team may hold back individual timesheets from automatic processing when additional review is required. This is an internal mechanism that is not surfaced in the API and not something you need to configure from your side. If you see that a submitted timesheet has not produced a payment request and you have verified hire and date resolution are correct, contact your Worksome customer success manager. ### Schedule Automatic payment request creation runs **every Wednesday at 13:00 UTC.** Timesheets for weeks ending on or before the most recent Sunday are processed at that time. ### Auto-approval All payment requests created from API-submitted timesheets are **automatically approved.** The integration assumes timesheets have been reviewed and approved on the client side before being pushed to Worksome. If auto-approval is not compatible with your requirements — for example if you need a second review inside Worksome before payouts run — contact your Worksome customer success manager to discuss options. ### Rate calculation Payment request amounts are calculated based on the hire's rate type: - **Hourly:** Total payable hours × hourly rate. - **Daily:** Number of unique payable days × daily rate. - **Weekly:** Worked-day proportion of the week × weekly rate (see [Pro-rating](#pro-rating-weekly-rate-hires)). ### Pro-rating (weekly rate hires) For hires on a weekly rate, a full week is paid when 5 or more payable weekdays are worked. The week is pro-rated only when the contract starts or ends mid-week: - **Contract starts mid-week:** the week is pro-rated based on the number of weekdays from the contract start date to the end of the timesheet week. If the worker logged more days than the entitled weekdays, the higher count is used. - **Contract ends mid-week:** the week is pro-rated based on the number of weekdays from the start of the timesheet week to the contract end date. Again, the higher of entitled weekdays and worked days is used. - **Full week within contract:** up to 5 worked days count; additional days do not increase the amount. The week's payable proportion is calculated as `worked-or-entitled-days ÷ 5` and multiplied by the weekly rate. If the timesheet has no payable hours, the proportion is `0` and no payment request is created. ### Billing period The billing period is constrained to the hire's contract dates. If a timesheet spans beyond the contract start or end, only the portion within the contract period is billed. ### Manual processing Payment request creation can also be manually triggered by the Worksome team for an individual timesheet, which is useful during sandbox testing or when correcting an issue. ## Current limitations - Deletion of individual registrations via API is not supported (use the update mechanism to zero out). - Overtime is currently calculated only for hires where Worksome is the Employer of Record in the United States. Non-US / non-EoR hires are billed at the contract rate without overtime adjustments. --- ## /errors # Error Reference This page is a quick reference for the error envelope returned by the Worksome GraphQL API. For a detailed guide on handling errors in your code, see [Error Handling](/graphql/guides/error-handling). ## Error envelope Every GraphQL error returned by the gateway includes an `extensions.code` field that identifies the broad failure mode. Use the `code` (not the `message`) to drive your control flow. Many platform-side errors also include a `serviceName` and a structured `validation` map.
Code HTTP Retryable Description
GRAPHQL_VALIDATION_FAILED 200 No The query references a field, argument, or type that does not exist on the schema. Fix the query.
GRAPHQL_PARSE_FAILED 200 No The GraphQL document could not be parsed (syntax error). Fix the query.
BAD_REQUEST 400 No The HTTP request shape is wrong (e.g. missing Content-Type: application/json, GET without an apollo-require-preflight header, or a body that is not valid JSON).
DOWNSTREAM_SERVICE_ERROR 200 Sometimes The query is valid but the platform rejected the operation. Discriminate using the additional extensions fields described below: presence of validation means input validation; guards: ["api"] with message: "Unauthenticated." means missing/invalid token; otherwise it is generally an authorization or business-rule failure.
> [!WARNING] > Errors arrive over HTTP 200 in almost every case — GraphQL puts failures in the response body. Always parse the `errors` array, even if `data` is present. ## Validation errors Validation errors come back as `DOWNSTREAM_SERVICE_ERROR` with an `extensions.validation` map of dot-notation field paths to messages. ```json { "errors": [ { "message": "Validation failed for the field [createDraftHire].", "path": ["createDraftHire"], "extensions": { "validation": { "input.startDate": ["The start date is not a valid date."], "input.trustedContact": ["The selected input.trusted contact is invalid."] }, "serviceName": "platform", "code": "DOWNSTREAM_SERVICE_ERROR" } } ] } ``` ### Common validation errors | Field pattern | Error | Resolution | |---|---|---| | `input.{field}` | "The {field} field is required." | Include the required field in your input. | | `input.{field}` | "The {field} is not a valid date." | Use ISO 8601 format: `YYYY-MM-DD`. | | `input.{field}` | "The selected {field} is invalid." | Verify the ID exists, is the right entity type, and is reachable by the authenticated user. | | `input.rate` | "The rate must be a number." | Provide a numeric value (integer or float). | | `input.currency` | "The selected currency is invalid." | Use a valid ISO 4217 currency code (e.g., `USD`, `GBP`, `EUR`, `DKK`). | ## Authentication errors A missing or invalid bearer token is also wrapped as `DOWNSTREAM_SERVICE_ERROR`, with the platform's `api` guard listed in `extensions.guards`. ```json { "errors": [ { "message": "Unauthenticated.", "extensions": { "guards": ["api"], "serviceName": "platform", "code": "DOWNSTREAM_SERVICE_ERROR" } } ] } ``` | Cause | Resolution | |---|---| | Missing `Authorization` header | Add `Authorization: Bearer {token}` to your request headers. | | Expired token | Tokens expire 6 months after creation. Generate a new Personal Access Token. | | Revoked token | The token was manually revoked. Create a new one. | | Malformed header | Ensure the format is exactly `Bearer {token}` with a single space. | ## Authorization errors When the token is valid but the operation is not allowed for the authenticated user (or for the requested record), you also receive a `DOWNSTREAM_SERVICE_ERROR`. The envelope has no machine-readable "authorization" tag — discriminate by the absence of a `validation` map plus the operation `path` and message. ```json { "errors": [ { "message": "You are not authorized to perform this action.", "path": ["terminateHire"], "extensions": { "serviceName": "platform", "code": "DOWNSTREAM_SERVICE_ERROR" } } ] } ``` | Cause | Resolution | |---|---| | Insufficient role | Ensure the API token belongs to a user with the required role (e.g., manager, admin). | | Wrong company scope | The resource belongs to a different company than the authenticated user. | | Field-level restriction | Some fields require elevated permissions. The field returns `null` in `data` with a corresponding entry in `errors`. | ## Query errors The gateway validates the query against the schema before reaching the platform. Field/argument/type mismatches surface as `GRAPHQL_VALIDATION_FAILED`; pure syntax errors surface as `GRAPHQL_PARSE_FAILED`. ```json { "errors": [ { "message": "Cannot query field \"nonExistent\" on type \"Hire\".", "locations": [{ "line": 3, "column": 5 }], "extensions": { "code": "GRAPHQL_VALIDATION_FAILED" } } ] } ``` | Cause | Resolution | |---|---| | Unknown field | Check the field name against the [schema reference](/graphql/reference/queries). | | Missing required argument | Add the required argument to the field. | | Type mismatch | Ensure argument values match the expected types (e.g., `ID!` expects a string). | | Syntax error | Check for unclosed braces, missing commas, or invalid GraphQL syntax. | ## Bad request errors The gateway rejects HTTP requests that do not conform to its expectations (e.g. missing `Content-Type`, `GET` without an `apollo-require-preflight` header) before reaching GraphQL. ```json { "errors": [ { "message": "This operation has been blocked as a potential Cross-Site Request Forgery (CSRF). Please either specify a 'content-type' header (with a type that is not one of application/x-www-form-urlencoded, multipart/form-data, text/plain) or provide a non-empty value for one of the following headers: x-apollo-operation-name, apollo-require-preflight", "extensions": { "code": "BAD_REQUEST" } } ] } ``` | Cause | Resolution | |---|---| | Missing `Content-Type` | Send `Content-Type: application/json` on every POST. | | GET request | Use POST. If you genuinely need GET (e.g. for caching), add `apollo-require-preflight: true`. | | Body is not JSON | Make sure the body is a JSON object with `query` (and optional `variables`/`operationName`). | ## Server errors If the platform itself fails (timeout, unhandled exception), the gateway forwards a `DOWNSTREAM_SERVICE_ERROR` with `serviceName: "platform"` and a generic message. These are typically transient — retry with exponential backoff (1s, 2s, 4s) up to 3 times. If the error persists, contact [Support](/support) with the full error response and your query. ## Rate limiting The Worksome platform applies rate limiting per API token (default 60 requests per minute). When you exceed the limit, the gateway forwards a `DOWNSTREAM_SERVICE_ERROR`; the gateway does not currently expose `X-RateLimit-*` headers on the response. Treat repeated `DOWNSTREAM_SERVICE_ERROR` responses with a "Too Many Requests" message as rate-limit signals, and back off. See [Rate Limiting](/graphql/guides/rate-limiting) for details. ## Partial responses GraphQL can return **both data and errors** in the same response. Always check the `errors` array even when `data` is present. ```json { "errors": [ { "message": "You are not authorized to access this field.", "path": ["hire", "rate"], "extensions": { "serviceName": "platform", "code": "DOWNSTREAM_SERVICE_ERROR" } } ], "data": { "hire": { "id": "SGlyZTox", "rate": null } } } ``` Fields that error return `null` in the data, with corresponding entries in the `errors` array. Use the available data and handle individual field errors gracefully. --- ## /changelog # Changelog This page contains a log of notable changes to the API and webhooks, to help you understand what has changed and how it might affect your integrations. For a full changelog including all changes, please see the [Apollo Studio changelog](https://studio.apollographql.com/public/Worksome/variant/production/changelog). ## 2026-04-22

Medium Impact Changes

Deprecation of recruiter fee fields

The `recruiterFee` field on both the `Hire` and `CompanyRecruiter` types has been deprecated. It only supports percentage-based fees and returns `null` for any other fee basis (hourly, daily, weekly, monthly). Please migrate to the `fees` field, which returns the full set of fees associated with the hire or recruiter relationship and supports every fee basis. **Before:** ```graphql query Hire($id: ID!) { hire(id: $id) { recruiterFee } } ``` **After:** ```graphql query Hire($id: ID!) { hire(id: $id) { fees { id formattedRate } } } ``` ## 2026-04-18

New Integration Guide

Timesheet Integration

A new integration guide is available for clients who collect timesheet data in an external system and want to automate timesheet and payment request creation in Worksome. See [Timesheet Integration](/integrations/timesheet-integration). A JSON Schema for the timesheet registration payload is published at [/schemas/timesheet-registration.json](/schemas/timesheet-registration.json) for payload validation and client code generation. ## 2026-03-15

Documentation

Expanded API documentation

The developer documentation has been significantly expanded with new guides and reference pages: - **GraphQL reference** — Auto-generated pages for queries, mutations, objects, enums, scalars, unions, interfaces, and input objects, with example queries. - **Guides** — New pages on pagination and filtering, error handling, and rate limiting. - **Webhooks** — Dedicated event reference pages with full payload examples for all webhook events, plus a guide on handling webhooks. - **Integrations** — Documentation for the CLI, PHP SDK, MCP Server, Zapier, and AI agent integration options. - **AI-friendly docs** — Published `llms.txt` and `llms-full.txt` for loading documentation directly into LLM context windows. ## 2026-01-15

Medium Impact Changes

New createDraftHire mutation

A new `createDraftHire` mutation has been added to the public API for creating draft hires for trusted contacts. Draft hires must be completed in the Worksome UI before they become active. **Example mutation:** ```graphql mutation CreateDraftHire($input: HireInput!) { createDraftHire(input: $input) { id activeStatus } } ``` **Example variables:** ```json { "input": { "company": "Q29tcGFueTox", "trustedContact": "V29ya2VyOjE=", "name": "Backend contractor", "description": "Build integrations with our partner APIs", "startDate": "2024-10-01", "endDate": "2024-12-31", "includeStandardContract": true, "locationPreference": {"preference": "REMOTE_ONLY"}, "rateType": "FIXED", "rate": 12000 } } ```

Deprecation of hire mutation

The `hire` mutation has been deprecated in favor of `createDraftHire` for creating draft hires via the public API. Please migrate to `createDraftHire` for all new integrations.

HireInput customFieldValues clarification

The `customFieldValues` field on `HireInput` now includes clarified documentation: passing an empty array `[]` will skip custom field syncing entirely. If you have required custom fields configured, you must provide values for them or validation will fail. ## 2025-06-03

Low Impact Changes

Webhook hire status field

A new `hireStatus` field has been added to all hire-related webhook payloads within the `contract` object. This field provides a consistent way to track hire status across all webhook events. The field is included in the following webhook events: - `contractAccepted` - `hireCancelled` - `hireEnded` - `hireTerminated` - `hireUpdated` Possible values include: `draft`, `offered`, `ready`, `active`, `ended`, `cancelled`, and `terminated`. **Example payload structure:** ```json { "event": "hireUpdated", "data": { "contract": { "id": "Q29udHJhY3Q6MTIzNA==", "hireStatus": "active", ... }, "worker": { ... } } } ``` This is a non-breaking change - existing webhook consumers will continue to work without modification, and the existing `contract.status` field remains unchanged. ## 2024-09-02 View the full changelog for this date on [Apollo Studio](https://studio.apollographql.com/public/Worksome/variant/production/changelog/version/f5cd4f84-bcda-4e23-9bd5-f762a2a2239e).

Low Impact Changes

Job payment schemes

The `Job.paymentScheme` field and related arguments have been deprecated as part of our platform simplification efforts and were **removed** from the API on 29 September 2024. No direct replacement is planned. ## 2024-08-29 View the full changelog for this date on [Apollo Studio](https://studio.apollographql.com/public/Worksome/variant/production/changelog/version/04ec1973-e5f8-45ec-8078-937fcc64553e).

Low Impact Changes

Payment request type

All references to bills have been removed from the API. ## 2024-02-27 View the full changelog for this date on [Apollo Studio](https://studio.apollographql.com/public/Worksome/variant/production/changelog/version/630c02a5-4d9f-453a-90b6-85176b06e43f).

Medium Impact Changes

Hire date properties

The `dateStart` and `dateEnd` fields have been deprecated. Please migrate to the new `startDate` and `endDate` fields as soon as possible. ## 2024-02-12 View the full changelog for this date on [Apollo Studio](https://studio.apollographql.com/public/Worksome/variant/production/changelog/version/90124dc1-ccbe-4087-b9ff-6841a0606d70).

Medium Impact Changes

Payment request type

The `Bill` type has been renamed to `PaymentRequest`, and corresponding references to bills are now deprecated and were **removed** from the API on 29 August 2024. Please migrate any uses of the following in your integrations: - `bill` should be renamed to `paymentRequest` - `bills` should be renamed to `paymentRequests` --- ## /support # Help and Support For help with using the API, or if a new feature is required, contact us at dev-support@worksome.com ## Errors We try to make sure that all error messages are self-explanatory and easier to understand. For example, a validation error will show a message such as `Variable "$xyz" got invalid value "X"`. This allows for easier debugging of the queries and requests that are made to the API. An example error JSON object, returned by the API, is shown below.
Example validation error message ```json { "errors": [ { "message": "Variable \"$status\" got invalid value \"UNKNOWN\" at \"status[2]\"; Value \"UNKNOWN\" does not exist in \"InvoiceStatus\" enum.", "locations": [ { "line": 1, "column": 25 } ], "extensions": { "code": "BAD_USER_INPUT" } } ] } ```
For a complete list of error codes and resolution steps, see the [Error Reference](/errors). To learn how to handle errors programmatically in your integration, see the [Error Handling guide](/graphql/guides/error-handling). ---