Skip to content

Latest commit

 

History

History
303 lines (256 loc) · 23.1 KB

File metadata and controls

303 lines (256 loc) · 23.1 KB

REST API

Base URL: https://<your-domain>/api/v1. All bodies are JSON (except uploads) and all timestamps are ISO-8601 with a time zone. The OpenAPI document and Swagger UI (/docs) are available outside production and disabled in production.

Authentication

Method How
User session POST /auth/login {email, password} returns an access_token (10 min) and a refresh_token. If MFA is enabled it instead returns {mfa_required: true, mfa_token, expires_in, methods}; finish with POST /auth/mfa/verify {mfa_token, code} (a TOTP code or a recovery code) or with a passkey (below). Send Authorization: Bearer <access_token>.
Refresh POST /auth/refresh {refresh_token} issues a new pair. Refresh tokens are single-use; presenting a used one revokes the whole session.
API key Create with POST /api-keys {name, role, scopes[], expires_in_days}. The nxf_… token is shown once; send it as Authorization: Bearer nxf_…. Scopes must be a subset of the role's permissions.
OAuth2 password form POST /auth/token (form-encoded), for tools that expect the OAuth2 password flow.
Single sign-on Through the organization's OpenID Connect identity provider: POST /auth/sso/start, then POST /auth/sso/callback (below). The session it opens is bound to that organization. See SSO.md.
SCIM token nxp_…, created by an owner; valid on /scim/v2 only (below).

The public signing keys are published at /.well-known/jwks.json.

