HabeoDevelopersAPI

REST API · v1 · updated May 25, 2026

One JSON over the institutional ledger.

Cursor-paginated reads over assets, people, and sites. Bearer-auth on every request. College-unit RBAC and tenancy enforced server-side — not in your client code, not in the prompt.

v1
Stable · additive-only
3.1
OpenAPI · codegen-ready
Bearer
Clerk JWT
RLS
Tenant-scoped at the row
At a glance

Everything you need on the back of a napkin.

Base URL
https://www.usehabeo.com/api/v1
Auth
Authorization: Bearer <clerk-jwt>
Format
JSON — UTF-8, ISO-8601 dates, UUIDs
Pagination
Opaque cursor via meta.nextCursor + ?cursor=
Rate limit
100 req/min/token · burst 200
Schema
/api/v1/openapi.json (OpenAPI 3.1)
Reference UI
/api/v1/docs (Swagger UI)
Status
v1 frozen · additive changes only
Resources

Four nouns. One verb.

v1 is deliberately small. Every endpoint is a paginated GET; every response is a { data, meta } envelope.

Session

GET /api/v1/me

Resolve the caller behind the bearer token — user, organization, and active role. Use this to confirm the token works and gate which UIs you render.

Assets

GET /api/v1/assets · /api/v1/assets/:id

The hardware and software lifecycle. Search by tag and serial, page through 25 at a time, fetch a single asset with assignment and location joins.

People

GET /api/v1/people

HR-sourced directory of staff, faculty, students, and contractors. Substring-matches on email, first name, last name. Mirrors what /people renders in the UI.

Sites

GET /api/v1/sites

Top-level location hierarchy — campuses, datacenters, leased facilities, virtual sites. The first level beneath the organization.

One request, one cursor, one envelope

What a v1 call looks like end-to-end.

Request
GET /api/v1/assets
curl "https://www.usehabeo.com/api/v1/assets?q=MBP&limit=2" \
  -H "Authorization: Bearer $HABEO_TOKEN" \
  -H "Accept: application/json"
Response · 200 OK
application/json
{
  "data": [
    {
      "id": "5d3a…-9f12",
      "assetTag": "ORG-MBP-22841",
      "serialNumber": "C02XJ1Q8JG5J",
      "status": "deployed",
      "typeName": "MacBook Pro 14\" (M3 Pro)",
      "assignedPersonName": "Mira Kahn"
    },
    {
      "id": "a8b1…-0c47",
      "assetTag": "ORG-MBP-22842",
      "serialNumber": "C02XJ1Q8JG5K",
      "status": "in_repair",
      "typeName": "MacBook Pro 14\" (M3 Pro)",
      "assignedPersonName": null
    }
  ],
  "meta": { "nextCursor": "eyJpZCI6ImE4YjEt…" }
}
Conventions

Boring on purpose.

We picked one way to do each thing and stuck with it. Less surface to argue with, less surface to break.

Envelope shape.

Every successful response is { "data": ..., "meta": ... }. List endpoints return an array under data; detail endpoints return an object. meta is reserved for pagination and (rarely) a total count.

{ data, meta }

Cursor pagination.

Opaque base64url cursors — never positional offsets. Pass meta.nextCursor back as ?cursor= to walk the next page. Absent cursor means you've reached the end.

?cursor=eyJpZCI6…

Search.

List endpoints accept ?q= for case-insensitive substring search across the most-asked-about columns (asset tag + serial, person email + name, site name + code). No DSL — keep it cheap.

?q=MBP

Limit clamping.

?limit= accepts 1–100 with a default of 25. Out-of-range values are clamped, not rejected — the response header X-Habeo-Limit-Applied tells you what we used.

?limit=25 · max 100

Dates and ids.

All timestamps are RFC 3339 / ISO 8601 in UTC with a trailing Z. All ids are RFC 4122 UUIDs unless otherwise noted (asset tags are free-form strings).

2026-05-25T15:42:09Z

