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.

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

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

Authorization

Send your personal access token in the Authorization header for every API request.

Authorization: Bearer ro_live_...

Personal access tokens use the prefix ro_live_. Tokens are shown once and should be stored like passwords.

RoadOps restricts browser CORS access for authenticated API requests because a personal access token should never be embedded in frontend website code. Treat personal access tokens as server-side secrets. For website integrations, your website should call your backend, and your backend should call the RoadOps API with the personal access 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.

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 multipart uploads, such as day file uploads, send the same visibility object as a JSON string in the visibility form field.

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.

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.

{
  "status": "accepted",
  "resource": "schedule_item",
  "id": "00000000-0000-4000-8000-000000000201",
  "cursor": 87235,
  "links": {
    "refetch": "/v1/teams/00000000-0000-4000-8000-000000000100/days/00000000-0000-4000-8000-000000000200/schedule-items/00000000-0000-4000-8000-000000000201"
  }
}

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, the token is malformed, the token is expired or revoked, or the token secret does not match an active personal access token. Send Authorization: Bearer ro_live_..., confirm the token was copied completely, and create a replacement token if it has been revoked or expired.

{
  "error": {
    "code": "unauthorized",
    "message": "A valid personal access token is required."
  }
}

403 Forbidden

Returned when the token is valid but does not grant the scope required by the endpoint, or the token owner cannot access the requested team/resource. Create or update the token with the narrow required scope shown in the operation reference, such as schedule_item:read or schedule_item:write, and confirm the user can access the RoadOps team in the app.

{
  "error": {
    "code": "insufficient_scope",
    "message": "The personal access token does not include the required scope.",
    "details": {
      "requiredScope": "schedule_item:write"
    }
  }
}

404 Not Found

Returned when the route does not exist or the requested resource cannot be found within the token owner's accessible teams. Verify the endpoint path, resource IDs, and team access. For deleted resources, refetch the parent list to confirm current state.

{
  "error": {
    "code": "not_found",
    "message": "The requested resource was 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 token or client 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": "Too many requests. Please retry later."
  }
}

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.

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