Skip to content

Developers

Build on the NextEAM API.

The same REST API the NextEAM screens are built on, served under /api on your workspace. Sign in for a JWT, subscribe to events with signed webhooks, and feed Power BI or any OData client with a scoped API key. One surface, no shadow API.

Authentication

One JWT, scoped to your workspace.

Every request carries a bearer token issued by POST /api/auth/login. The token already names the workspace and the user it belongs to, so there is no tenant header to send: a request can only ever read and write its own workspace.

Sign in against your workspace address (https://<workspace>.nexteam.me) or against nexteam.me with workspaceSlug in the body. An account with MFA enabled receives { mfaRequired: true, partialToken } instead of a token; exchange it at POST /api/auth/verify-totp with the six-digit code.

Tokens expire after 8 hours by default and are invalidated at once by a sign-out-everywhere or an admin revocation. Sign in again to get a new one; there is no refresh endpoint.

Required header2 lines
Authorization: Bearer <accessToken>
Accept: application/json

Roles and permissions apply to API calls exactly as they apply in the app: a technician's token cannot approve a purchase order, and a viewer's token cannot write anything.

Code examples

curl, JavaScript, Python: one surface.

curl11 lines
# 1. Sign in. The token is scoped to your workspace and your user.
curl -s -X POST https://<workspace>.nexteam.me/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"..."}'
# -> {"accessToken":"eyJ...","user":{...}}

# 2. Call the API with it.
curl -s "https://<workspace>.nexteam.me/api/work-orders?status=OPEN&pageSize=50" \
  -H "Authorization: Bearer eyJ..." \
  -H "Accept: application/json"
# -> {"items":[...],"total":312,"page":1,"pageSize":50}
JavaScript (fetch)13 lines
const base = 'https://<workspace>.nexteam.me/api';

const { accessToken } = await fetch(`${base}/auth/login`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email, password }),
}).then((r) => r.json());

const page = await fetch(`${base}/work-orders?status=OPEN&pageSize=50`, {
  headers: { Authorization: `Bearer ${accessToken}` },
}).then((r) => r.json());

console.log(`${page.total} open work orders`);
Python (requests)15 lines
import requests

base = "https://<workspace>.nexteam.me/api"
token = requests.post(
    f"{base}/auth/login",
    json={"email": email, "password": password},
).json()["accessToken"]

page = requests.get(
    f"{base}/work-orders",
    params={"status": "OPEN", "pageSize": 50},
    headers={"Authorization": f"Bearer {token}"},
).json()

print(f"{page['total']} open work orders")
Webhook delivery (NextEAM to your endpoint)10 lines
POST https://your-app.example.com/webhooks/nexteam HTTP/1.1
Content-Type: application/json
X-Webhook-Signature: sha256=3f1a...   (HMAC-SHA256 of the raw body, keyed with your secret)

{
  "eventType": "work_order.completed",
  "tenantId": "...",
  "occurredAt": "2026-09-22T08:14:22.110Z",
  "payload": { ... }
}

There is no official SDK. The API is plain JSON over HTTPS, so any HTTP client works; the examples above are exactly what the NextEAM frontend itself does.

Core resources

REST resources under /api.

Lists take page and pageSize (at most 200) and answer { items, total, page, pageSize }. Unknown query or body fields are rejected with 400, so a typo never silently widens a filter. Every resource is scoped to the workspace in the token.

Core resources
ResourceEndpointVerbs
Assets/api/assetsGET, POST, PATCH, DELETE
Work orders/api/work-ordersGET, POST, PATCH, DELETE
PM schedules/api/pm-schedulesGET, POST, PATCH, DELETE
Spare parts/api/spare-partsGET, POST, PATCH, DELETE
Purchase orders/api/purchase-ordersGET, POST, PATCH
Vendor invoices/api/vendor-invoicesGET, POST
Contracts/api/contractsGET, POST, PATCH, DELETE
Users and roles/api/usersGET, POST, PATCH, DELETE
Audit log/api/audit-logsGET (read-only)
Webhook subscriptions/api/admin/webhooksGET, POST, PATCH, DELETE
API keys/api/admin/api-keysGET, POST

The full route list lives in the OpenAPI document below. Work-order status changes accept an X-Idempotency-Key header (16–64 characters) so a retried request cannot apply twice.

Webhooks

Signed deliveries, retried on failure.

An org admin subscribes an endpoint to events at Admin → Webhooks (POST /api/admin/webhooks with name, url and eventTypes). The signing secret is shown once at creation and can be rotated at POST /api/admin/webhooks/:id/regenerate-secret.

