RoadOpsDeveloper

Explore our guides to integrate RoadOps

Interact with RoadOps data however you wish.

Getting Started

The RoadOps Developer API exposes RESTful routes for reading and updating RoadOps data. Start with the path that matches whether you are working with your own data or building an application for other RoadOps users.

Base URL: https://api.roadops.app/v1

curl https://api.roadops.app/v1/teams \
  -H "Authorization: Bearer ro_live_..."

Interact with your data

Use a personal access token when an integration only needs to act as you. Personal access tokens are the simplest choice for private scripts, internal tools, backend jobs, and personal automation that work with your own RoadOps data or data available through your teams.

Choose the narrowest scopes the integration needs, copy the token when it is created, and store it like a password. Personal access tokens use the prefix ro_live_ and are shown only once.

Never distribute a personal access token to other users or embed one in browser, mobile, desktop, or other publicly distributed application code. A website using a personal access token should call its own backend, which then calls RoadOps. Treat personal access tokens as server-side secrets.

Integrate RoadOps to an app

Use OAuth when an application will be used by other RoadOps users and each person should connect and authorize access to their own account. OAuth is the appropriate choice for third-party products, multi-user integrations, single-page applications, native applications, and any distributed product that cannot safely share one person's credentials.

Register the integration from OAuth Apps, request only the scopes it needs, and implement Authorization Code with PKCE. Request refresh-token access when the integration must continue working after the interactive session. Browser applications may send OAuth access tokens directly to supported Developer API resource routes.

Each person approves the requested access. Every request continues to reflect that person's current RoadOps teams, permissions, visibility, and resource ownership.

Authorization

Choosing between personal access tokens and OAuth

Use a personal access token when only you need access to your own RoadOps data or data available through your teams, such as for an internal tool or personal automation. Build an OAuth app when other RoadOps users will use your product and need to connect their own accounts.

Access and permission boundaries

Both credential types use the same permission-scope catalog. Effective access is always limited by both the credential's scopes and the authenticated user's current RoadOps teams, permissions, visibility, and resource ownership. Neither credential can grant access the user does not already have.

Authorizing API requests

Send either credential as a RoadOps bearer token in the Authorization header for every API request.

Authorization: Bearer YOUR_TOKEN

Core Principles

Mutation endpoints (create/update/delete) return 202 Accepted with a receipt instead of returning the fully projected resource. The receipt includes a cursor value for the accepted update and a links.refetch URL for the resource that should be read again.

Update endpoints accept incremental payloads. Send only the properties that need to change; you do not need to resend the full resource. To clear a value, use the clearing form documented for that property: usually null when the schema marks the field nullable, or an empty string only when the field explicitly allows an empty string.

Eventual Consistency

RoadOps operates on the principle of eventual consistency: accepted changes are processed asynchronously, then projected into the read models used by API reads. The cursor exists so integrations can know when that projection has caught up to a specific accepted change.

Every record returned by the API includes its latest cursor. Read responses also include meta.cursor, which is the highest cursor represented by that response. After dispatching a change, refetch the resource or list endpoint until the returned record cursor, or response meta.cursor, is greater than or equal to the mutation receipt cursor.

That comparison tells your integration that RoadOps has projected the accepted change into the public read model. Until then, treat earlier reads as still valid but not yet caught up to the command you just dispatched.

Mutation requests may include an optional Idempotency-Key header. The value must be a caller-generated UUID, and the same UUID should only be reused when retrying the exact same request. When supplied, RoadOps stores the accepted response for that key and returns the same response if the caller retries the same request, which helps protect create/update/delete calls from duplicate effects after network failures. If the header is omitted, the mutation is processed normally without retry replay protection.

Visibility

Some records include a visibility property that narrows who can see that record inside an otherwise accessible team. Visibility does not grant access by itself: the token still needs the required scope, the token owner still needs access to the team, and the endpoint permission checks still apply.

When visibility is omitted, null, or contains no groupIds or userIds, the record follows normal team access. When visibility contains values, non-admin users can see the record only when their RoadOps user ID appears in userIds or they belong to one of the Travel Parties listed in groupIds. Team admins generally retain access to team records for management unless an operation documents a narrower rule.

{
  "visibility": {
    "groupIds": ["00000000-0000-4000-8000-000000000301"],
    "userIds": ["00000000-0000-4000-8000-000000000101"]
  }
}

groupIds are Travel Party IDs. userIds are RoadOps user IDs from member records. All values must be UUIDs. On update endpoints that support clearing custom visibility, send visibility: null to return the record to normal team access. For day file uploads, include the visibility object in the JSON body sent to the completion endpoint.

Receive changes with webhooks

Use webhooks to receive RoadOps changes without polling the Developer API. Because RoadOps is eventually consistent, webhook delivery begins only after the originating change has reached the projected read models used by API reads.

Webhook payloads contain the current Developer API representation of the resource. Before every delivery, RoadOps applies the webhook owner's current scopes, team permissions, record visibility, ownership rules, and private-resource protections.

Start with the Webhooks guide, then complete the Receiver Readiness checklist before sending production traffic to your endpoint.

Pagination

List endpoints accept limit and cursor query parameters. Responses include page.nextCursor; pass that value back as cursor to fetch the next page. A null page.nextCursor means there are no more pages.

