API Overview

One integration surface: GraphQL โ€” endpoint, authentication, daily quotas, and what an API key actually reaches

4 min read
Print this section

FreeMaint has one API surface for machine-to-machine integration: GraphQL, at /api/v1/graphql. The REST routes you see in the browser are the application's own API and are authenticated by a user session โ€” an API key does not reach them. Plan for GraphQL when you integrate.

REST API

The REST routes under /api/v1/{resource} are what the FreeMaint web and mobile apps call, authenticated with a user session (JWT). An API key (fmk_...) does NOT authenticate them โ€” it is accepted on /api/v1/graphql and on the energy ingest endpoint only. If you are building an integration, read the GraphQL section below; the REST paths are documented here because you will see them in your browser's network tab, not because a key can call them.

Example (user session, NOT an API key โ€” a fmk_ key will not authenticate this route): curl -H 'Authorization: Bearer <session-token>' 'https://freemaint.com/api/v1/work-orders?status=OPEN&assigneeId=42&page=1&pageSize=20'

GraphQL API

The GraphQL endpoint is POST /api/v1/graphql. It supports the full Query/Mutation surface for the same resources plus relationship traversal. Use GraphQL when you would otherwise have to call several REST endpoints to assemble one screen โ€” for example, a dashboard tile that shows a work order with its asset details, parts used, and total labor cost.

Example query: query { workOrders(status: "OPEN", limit: 10) { id reference title status priority assetId dueDate } }

Authentication

Both APIs accept a JWT bearer token in the Authorization header, issued by POST /api/v1/auth/login and valid for 2 hours (refresh with the refresh token). For server-to-server integrations use an API key instead: create it in Company Settings > API (/company-settings/api) and send Authorization: Bearer fmk_... . IMPORTANT: an API key authenticates the GraphQL endpoint (POST /api/v1/graphql), the energy ingest endpoint and /public-api/{whoami,ping} โ€” it does NOT authenticate the REST resource routes, which require a user JWT.

Header

Authorization: Bearer <token>

Token TTL

Access token: 2 hours. Refresh token: 30 days.

Service accounts

Business+ tier โ€” issue a non-expiring token tied to a specific role

Tier availability

Business

API keys, GraphQL reads and writes, 5,000 calls per day

Enterprise

Everything in Business, 100,000 calls per day, plus the energy ingest endpoint

Rate limits

The quota is a DAILY counter per company, shared between GraphQL and the energy ingest endpoint, and it resets at 00:00 UTC. Business allows 5,000 calls per day, Enterprise 100,000. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. There is no per-minute window and no Retry-After header. On the REST surfaces the refusal is HTTP 403; on GraphQL the HTTP status stays 200 โ€” GraphQL always answers 200 โ€” and the refusal is in errors[0].extensions.code = FORBIDDEN. Do not poll for HTTP 403 on GraphQL: read the errors array.

Error format

Every error carries an HTTP status, a machine-readable code and a human-readable message. 4xx are client errors (validation, permission, not found); 5xx are server errors and should be retried with exponential backoff. On GraphQL the code is in extensions.code, and the values you can pattern-match are: BAD_REQUEST, UNAUTHENTICATED, FORBIDDEN, NOT_FOUND, CONFLICT, BAD_USER_INPUT and TOO_MANY_REQUESTS. When input validation fails the message names the exact fields at fault โ€” for example "quantity must be a number conforming to the specified constraints" โ€” so you never have to guess which one your system got wrong. A lookup that simply matches nothing is not an error: part(barcode:) returns null.

Official SDKs and tooling

There is no official client SDK today. The GraphQL endpoint has introspection enabled, so any GraphQL client (Apollo, graphql-codegen, Insomnia, Postman) can discover the schema by pointing at POST /api/v1/graphql with your API key. We do not currently publish a static OpenAPI document or an SDL file at a fixed URL.

Tip

URLs include /v1 โ€” when we introduce breaking changes (rare), we ship /v2 alongside and keep /v1 supported for at least 12 months before deprecation.

API keys are not available on Core or Starter.

Was this page helpful?