Every delivery is signed: X-Webhook-Signature carries sha256= followed by the HMAC-SHA256 of the raw request body, keyed with the subscription secret. Verify it before trusting the payload.

Verifying the signature (Node)9 lines
import { createHmac, timingSafeEqual } from 'node:crypto';

// rawBody is the request body BEFORE any JSON parsing.
const expected =
  'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
const given = String(req.headers['x-webhook-signature'] ?? '');
const ok =
  given.length === expected.length &&
  timingSafeEqual(Buffer.from(given), Buffer.from(expected));

A non-2xx answer or a 10-second timeout is retried after 1, 5, 15 and 60 minutes and then 4 hours (five attempts over roughly five hours) and then the delivery is marked FAILED. Every attempt is listed at GET /api/admin/webhooks/:id/deliveries, and POST /api/admin/webhooks/:id/test-fire sends a synthetic work_order.created so you can prove a receiver before a real event reaches it.

Events GET /api/admin/webhooks/event-catalog

  • work_order.created
  • work_order.assigned
  • work_order.completed
  • purchase_requisition.submitted
  • purchase_requisition.approved
  • contract.activated
  • contract.expiring
  • permit.activated
  • permit.closed
  • calibration.recorded
  • calibration.failed
  • invoice.issued
  • invoice.paid
  • invoice.overdue
  • payment.recorded
  • approval_request.created
  • approval_request.step_advanced
  • approval_request.approved
  • approval_request.rejected
  • meter.reading_recorded
  • meter.threshold_crossed

Endpoints that resolve to private, loopback or link-local addresses are refused, and a delivery that follows a redirect to another origin drops its signature header.

API keys and BI feed

Read-only keys for Power BI, Metabase and OData clients.

API keys do not replace sign-in. They exist for BI tools that cannot hold a JWT: an org admin creates one at Admin → API keys (POST /api/admin/api-keys), scopes it to the entities it may read, gives it an expiry, and copies the bi_live_… key once; it is never shown again.

Send it as X-API-Key. GET /api/bi/:entity streams CSV with keyset pagination (?after=<id>, up to 5,000 rows a page, with X-BI-Next-Cursor and X-BI-Row-Count headers). GET /api/bi/odata serves an OData service document and $metadata for Power BI and Excel.

BI feed2 lines
curl -s "https://<workspace>.nexteam.me/api/bi/work_orders?limit=5000" \
  -H "X-API-Key: bi_live_..." -o work_orders.csv

Entities a key can be scoped to

  • work_orders
  • assets
  • invoices
  • payments
  • audit_log
  • pm_schedules
  • spare_parts
  • contracts
  • vendors

A revoked or expired key answers 401; a valid key asking for an entity outside its scope answers 403. The feed is throttled at 300 requests a minute per IP address.

Rate limits and errors

Generous by default, strict on credentials.

  • API, per IP address

    Limit
    600 requests / minute
  • Sign-in, per account

    Limit
    5 attempts / 15 minutes
  • BI feed, per IP address

    Limit
    300 requests / minute

Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; an exceeded limit answers 429 with a Retry-After header. Validation failures are 400 and name the offending fields; a missing or expired token is 401; a role without the permission is 403; a unique-constraint clash (a duplicate code, say) is 409.

OpenAPI

The generated reference.

The backend generates an OpenAPI 3 document from its controllers and serves Swagger UI at /api/docs, with the raw document at /api/docs-json. It is switched off on production workspaces unless the operator sets EXPOSE_SWAGGER=true, and switched on in every non-production environment.

Need the document for your production workspace? Ask us and we will share the current export together with your API guide.

Integrations

Build on REST and webhooks.

Every integration NextEAM ships (ERP posting, ticket sync, BI dashboards) uses the surface on this page and nothing else. That is the guarantee: what you can build is what we build with.

  • ERP (SAP, Oracle Fusion)

    Requisitions, purchase orders and vendor invoices are ordinary resources: read them with a token, write cost-centre alignment back with PATCH, and react to purchase_requisition.approved or invoice.issued through a webhook. The ERP connector itself is scoped and delivered with you during onboarding.

  • Service desk and IT

    Tickets, IT assets and discovery runs are all reachable under /api, so an inventory agent can push what it finds the same way the CSV import does.

  • BI and reporting

    Point Power BI, Metabase or Excel at the API-key feed for live, workspace-scoped extracts, with no export files to schedule.

Building something? Tell us.

Integration partners get an architect to walk through the deployment and a review before the integration goes live.