Sign-up proves the e-mail address first. POST /auth/register {email} answers 202 {"status": "check_email"} for every address and mails it a link (/complete-signup#token=…, valid 24 hours); an address that already has an account is sent a notice instead, so the answer reveals nothing. Whoever opens the link finishes with POST /auth/register/complete {token, password, full_name, organization_name}, which returns 201 and a token pair for the new owner. An invitation creates the account directly - its link already went to the address: POST /auth/register/invitation {token, password, full_name} (an existing account accepts with /auth/invitations/accept instead; this endpoint answers 409 account_exists for it). Starting is limited to 10 an hour per client and 3 an hour per address; finishing to 60 an hour per client.

Other authentication endpoints:

  • sign-up: /auth/register, /auth/register/complete, /auth/register/invitation;
  • sign-out: /auth/logout, /auth/logout-all;
  • passwords: /auth/password/change, /auth/password/reset-request, /auth/password/reset;
  • MFA: /auth/mfa/enroll, /auth/mfa/confirm (for a member of an organization that requires passkeys who has one, only from a session that signed in with one: 403 passkey_session_required), /auth/mfa/disable (turns every second factor off: the authenticator app, all passkeys and the recovery codes - for a member of an organization that requires passkeys, only from a session that signed in with one: 403 passkey_session_required);
  • organizations: /auth/switch-organization, /auth/invitations/accept;
  • single sign-on: /auth/sso/start, /auth/sso/callback.

Passkeys (WebAuthn) are a second factor equal to TOTP; see PASSKEYS.md. Options and credentials use WebAuthn's own JSON forms (PublicKeyCredential.parseCreationOptionsFromJSON, credential.toJSON()), with binary values as unpadded base64url.

Method Path
POST /auth/webauthn/register/begin {password} Creation options ({options, expires_in}): a single-use challenge for five minutes, bound to the account and session. From a session only; once the account has a second factor, from a session that passed one (403 mfa_session_required); for a member of an organization that requires passkeys, once the account has one, from a session that signed in with one of them (403 passkey_session_required)
POST /auth/webauthn/register/finish {name?, credential} 201 {passkey, recovery_codes} - the codes only when this passkey turned MFA on. 422 passkey_rejected names the reason in details[0].code; 409 passkey_exists, 409 too_many_passkeys (ten at most)
GET /auth/webauthn/credentials My passkeys: name, algorithm, transports, backup flags, times - no key material
PATCH /auth/webauthn/credentials/{id} {name} Rename
POST /auth/webauthn/credentials/{id}/delete {password} Remove; 409 last_second_factor for the last passkey of an account without TOTP; 403 passkey_session_required as for registering
POST /auth/mfa/webauthn/begin {mfa_token} Request options for the sign-in the mfa_token belongs to (409 no_passkeys if the account has none)
POST /auth/mfa/webauthn/verify {mfa_token, credential} The token pair; any refusal is 401 mfa_failed and counts toward the lockout

Passkey endpoints answer 503 passkeys_unavailable where the public URL's host is not a domain name.

Single sign-on (OpenID Connect, authorization code flow with PKCE; details and the security model in SSO.md):

Step Request Answer
Start POST /auth/sso/start {organization: "<slug>"} 200 {authorization_url, state, binding, expires_in}. Send the browser to authorization_url; keep binding in memory (never in a URL). 404 sso_not_available - the same for an unknown organization and one without usable single sign-on; 503 sso_unavailable if the provider's discovery document cannot be used
Callback The provider redirects to {public_base_url}/sso/callback?code=…&state=…; the front end checks state and posts POST /auth/sso/callback {code, state, binding} 200 token pair - or {mfa_required: true, mfa_token, expires_in, methods} when the organization requires MFA the provider did not provide and the person has the platform's second factor: finish with /auth/mfa/verify (a TOTP or recovery code) or with a passkey (/auth/mfa/webauthn/begin, then /verify) - either way into a session bound to the organization. An organization that requires passkeys always asks, whatever the provider reported, with methods: ["webauthn"]: only a passkey finishes it

Callback errors: 401 sso_failed for anything about the state (unknown, used, expired), the binding or the ID token (one message, the reason is in the organization's audit trail); 403 sso_email_not_verified, 403 sso_domain_not_allowed (not an address in one of the organization's verified domains), 403 sso_account_not_managed (from Google: an account the organization's Workspace does not manage - no hd claim naming a verified domain), 403 sso_access_revoked (deactivated by the organization's directory), 403 ip_not_allowed, 403 mfa_required, 403 passkey_required (no passkey, where the organization requires one; also from /auth/mfa/verify for a TOTP or recovery code there), 403 org_inactive; 503 sso_unavailable. The session a single sign-on opens is bound to its organization: switch-organization to another one is 403 sso_session_bound, and what belongs to the account rather than the organization - the personal-data export, the password change, the MFA and passkey endpoints (/auth/mfa/enroll, /confirm, /disable, /auth/webauthn/register/*, /auth/webauthn/credentials*) and /users/me/delete - is 403 sso_session_restricted: use a password sign-in.

An organization that requires single sign-on answers password sessions with 403 sso_required (owners who passed the platform's MFA excepted), including /auth/login naming it and /auth/switch-organization into it.

An organization that requires passkeys (require_passkey) answers every session that did not sign in with one with 403 passkey_required - on every request, at /auth/login naming it and at /auth/switch-organization into it. The MFA and passkey set-up endpoints answer for the account instead, so a passkey can be registered; API keys are not affected (PASSKEYS.md, section 4).

Conventions

Errors always have the same shape. request_id matches the X-Request-ID response header and the logs:

{"error": "permission_denied", "message": "You are not allowed to perform this action.", "request_id": "…", "details": null}
Status Meaning
400 Malformed multipart or protocol-level problem
401 Missing or invalid credentials (WWW-Authenticate set)
403 Authenticated but not permitted
404 Not found or belongs to another tenant
409 State conflict (e.g. automation_frozen, report_not_ready, duplicate)
413 / 415 Body too large / unsupported media type
422 Validation failed: details lists fields and codes, never the submitted values. invalid_cursor for a cursor that does not fit the sort; invalid_value for a value the database cannot store
429 Rate limited: honour Retry-After
503 Dependency unavailable or automation kill switch engaged

Pagination is keyset-based. Query parameters are limit (1 to 200, default 50), cursor (opaque, from next_cursor) and sort (field or -field, from a per-resource allowlist). Responses have the form {"items": [...], "next_cursor": "…"|null}. A cursor is only valid with the sort it was issued for; tampered cursors are rejected with 422 invalid_cursor.

Idempotency: POST /sources/{id}/runs, POST /workflows/{id}/runs, POST /intelligence/analyses and POST /reports accept an Idempotency-Key header (8 to 128 characters of [A-Za-z0-9._:-]). A repeat with the same key returns 200 with the original resource instead of 202 with a new one. A key belongs to the request it started: sending it with a different request (another source, workflow, dataset or report period) answers 409 idempotency_key_reused. Keys are honoured for retention.idempotency_keys_hours (24 by default) and released within a day after that.

Rate limits (defaults, per principal unless noted):

Scope Limit
Reads / writes 600 / 120 per minute
Exports and downloads 20 per hour per person (all of a user's API keys share it)
AI analyses 30 per hour per person (all of a user's API keys share it)
Login 20 per minute per IP, 5 per minute per account
Passkeys Sign-in step: 20 per 5 minutes per IP; registering and managing: 30 per 15 minutes per person
Single sign-on 30 starts and 30 callbacks per minute per IP
SCIM 600 per minute per SCIM token
Password change 5 per 15 minutes per user; a wrong current password also counts toward lockout
Webhook receipt 600 per minute per IP (before verification); 120 per minute per endpoint, counted only for correctly signed deliveries

Mass assignment: request bodies reject unknown fields with 422. Fields such as org_id, status or role can never be smuggled into a create or update.

Resources

Permissions in brackets. Owners have all permissions; see domain/authorization/roles.py for the role matrix.

Organization, users, access

Method Path
GET/PATCH /users/me Profile
GET /users Users of the current organization, with their roles [members:read]
POST /users/me/delete Erase own account (password confirmation)
GET /users/me/sessions My signed-in sessions: device (a coarse description such as "Firefox on Windows"), IP address, times, MFA (mfa_verified, and mfa_method: the second factor the session passed - totp, recovery_code, webauthn or null), and which one this request uses. Signed-in users only - never with an API key (403 session_required)
DELETE /users/me/sessions/{id} End one of my sessions: its access and refresh tokens stop working at once (audited as auth.session.revoked); another user's session is 404
GET /organizations Organizations of the current user
GET/PATCH /organizations/current View / update [org:update]. settings is a partial update; allowed_ip_ranges (up to 100 CIDRs, IPv4 or IPv6; null lifts it) confines members' sessions and API keys to those networks - a change that would exclude the caller's own address is 422 would_lock_you_out. require_mfa admits only MFA-verified sessions (403 mfa_required); require_passkey only sessions that signed in with a passkey (403 passkey_required), and turning it on from any other session or with an API key is 422 would_lock_you_out
POST /organizations/current/automation-freeze Tenant kill switch with reason [workflows:disable]
POST /organizations/current/deletion Request deletion (slug confirmation, 7-day grace) [org:delete]
GET, PATCH, DELETE /organizations/current/members[/{id}] Members and roles [members:read / members:manage]
GET, POST, DELETE /organizations/current/invitations[/{id}] Invitations [members:manage]
GET, POST, DELETE /api-keys[/{id}] API keys [api_keys:manage]
GET /audit, /audit/verify Audit trail and hash-chain verification [audit:read]
GET, PUT, DELETE /organizations/current/sso The organization's OpenID Connect provider: {issuer, client_id, client_secret, allowed_domains[], default_role (viewer or analyst), sso_required, trust_idp_mfa}. The secret is write-only (required the first time; omitted keeps it) and never returned. The answer lists each domain's TXT record and the redirect_uri to register. 422 sso_discovery_failed, invalid_issuer, invalid_default_role, sso_domain_not_verified, would_lock_you_out; 404 sso_not_configured. A signed-in owner's or administrator's session only (403 session_required for API keys) [org:update]
POST /organizations/current/sso/domains/verify Look up each unverified domain's TXT record: {configuration, checks: [{domain, result}]}, result verified, already_verified, record_not_found or lookup_failed [org:update]
GET, POST, DELETE /organizations/current/scim-tokens[/{id}] SCIM tokens {name, expires_in_days (1-365)}; the nxp_… token is shown once; at most 10 live ones (409 too_many_tokens). Owners' signed-in sessions only (403 owner_required, session_required)

Data

Method Path
GET, POST, GET/PATCH/DELETE /projects, /projects/{id} [projects:read / projects:write]. Deleting a project that still has datasets answers 409 project_not_empty
GET, POST, GET/PATCH/DELETE /datasets, /datasets/{id} Typed schema, classification, retention [datasets:*]. The key field cannot be sensitive (422): record keys appear in alerts, reports and AI prompts
GET /datasets/{id}/records Current records; sensitive fields masked without [records:read_sensitive]
GET /datasets/{id}/export?format=csv|jsonl Streaming, audited export [datasets:export]
GET /records/{id}/history Version history [records:read]
GET /changes Filter by dataset_id, record_id, change_type, min_significance, since [changes:read]
GET /analytics/changes?project_id=&dataset_id=&period_start=&period_end= Change volume of a project or one of its datasets: totals per type and significance, every UTC day of the period (quiet days included), unusual days (robust z-score) and a trend note. Counts only, computed on request; the period defaults to the last 30 days and is at most a year (422 invalid_period) [changes:read]

Collection

Method Path
GET, POST, GET/PATCH/DELETE /sources, /sources/{id} Website, REST API, webhook and file-upload sources [sources:*]
POST /sources/{id}/runs Run now (pull sources) [sources:run]
GET /sources/{id}/runs, /runs/{id} Run history and status
POST /sources/{id}/uploads multipart/form-data, one part named file (CSV/XLSX). Re-uploading a file that is already live returns 200 with the existing upload; a file whose run failed can be uploaded again (201) [uploads:write]
GET /sources/{id}/uploads Upload history
GET, POST, GET/DELETE /integrations, /integrations/{id} Credentials; secrets are write-only [integrations:*]
POST /integrations/{id}/rotate, /integrations/{id}/status Rotate / revoke / quarantine
GET, POST /webhook-endpoints Inbound endpoints; the signing secret is shown once [webhooks:*]
POST /webhook-endpoints/{id}/rotate-secret, /webhook-endpoints/{id}/status Rotation (24 h grace), disable

Intelligence, alerting, reporting, automation

Method Path
POST /intelligence/analyses {dataset_id} Queue an analysis [insights:generate]
GET /intelligence/insights[/{id}] Insights [insights:read]
GET, POST, PATCH, DELETE /alert-rules[/{id}] Conditions: change_type, significance_at_least, field_changed (updates only; use change_type for new or deleted records), numeric_change, insight_risk_at_least, run_failed. Conditions see the complete change, including sensitive fields; the alert shows them masked [alerts:*]
GET /alerts?status= Alerts [alerts:read]
POST /alerts/{id}/acknowledge {resolve} [alerts:ack]
GET, POST, PATCH, DELETE /channels[/{id}] Email, Slack, Telegram, signed webhook [channels:*]
POST /reports JSON, CSV, XLSX or PDF for a period of up to a year [reports:generate]. JSON, XLSX and PDF contain the executive summary, totals, the daily trend and a trend note, unusual days, anomalies (the 25 most significant high and critical changes), the 200 most significant changes, alerts, AI insights and sources; XLSX and PDF add a change-volume chart. Totals, the trend and the summary count every change of the period, however many. CSV is one table: the listed changes
GET /reports[/{id}], /reports/{id}/download Downloads are attachments with a Repr-Digest SHA-256 header [reports:download]
GET, POST, GET/PATCH/DELETE /workflows[/{id}] Scheduled collection → detection → analysis → alerting [workflows:*]
POST /workflows/{id}/status {status, reason} Activate, pause or disable (emergency stop)
POST, GET /workflows/{id}/runs Run now (idempotent) / history [workflows:execute / read]
GET /dead-letters Failed jobs [dead_letters:manage]
POST /dead-letters/{id}/retry, /dead-letters/{id}/discard Re-drive idempotent jobs

Inbound webhooks

POST /api/v1/webhooks/{org_id}/{endpoint_id} with a JSON body and two headers:

X-NexusFlow-Delivery:  <unique id, 8-128 chars of [A-Za-z0-9._:-]>
X-NexusFlow-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256>

The signature is HMAC_SHA256(secret, f"{t}.{delivery_id}." + raw_body):

import hashlib, hmac, json, time, uuid

body = json.dumps({"items": [{"sku": "A-1", "price": "9.99"}]}).encode()
timestamp, delivery = int(time.time()), str(uuid.uuid4())
mac = hmac.new(secret.encode(), f"{timestamp}.{delivery}.".encode() + body, hashlib.sha256).hexdigest()
headers = {
    "Content-Type": "application/json",
    "X-NexusFlow-Delivery": delivery,
    "X-NexusFlow-Signature": f"t={timestamp},v1={mac}",
}
Response Meaning
202 {"status": "accepted", "run_id": …, "truncated": false} Queued for processing; truncated: true means items beyond the source's max_items were not taken
200 {"status": "duplicate"} Already received and stored; safe to stop retrying
401 invalid_signature Bad signature, stale timestamp or unknown endpoint (indistinguishable)
409 delivery_in_progress An earlier attempt with this delivery id has not finished; retry later with the same id
409 source_inactive The source or organization is not accepting data; retry later
422 The signed body is not valid JSON or does not match the source mapping; fix it and retry with the same delivery id
429 The endpoint's delivery quota is spent; honour Retry-After

Timestamps must be within 5 minutes. During secret rotation, both secrets are valid for 24 hours, and a header may carry several v1= values. A delivery that was not stored (any non-2xx answer) can always be retried under the same delivery id; sign each attempt with a fresh timestamp.

SCIM 2.0 provisioning

https://<your-domain>/scim/v2 (RFC 7643/7644) lets an organization's identity provider provision and deprovision its members; setup and behaviour in SSO.md. Authenticate with the organization's SCIM token: Authorization: Bearer nxp_…. Requests and responses are application/scim+json (application/json is accepted); bodies are capped at 64 KiB.

Method Path
GET /ServiceProviderConfig, /ResourceTypes[/User], /Schemas[/{urn}] What is supported: PATCH yes; filter yes (200 results at most); bulk, sort, ETags and password changes no
GET /Users?filter=&startIndex=&count= A ListResponse. Filters: userName eq "…", externalId eq "…", emails.value eq "…" only (else 400 invalidFilter); count at most 200
POST /Users Provision: userName (an address in a verified domain), externalId, name, displayName, emails, active. 201 with Location; 409 uniqueness for a second entry of the same person or externalId; passwords are refused
GET, PUT /Users/{id} Read or replace (userName cannot change: 400 mutability)
PATCH /Users/{id} PatchOp add/replace of active, externalId, displayName, name[.givenName/.familyName/.formatted], emails. active: false deprovisions (membership removed, the member's API keys in the organization revoked); active: true provisions again with the default role
DELETE /Users/{id} Deprovision and delete the entry - never the account

Owners are never deactivated or removed (403). Errors use the SCIM Error schema: {"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"], "status": "400", "scimType": "invalidFilter", "detail": "…"}, with 401 (a missing, unknown, revoked or expired token, or any other credential), 403, 404 (also another organization's resources), 409, 413, 415, 429 (with Retry-After) and 503. A SCIM token is refused (401) by every endpoint outside /scim/v2.

Data classification

Datasets are classified public, internal (default), confidential or restricted. Restricted data never leaves the platform: it is never sent to an external AI provider (the offline analyser is used even when the organization opted in), and alert messages - which are sent by e-mail, Slack, Telegram or webhook - name the changed fields but carry no record keys or values. The full details stay available in the API to users allowed to see them.

Snapshot deletions

Website, REST API and upload sources with snapshot_mode: "full" mark records that a complete, fully valid snapshot no longer contains as deleted (soft delete, with a version entry). A snapshot that would delete more than the source's max_deletion_ratio (0 to 1, default 0.5) of the live records deletes nothing; the run succeeds and reports deletions_withheld in its stats. Set the ratio to 1.0 on a source to accept any deletion.

Internal API (not public)

/internal/v1/automation/* (n8n, service tokens) and /internal/v1/sandbox/* (sandbox, per-run tickets) are served only by the internal app on the private network. See ARCHITECTURE.md and workflows/n8n.