Rate Limits

The standard Developer API limit is 120 requests per minute for each credential, measured in fixed one-minute windows. A personal access token has its own limit. OAuth requests are limited separately for each combination of OAuth app and authenticated RoadOps user, so one user's traffic does not consume another user's allowance.

Requests over the limit return 429 Too Many Requests with the error code rate_limited. Use the response's Retry-After header to determine how many seconds to wait before retrying. RoadOps does not currently return remaining-request or quota headers on successful responses.

Avoid tight polling loops. Cache stable data where appropriate, use pagination deliberately, and apply exponential backoff with jitter after transient failures. Separate network-level protections may also reject unusually high traffic from a source before it reaches the per-credential limit.

Common Response Codes

RoadOps responses use a consistent envelope so clients can branch on the HTTP status first, then inspect the JSON payload for details.

200 OK

Returned by successful read endpoints. List endpoints return a data array plus page and meta objects. Retrieve endpoints return a single resource in data.

{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000100",
      "name": "RoadOps Touring",
      "cursor": 87234
    }
  ],
  "page": {
    "limit": 50,
    "nextCursor": null
  },
  "meta": {
    "cursor": 87234
  }
}

202 Accepted

Returned by mutation endpoints after RoadOps accepts a create, update, or delete request for processing. The read model may not reflect the change immediately. Use cursor with the returned links.refetch URL, or with a later list response meta.cursor, to know when the change has been projected.

{
  "commandId": "00000000-0000-4000-8000-000000000010",
  "resourceId": "00000000-0000-4000-8000-000000000301",
  "resourceType": "schedule_item",
  "status": "applied",
  "aggregateVersion": 2,
  "cursor": 87234,
  "links": {
    "refetch": "/v1/teams/00000000-0000-4000-8000-000000000100/days/00000000-0000-4000-8000-000000000201/schedule-items/00000000-0000-4000-8000-000000000301"
  }
}

400 Bad Request

Returned when the request cannot be parsed or has an invalid shape before resource-level validation can run. Check that the request body is valid JSON, the Content-Type is application/json for JSON requests, and path/query values use the expected format.

{
  "error": {
    "code": "bad_request",
    "message": "Request body must be valid JSON."
  }
}

401 Unauthorized

Returned when the request is missing a bearer token or the personal access token or OAuth access token is malformed, expired, revoked, or otherwise invalid. Confirm the full credential was sent as Authorization: Bearer YOUR_TOKEN. Replace an invalid personal access token or restart OAuth authorization when the user-delegated credential can no longer be refreshed.

{
  "error": {
    "code": "unauthorized",
    "message": "Bearer token is required"
  }
}

403 Forbidden

Returned when the credential is valid but does not grant the scope required by the endpoint, or the authenticated user lacks the required RoadOps team permission. Request the narrow required scope shown in the operation reference, such as schedule_item:read or schedule_item:write, and confirm the user has the corresponding team access in RoadOps.

{
  "error": {
    "code": "insufficient_scope",
    "message": "Missing required scope: schedule_item:write"
  }
}

When the credential has the required scope and the authenticated user is an active member of the team, but that membership lacks the corresponding RoadOps permission, the API returns:

{
  "error": {
    "code": "permission_denied",
    "message": "You do not have permission to perform this action."
  }
}

404 Not Found

Returned when the route does not exist, the authenticated user has no active membership in the requested team, or an authorized request references a resource that does not exist, was deleted, is hidden by visibility rules, or is outside the requested parent resource. Team membership failures intentionally use the same response so team IDs cannot be enumerated. Verify the endpoint path and resource IDs. For deleted resources, refetch the parent list to confirm current state.

{
  "error": {
    "code": "not_found",
    "message": "Not Found"
  }
}

409 Conflict

Returned when a mutation cannot be accepted because it conflicts with current resource state. Common causes include stale versions, duplicate caller-supplied IDs, or reusing an Idempotency-Key for a different request body. Refetch the resource, apply your change to the latest state, and retry. If this is an idempotency conflict, generate a new UUID for Idempotency-Key unless you are retrying the exact same request.

{
  "error": {
    "code": "conflict",
    "message": "The request conflicts with the current resource state."
  }
}

422 Unprocessable Entity

Returned when JSON is valid but the command validator rejects the payload. Use the details object to map validation messages back to form fields or request properties, then resend the request with the corrected values.

{
  "error": {
    "code": "validation_failed",
    "message": "Request validation failed.",
    "details": {
      "name": ["Name is required."]
    }
  }
}

429 Too Many Requests

Returned when the credential exceeds the API rate limit. Back off requests, avoid tight polling loops, and retry after the window resets. When the response includes Retry-After, wait at least that many seconds before retrying.

{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded",
    "details": {
      "limit": 120
    }
  }
}

500 Internal Server Error

Returned when RoadOps cannot complete the request because of an unexpected platform error. Retry with exponential backoff for safe reads. For mutations, use an Idempotency-Key so a retry can safely replay the same accepted response if the first attempt reached RoadOps.

RoadOps automatically monitors these errors, so our team has likely already been alerted. If the problem continues or you need help with a specific request, open an inquiry with RoadOps Support.

{
  "error": {
    "code": "internal_error",
    "message": "An unexpected error occurred."
  }
}