Authorize

#Auth & API Gateway

Every Helm API call goes through one gateway. It authenticates the caller, applies rate limits, and routes the request to the surface that owns it.

#Base URLs

Environment URL
Production https://api.helm.bridge-labs.com
Staging https://api.staging.bridge-labs-dev.com

The interactive console on this site (the Try it buttons and the API reference) targets staging by default, so experimenting never touches production.

#Authentication

Authenticate with a Personal Access Token using HTTP Basic auth — your client_id and client_secret encoded as base64(client_id:client_secret) in the Authorization header. To create a token, see Provisioning.

Enter your token once in the Authorize bar at the top of this page, then Send the call below — it returns the actions your token is allowed to perform, which is the quickest way to confirm your credentials work:

GET/access/me/permissionsWho am I — verify the tokenAPI docs ↗Try it

A 401 means the token is missing, malformed, expired, or revoked.

#Routes

The gateway routes by path prefix:

Prefix What lives here
/entity/* Entity types, entities, relationships, query, GraphQL, artifacts
/schema/* JSON schemas and validation
/definition/* Workflow, job, and app-spec definitions, plus owner shares
/workflows/* Workflow activation, triggering, and run monitoring
/apps/* App activation (config, integrations, tokens) and render
/access/* Users, tokens, roles, policies, grants, shares, manifests, secrets
/event/* Event publishing and processing

GET /healthz is unauthenticated and reports gateway health.

Two other routes are reachable without an Authorization header, both because the caller is a browser holding no credentials, and both authorised by a signed capability in the URL rather than by a token:

Route Who follows it
GET /entity/temp-files/{id}/download anyone holding a minted download link
POST /apps/upload/{integrationId}/{uploadId} a provider's upload widget, POSTing the picked file

The carve-outs are exact — the sibling temp-file routes stay authenticated — and the upload ingest gets its own body-size ceiling (25 MiB) rather than the 1 MiB default that applies to ordinary API calls.

#Rate limits

Production applies two independent limits, each over a rolling 60-second window:

Limit Ceiling Applied Keyed on
Per-IP 10,000 req / 60 s before auth the originating client IP
Per-identity 1,000 req / 60 s after auth the authenticated identity (see below)

A request must satisfy both. The per-IP ceiling is deliberately generous so a shared egress (an office or VPN where many users share one IP) isn't throttled as a group; in normal authenticated use you reach the per-identity 1,000/60 s limit first. "Identity" is whatever credential you present, and each gets its own independent budget:

  • a signed-in user (per user);
  • an API token / machine client (per client);
  • an activated app rendering for one of its end users (per end user, per integration) — so one busy end user can't exhaust every other end user of the same app;
  • an inbound webhook (per trigger).

Because the windows are rolling, capacity recovers continuously as your older requests age out of the trailing 60 seconds — there's no clock-aligned reset to wait for.

When you exceed a limit the API returns HTTP 429:

{
  "error": {
    "status": 429,
    "code": "TOO_MANY_REQUESTS",
    "message": "Too many requests. Please try again later.",
    "details": { "scope": "identity", "limit": 1000 }
  }
}

details.scope is "ip" or "identity" (which limit you hit) and details.limit echoes that ceiling; no identifying data is included. There is no Retry-After header — back off (exponential, with jitter) and retry. On successful responses the API returns X-Rate-Limit-Limit and X-Rate-Limit-Remaining so you can pace yourself; these are not present on a 429. To stay under the limits: batch where the API supports it, cache reusable responses, and spread bulk work over time rather than bursting.

#Responses

Every success body wraps the resource in data:

// single resource (200/201)
{ "data": { "id": "…", "name": "…" } }

// list (200) — pagination is a SIBLING of data, never inside it
{ "data": [ … ], "pagination": { "page": 1, "limit": 20, "total": 42, "total_pages": 3 } }

Keyset-paginated endpoints return a next_cursor sibling instead of pagination, and only when a full page came back — its absence means you have reached the end. Paging is forward-only; there is no prev_cursor.

Token-mint endpoints return the raw secret once — capture it then, because it is stored only as a hash and is never retrievable again. Where it sits depends on the endpoint: minting a PAT (POST /auth/tokens/api, POST /access/api-tokens) returns client_secret inside data alongside its client_id, while issuing an app token (PUT /apps/{specId}/integrations/{integrationId}/token) returns raw_token as a sibling of data, so audit-log redaction can target that one key.

#Errors

Every error uses one envelope:

{
  "error": {
    "status": 422,
    "code": "VALIDATION_FAILED",
    "message": "Human-readable explanation.",
    "details": {}
  }
}

The HTTP status is authoritative; code is a stable machine-readable string and details is code-specific (validation errors carry the offending JSON path; conflicts carry the conflicting id). Common codes: 400 BAD_REQUEST, 401 UNAUTHORIZED, 403 FORBIDDEN, 404 NOT_FOUND, 409 CONFLICT, 422 VALIDATION_FAILED, 429 TOO_MANY_REQUESTS.

Two codes share the 422: VALIDATION_FAILED when the body or query failed schema validation, and UNPROCESSABLE_ENTITY when it parsed but the request could not be carried out. Branch on the status; use code to tell the two apart.

A VALIDATION_FAILED carries details.validation_errors: one "<path>: <reason>" line per problem, pointing at the value that caused it.

{
  "error": {
    "status": 422,
    "code": "VALIDATION_FAILED",
    "message": "Invalid request body",
    "details": {
      "validation_errors": [
        "filter.operator: expected one of \"eq\"|\"neq\"|\"gt\"|\"lt\"|\"in\"",
        "filter.value: Invalid input: expected array, received string"
      ]
    }
  }
}

Where a field accepts one of several shapes, the reasons are merged rather than repeated per shape: a bad operator comes back as one line naming every operator that field accepts, and a pair of mutually-exclusive fields names both. Array positions are included, so a bad filter nested in a composite reports filter.filters.0.operator. Beyond twenty lines the rest are counted rather than listed. details.errors repeats the same lines.

#Conventions

  • Casing — request/response fields are snake_case; the only camelCase surface is an app spec's spec_document (the authored definition), which the platform translates at the boundary.
  • MethodsGET reads, POST creates (201), PUT replaces or mints, PATCH partially updates, DELETE removes.
  • Idempotency — endpoints that support it read an Idempotency-Key header; duplicate keys within the dedup window return the original response verbatim. It is opt-in per endpoint — see the relevant guide.

#API reference

The complete, interactive API — every route, schema, and a Try-It console — is at /openapi/.