API Conventions: Pagination, Filtering, Sorting, and Error Handling

This document describes the conventions used across all Operating API endpoints. Understanding these patterns will help you work efficiently with any endpoint in the API.

Base URL

All API requests use this base URL:

https://api.operating.app/v1

Authentication

Every request must include your API key in the Authorization header using the Bearer token scheme:

Authorization: Bearer YOUR_API_KEY

See the Getting Started: Your First API Call guide for details on obtaining your API key.

Response Structure

List Endpoints

All list endpoints (e.g., GET /projects, GET /time-entries) return a consistent structure:

{
  "data": [ /* array of resources */ ],
  "meta": {
    "nextCursor": "cursor_string_or_null"
  }
}
  • data: Array of resource objects
  • meta.nextCursor: Pagination cursor (see Pagination section below). null if there are no more results.

Single-Resource Endpoints

Single-resource endpoints (e.g., GET /projects/{id}, GET /persons/{id}) return the resource object directly:

{
  "id": "7001",
  "name": "Stellar Platform Modernization",
  "clientId": "4001",
  ...
}

No wrapping data array, no meta object.

Pagination

Cursor-Based Pagination

The Operating API uses cursor-based pagination for all list endpoints. This approach is more reliable than offset/limit pagination when data is being created or modified during pagination.

Query Parameters

  • limit (optional): Maximum number of results to return.
  • after (optional): Cursor token from a previous response's meta.nextCursor field. Returns results after this cursor.

How It Works

  1. Make your first request without an after parameter:
curl "https://api.operating.app/v1/projects?limit=10" \
  -H "Authorization: Bearer $OPERATING_API_KEY"
  1. The response includes a meta.nextCursor:
{
  "data": [ /* 10 projects */ ],
  "meta": {
    "nextCursor": "eyJpZCI6IjcwMTAifQ"
  }
}
  1. To fetch the next page, pass the cursor in the after parameter:
curl "https://api.operating.app/v1/projects?limit=10&after=eyJpZCI6IjcwMTAifQ" \
  -H "Authorization: Bearer $OPERATING_API_KEY"
  1. Continue until meta.nextCursor is null:
{
  "data": [ /* last page of results */ ],
  "meta": {
    "nextCursor": null
  }
}

Cursor Behavior

  • Cursors are opaque strings. Do not attempt to parse or modify them.
  • Cursors are tied to the specific filters and sort order of the original request. Changing filters requires starting a new pagination sequence.

Filtering

List endpoints support various filter parameters. Filters are applied using query parameters.

Common Filter Patterns

Date Range Filters

Most list endpoints support these timestamp filters:

  • createdBefore: ISO 8601 datetime in UTC (e.g., 2025-04-15T23:59:59Z)
  • createdSince: ISO 8601 datetime in UTC
  • updatedBefore: ISO 8601 datetime in UTC
  • updatedSince: ISO 8601 datetime in UTC

Example — fetch projects created in April 2025:

curl "https://api.operating.app/v1/projects?createdSince=2025-04-01T00:00:00Z&createdBefore=2025-04-30T23:59:59Z" \
  -H "Authorization: Bearer $OPERATING_API_KEY"

Entity-Specific Date Filters

Some endpoints have domain-specific date filters:

  • entryDateAfter / entryDateBefore (time entries): Filter by the date the work was done (YYYY-MM-DD format)
  • archivedBefore / archivedAfter (projects): Filter by archive date

Example — fetch time entries from April 7, 2025:

curl "https://api.operating.app/v1/time-entries?entryDateAfter=2025-04-07&entryDateBefore=2025-04-07" \
  -H "Authorization: Bearer $OPERATING_API_KEY"

Relationship Filters

Filter by related entity IDs:

  • projectId: Filter time entries, positions, or allocations by project
  • personId: Filter time entries or positions by person
  • clientId: Filter projects by client
  • tagId: Filter projects by tag

Example — fetch all time entries for person ID 1001:

curl "https://api.operating.app/v1/time-entries?personId=1001" \
  -H "Authorization: Bearer $OPERATING_API_KEY"

Boolean Filters

Some endpoints support boolean filters:

  • archived: Filter by archive status. Values: "true", "false". By default, both archived and non-archived records are returned.

Example — fetch only non-archived projects:

curl "https://api.operating.app/v1/projects?archived=false" \
  -H "Authorization: Bearer $OPERATING_API_KEY"

External Identifier Filters

Filter by external system identifiers:

  • externalId: Filter records by external ID (see External identifiers in Operating API guide)

Example:

curl "https://api.operating.app/v1/time-entries?externalId=harvest-te-98765" \
  -H "Authorization: Bearer $OPERATING_API_KEY"

Combining Filters

Multiple filters can be combined. They are applied with AND logic:

curl "https://api.operating.app/v1/time-entries?personId=1001&projectId=7001&entryDateAfter=2025-04-01&entryDateBefore=2025-04-30" \
  -H "Authorization: Bearer $OPERATING_API_KEY"

This returns time entries for person 1001 on project 7001 during April 2025.

Sorting

The Operating API does not currently support custom sort orders via query parameters. Results are returned in a default order:

  • Most list endpoints sort by id ascending
  • Some endpoints may use other default sort orders (e.g., time entries may sort by date descending)

