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 objectsmeta.nextCursor: Pagination cursor (see Pagination section below).nullif 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'smeta.nextCursorfield. Returns results after this cursor.
How It Works
- Make your first request without an
afterparameter:
curl "https://api.operating.app/v1/projects?limit=10" \
-H "Authorization: Bearer $OPERATING_API_KEY"- The response includes a
meta.nextCursor:
{
"data": [ /* 10 projects */ ],
"meta": {
"nextCursor": "eyJpZCI6IjcwMTAifQ"
}
}- To fetch the next page, pass the cursor in the
afterparameter:
curl "https://api.operating.app/v1/projects?limit=10&after=eyJpZCI6IjcwMTAifQ" \
-H "Authorization: Bearer $OPERATING_API_KEY"- Continue until
meta.nextCursorisnull:
{
"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 UTCupdatedBefore: ISO 8601 datetime in UTCupdatedSince: 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 projectpersonId: Filter time entries or positions by personclientId: Filter projects by clienttagId: 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
idascending - Some endpoints may use other default sort orders (e.g., time entries may sort by
datedescending)
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 Code | Meaning |
|---|---|
| 200 | Success |
| 400 | Bad Request — Invalid input data |
| 401 | Unauthorized — Missing or invalid API key |
| 403 | Forbidden — Insufficient permissions |
| 404 | Not Found — Resource doesn't exist or you don't have access |
| 500 | Internal 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
issuesarray 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
Authorizationheader - Invalid API key
- API key was regenerated (remember: creating a new key invalidates the old one)
How to fix:
- Verify the
Authorizationheader 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-DDformat (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
nullclears the value (if the field is nullable)