Requests and errors

Page through lists, filter and sort them, retry writes safely, stay inside the rate limits, and read the errors the Assignar Pay API returns.

These rules apply to every endpoint an API key can call. Paths are relative to the base URL, https://pay.assignar.com/api/v1. For which query parameters, filter fields and sort fields each endpoint takes, see its entry in the API reference.

Page through a list

List endpoints such as /timesheets, /pay-rates, /invoices and /cost-codes return one page at a time:

ParameterDefaultAllowed
page1A whole number from 1
page_size100A whole number from 1 to 500

The response carries the page and a pagination object:

{
  "data": [ … ],
  "pagination": { "page": 2, "page_size": 100, "total": 267, "total_pages": 3 }
}

Keep requesting the next page until it equals total_pages. A value outside the allowed range is refused with 400, for example {"error": "Invalid pagination parameter: page_size must be an integer between 1 and 500"}; it is never quietly changed.

Pick lists

/clients, /projects, /suppliers, /employees and /tags are pick lists: they return a short record for each item (its id, a label and a few other fields) and no pagination object. Where a resource also has a full, paginated list, it is at the same path plus /list: GET /projects/list returns whole project records and takes the filters and sorting described below. Pick lists take different parameters:

ParameterDefaultAllowed
limit100A whole number from 1 to 500
searchnoneText matched anywhere in the label, ignoring case
curl "https://pay.assignar.com/api/v1/projects?search=aquatic&limit=20" \
  -H "X-API-KEY: sk_your_api_key"

/clients returns id, label, external_id and state for each client. state is the client's address state or region. Pass state for an exact match, for example state=NSW.

For every field of one record, call its detail endpoint, such as /clients/{id} or /employees/{id}.

The default client's name and label use your account's English client terminology. Nested client names in other responses use the same wording. Other client names are returned as entered.

Filter and sort

Filter a list with field=operator.value. Several filters combine, and a record must match all of them:

curl "https://pay.assignar.com/api/v1/timesheets?status=eq.completed&submitted_date=gte.2026-09-01&sort_by=submitted_date.desc" \
  -H "X-API-KEY: sk_your_api_key"
OperatorMeaningExample
eq, neqEquals, not equalsstatus=neq.archived
gt, gte, lt, lteGreater or less thansubmitted_date=lte.2026-09-30
in, not.inOne of a comma-separated liststatus=in.pending,overdue
like, ilikePattern match, ilike ignoring casedescription=ilike.*concrete*
contains, starts_with, ends_withText contains, starts or ends withcode=starts_with.SP-
is, is.notIs null, is not nullinvoice_id=is.null

Which fields each endpoint can filter on is listed in the reference; open Filter Operators Reference at the top of it for every operator and its aliases.

For /pay-rates, eq and neq ignore case on worker.name, worker.employee_id, worker.email, client.name, project.name, order.order_number, role.code, role.description and pay_rate_document.name. For example, worker.name=eq.john matches the first name John. These comparisons match the whole value; %, _ and * are literal characters.

/pay-rates leaves out the time of workers linked to a supplier, such as consultants and agency workers, because their supplier invoices for it. Add include_supplier_workers=true to include them. Timesheets of workers whose employment type is excluded from payroll are left out either way, because Assignar Pay doesn't calculate their pay. Timesheets that already had pay before the type was excluded are still returned. Any value other than true or false is refused with 400: {"error": "include_supplier_workers must be true or false"}. A worker's supplier_id on /employees/{id} names their supplier, and is null for an employee paid through payroll.

Know when /pay-rates left timesheets out

Check complete before you total a /pay-rates response. A timesheet whose saved pay can't be read is left out of data, and pagination.total does not count it.

{
  "data": [ … ],
  "pagination": { "page": 1, "page_size": 100, "total": 42, "total_pages": 1 },
  "unreadable_timesheet_count": 1,
  "unreadable_timesheet_ids": ["550e8400-e29b-41d4-a716-446655440000"],
  "complete": false
}

unreadable_timesheet_count is how many timesheets this query left out, and it is the same on every page. unreadable_timesheet_ids lists their id, the same id each timesheet in data carries, with no worker name and no date.

complete is false when that count is greater than zero, even when page equals total_pages. When the count is zero, complete is true and unreadable_timesheet_ids is an empty array.

