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.
Authorization: Bearer <accessToken>
Accept: application/jsonRoles 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.
# 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}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`);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")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.
| Resource | Endpoint | Verbs |
|---|---|---|
| Assets | /api/assets | GET, POST, PATCH, DELETE |
| Work orders | /api/work-orders | GET, POST, PATCH, DELETE |
| PM schedules | /api/pm-schedules | GET, POST, PATCH, DELETE |
| Spare parts | /api/spare-parts | GET, POST, PATCH, DELETE |
| Purchase orders | /api/purchase-orders | GET, POST, PATCH |
| Vendor invoices | /api/vendor-invoices | GET, POST |
| Contracts | /api/contracts | GET, POST, PATCH, DELETE |
| Users and roles | /api/users | GET, POST, PATCH, DELETE |
| Audit log | /api/audit-logs | GET (read-only) |
| Webhook subscriptions | /api/admin/webhooks | GET, POST, PATCH, DELETE |
| API keys | /api/admin/api-keys | GET, 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.
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.
curl -s "https://<workspace>.nexteam.me/api/bi/work_orders?limit=5000" \
-H "X-API-Key: bi_live_..." -o work_orders.csvEntities 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.