Tenancy.

Tenancy is enforced server-side via row-level security. There is no orgId query parameter; the bearer token resolves a tenant scope and the database refuses to return rows outside it.

RLS · server-enforced
Error shape

One error envelope. Six codes that matter.

Every error response is { "error": { "code", "message", "details?" } }. Switch on error.code in your client — message is for humans.

error.codeHTTPWhen you'll see it
bad_request400Query parameters failed Zod validation.
unauthorized401Missing or invalid bearer token.
forbidden403Authenticated but the role/scope is not permitted.
not_found404No such resource in the caller's organization.
rate_limited429Per-token rate limit exceeded; Retry-After is set.
internal500Unhandled server error. Correlation id in X-Habeo-Request-Id.
Rate limits

100 req/min/token. Burst to 200.

The standard headers are on every response. Higher limits are a config flag — talk to us if you need them.

X-RateLimit-Limit

Total requests permitted in the current window.

X-RateLimit-Remaining

Requests remaining before throttling kicks in.

X-RateLimit-Reset

Unix timestamp when the window resets.

Retry-After

Seconds to wait, only set on a 429.

Versioning

A contract worth integrating against.

We don't break v1. The Habeo API is part of your audit posture; surprise renames are not on the table.

v1 is frozen.

Existing fields keep their names, types, and nullability for the lifetime of v1. Removing or renaming a field would be a breaking change and ships under /api/v2.

Additive is fair game.

We may add new optional fields, new endpoints, and new query parameters to v1 at any time. Your client should ignore unknown fields rather than fail closed.

12-month overlap.

When v2 ships, v1 runs alongside it for at least 12 months. Deprecation lives in the response Deprecation and Sunset headers before it lives in your inbox.

Changelog with each release.

Every v1 schema change is shipped through /api/v1/openapi.json with a build id and a human-readable changelog at /developers#changelog.

Frequently asked

API, answered.

The questions that come up in every integration review. Answered briefly, with the detail your platform engineer will ask for next.

Why is v1 read-only?
Writes against the institutional ledger move money, change capital classifications, and trigger grant-compliance disclosures. We exposed write paths through the in-product Copilot first (with a propose → confirm flow and the audit log threaded through) so we could earn the right to a general-purpose POST/PATCH surface. Those endpoints land under /api/v1 once the audit posture matches the read surface.
Is there a sandbox tenant?
Yes — every paid plan includes a sandbox organization seeded with realistic synthetic higher-ed data (the same seed used for /demo). Sandbox tokens are interchangeable with production tokens, so the only thing that changes is the org you point at.
Do tokens expire?
Yes. Bearer tokens are Clerk session JWTs and expire on the Clerk session schedule (default 60 minutes, refreshable via the Clerk client SDK). For backend-to-backend integrations, use a service-account user with a long-lived session — talk to us about provisioning one.
Can I get a webhook instead of polling?
Yes — see the webhooks section of the developer page. We publish outbound events for asset changes, ticket bridges, MDM enrollments, and Stripe billing. Payloads are HMAC-signed and idempotent by event id. For inbound integrations (ITSM, MDM, punchout) we accept signed POSTs to /api/webhooks/* and /api/punchout/return.
Do you support CORS?
Browser clients can hit /api/v1 from the Habeo domain itself. For cross-origin browser usage from a different domain (e.g. an internal staff portal), allowlist your origin from /admin/integrations — we'll echo it on the Access-Control-Allow-Origin header for the duration of the trust window.
How do I see what changed?
The OpenAPI spec at /api/v1/openapi.json is the source of truth and is regenerated on every deploy. Diff two versions of the spec to see precisely what moved. A human-readable changelog sits at /developers#changelog with the same build ids.
Try it now

curl, schema, sandbox. Pick a starting point.

The Swagger UI is the fastest way to feel out the surface. The OpenAPI spec is the fastest way to wire up a generated client. The demo call is the fastest way to wire up your real data.