Keep requesting the next page until it equals total_pages. Don't total the period, or export it, while complete is false: those timesheets aren't in data.

Sort with sort_by=field.asc or sort_by=field.desc. Without .asc or .desc, the sort is ascending.

Where an endpoint supports client-name filters, equality, list and text filters accept the displayed default-client name. Its position in name sorting and gt, gte, lt and lte comparisons remains unchanged.

/invoices, /quotes, /purchase-orders and /supplier-invoices also take include=line_items, which adds each record's line items to the list. Their detail endpoints always include them.

Filtering on a field the endpoint doesn't support, an unknown operator, an unknown sort field or an unsupported include value is refused with 400, and the message lists what is allowed:

{ "error": "Invalid sort field: \"nope\". Sortable fields: submitted_date, approval_status, status, created_at, updated_at" }

Retry a write safely

POST /cost-codes, POST /cost-codes/batch, POST /clients, DELETE /clients/{id}, POST /clients/{id}/restore, PATCH /invoices/{id}/status, PATCH /purchase-orders/{id}/status, POST /documents/send, the project writes (POST /projects, PATCH and DELETE /projects/{id}, POST /projects/{id}/restore) and the three competency import endpoints (POST /competencies/import, POST /worker-competencies/evidence-uploads, POST /worker-competencies/import) accept an Idempotency-Key header. Send a unique value of up to 255 characters with each distinct write, and send the same value again when you retry it:

curl -X POST "https://pay.assignar.com/api/v1/documents/send" \
  -H "X-API-KEY: sk_your_api_key" \
  -H "Idempotency-Key: send-INV-100231-2026-09-27" \
  -H "Content-Type: application/json" \
  -d '{"document_type": "invoice", "document_id": "…", "to": ["accounts@harbourside.example"], "subject": "Invoice INV-100231", "message": "Please find your invoice attached."}'

A retry with the same key returns the first attempt's response instead of doing the work again, so a timed-out request never sends an invoice twice. The header is optional; without it, every request is carried out.

StatusBodyWhat to do
409A request with this Idempotency-Key is still in progress. Retry shortly.The first attempt hasn't finished. Wait and retry with the same key.
422This Idempotency-Key was already used with a different request bodyUse a new key for a different request.

Stay within the rate limits

Each API key may make, by default:

  • 60 requests a minute
  • 1,000 requests an hour
  • 10,000 requests a day

All three apply at once, and the tightest one decides. Every response to a recognised key carries its current position:

HeaderMeaning
X-RateLimit-LimitThe limit of the window that is closest to running out
X-RateLimit-RemainingRequests left in that window
X-RateLimit-ResetWhen that window resets, as a Unix timestamp in seconds
Retry-AfterOnly on a 429: seconds to wait before the next request

Over the limit, the request is refused with 429:

{
  "error": "Rate limit exceeded",
  "message": "You have exceeded the rate limit of 60 requests. Please try again after 45 seconds."
}

Wait for the number of seconds in Retry-After, then continue. To stay under the limit, watch X-RateLimit-Remaining, fetch large pages (page_size up to 500) rather than many small ones, and use POST /cost-codes/batch for bulk cost code changes. If your integration needs higher limits, contact Assignar support: limits can be raised for an organization or for a single key.

Errors

Every error response is JSON with an error message that says what went wrong:

StatusWhenExample error
400A parameter or the request body is invalid. The message names each field and why.Validation error: code: Invalid input; description: Invalid input
400An {id} in the path is not a UUID.Invalid id format: expected UUID
401No key, or the key is wrong, revoked or expired.Invalid API key
403The key's scopes don't cover the endpoint.Insufficient scope: requires accounting:read
403The endpoint needs an Assignar Connect workspace, not an API key. The message says so.See What an API key cannot do.
404No record with that id in your organization.Timesheet not found
409, 422An Idempotency-Key conflict.See Retry a write safely.
409The client can't be archived or restored in its current state. A code field says why.See Archive and restore clients.
429A rate limit was reached.Rate limit exceeded
500Something failed on Assignar Pay's side.An internal server error occurred while processing your request

A 500 whose message ends in "retry the request" is temporary: retry after a short wait with the same key, and with the same Idempotency-Key for a write.