The sort order is consistent within a pagination sequence. If you need a different sort order, implement it client-side after fetching all results.

Error Handling

All error responses follow a consistent structure:

{
  "code": "ERROR_CODE",
  "message": "Human-readable error message",
  "issues": [ /* optional array of validation issues */ ]
}

HTTP Status Codes

The API uses standard HTTP status codes:

Status CodeMeaning
200Success
400Bad Request — Invalid input data
401Unauthorized — Missing or invalid API key
403Forbidden — Insufficient permissions
404Not Found — Resource doesn't exist or you don't have access
500Internal Server Error — Something went wrong on our end

Error Codes

400 Bad Request

Returned when the request contains invalid data.

Example — Invalid date format:

{
  "code": "BAD_REQUEST",
  "message": "Invalid input data",
  "issues": [
    {
      "message": "Invalid date format for 'createdSince'. Expected ISO 8601 format."
    }
  ]
}

Common causes:

  • Invalid date/time format (must be ISO 8601 in UTC)
  • Missing required fields in POST/PATCH requests
  • Invalid enum values (e.g., wrong billing type)
  • Type mismatches (e.g., string where number expected)

How to fix:

  • Check the issues array for specific validation errors
  • Verify all required fields are present
  • Ensure date/time values are in ISO 8601 format (e.g., 2025-04-15T10:30:00Z)
  • Consult the endpoint reference for valid enum values

401 Unauthorized

Returned when the API key is missing, invalid, or expired.

{
  "code": "UNAUTHORIZED",
  "message": "Authorization not provided"
}

Common causes:

  • Missing Authorization header
  • Invalid API key
  • API key was regenerated (remember: creating a new key invalidates the old one)

How to fix:

  • Verify the Authorization header is present: Authorization: Bearer YOUR_KEY
  • Check that your API key is correct
  • If you recently regenerated your API key, update all integrations with the new key

403 Forbidden

Returned when your API key doesn't have permission to access the resource.

{
  "code": "FORBIDDEN",
  "message": "Insufficient access"
}

Common causes:

  • API key lacks permission for the requested operation
  • Attempting to access resources outside your organization
  • Permission settings changed after API key was created

How to fix:

  • Verify your API key has the necessary permissions in Operating settings
  • Contact your Operating administrator to adjust API key permissions

404 Not Found

Returned when the requested resource doesn't exist or you don't have permission to see it.

{
  "code": "NOT_FOUND",
  "message": "Not found"
}

Common causes:

  • Resource ID doesn't exist
  • Resource was deleted
  • You don't have permission to view the resource (403 and 404 may be returned interchangeably for security reasons)

How to fix:

  • Verify the resource ID is correct
  • Check that the resource hasn't been deleted
  • Ensure your API key has permission to view this resource

500 Internal Server Error

Returned when something went wrong on the Operating API server.

{
  "code": "INTERNAL_SERVER_ERROR",
  "message": "Internal server error"
}

How to handle:

  • Implement retry logic with exponential backoff
  • If the error persists, contact Operating support at [email protected]

Handling Errors in Your Code

Implement appropriate error handling based on the error codes and HTTP status codes returned by the API. Use the issues array in 400 errors for detailed validation feedback.

Rate Limiting

The Operating API enforces rate limits to ensure system stability and fair usage across all customers. If you exceed the rate limit, you'll receive a 429 Too Many Requests response.

Best practices:

  • Implement exponential backoff for retries
  • Cache responses when appropriate
  • Batch operations using bulk endpoints where available (e.g., POST /time-entries/bulk)
  • Spread requests over time rather than making large bursts

If you have questions about rate limits for your specific use case, contact us at [email protected].

Data Formats

Dates

  • Date-only fields (e.g., estimatedStartDate, entryDate): YYYY-MM-DD format (e.g., 2025-04-15)
  • Timestamp fields (e.g., createdAt, updatedAt): ISO 8601 format in UTC (e.g., 2025-04-15T10:30:00Z)

Always use UTC for timestamps. The API does not accept or return timezone-aware timestamps (e.g., no +02:00 offsets).

Monetary Amounts

Monetary values are represented as objects with amount and currency:

{
  "amount": 165.00,
  "currency": "EUR"
}
  • amount: Decimal number (e.g., 165.00, 1250.50)
  • currency: ISO 4217 currency code (e.g., "EUR", "USD", "GBP")

Time Durations

Time durations (e.g., time entry hours) are represented in seconds:

{
  "seconds": 28800  // 8 hours
}

Conversion:

  • 1 hour = 3,600 seconds
  • 8 hours = 28,800 seconds
  • 7.5 hours = 27,000 seconds

Percentages

Allocation percentages use 10,000ths of a percent encoding:

{
  "percentage": 1000000  // 100%
}

Conversion:

  • 100% = 1,000,000
  • 50% = 500,000
  • 25% = 250,000
  • 12.5% = 125,000

Always encode percentages this way when creating or updating allocations.

Null Values

The API uses null to represent absence of a value:

{
  "clientId": null,  // Project has no client
  "through": null    // Project has no estimated end date
}

When updating resources with PATCH requests:

  • Omitting a field leaves it unchanged
  • Setting a field to null clears the value (if the field is nullable)