Skip to main content
Breaking changes to the /v1 API are announced here before they ship. The OpenAPI spec is the source of truth; the SDKs are generated from it.
Server-only: blocked-connection guard, and the error envelope drops path
Two server-side changes with no SDK/CLI surface, so no package version marks them:
  • 403 CONNECTION_BLOCKED on POST /v1/tools/{toolSlug}/execute. A tool call resolved against a connection with no licence, or an expired one, now returns 403 and the tool is not executed - unlike an ordinary tool-runtime failure, which is a 200 with isSuccess: false. See the error code table.
  • The error envelope no longer includes path. Every error body dropped the field; update anything that read it. It may still appear on /v1/auth/whoami failures pending an internal repin.
Triggers API live on production
The /v1/triggers API is enabled on production. The client.triggers resource and the engini triggers commands that shipped in 0.20.0 now work against api.engini.io with a normal API key - no upgrade needed to start, though CLI 0.23.0+ is recommended (see below).Start with Triggers for the model and the delivery contract, then Triggers in the SDKs, the engini triggers commands, or the new Triggers group in the API reference. The Cookbook has three new recipes under React to events: receive events on a webhook, debug and replay a failed delivery, and develop a trigger locally.EndpointsNew since 0.20.0: a test endpointPOST /v1/triggers/{triggerId}/test signs a synthetic event with the destination’s real secret, sends it straight away, and returns what your endpoint answered (delivered, status_code, error). Nothing is stored. It is not wrapped by any released package yet (Python SDK, TypeScript SDK and CLI all at 0.24.0), so call it over HTTP. Limited to 10 calls a minute per account.Delivery, in short
  • Any 2xx is delivered. 408, 429, 5xx, timeouts (10 s) and connection errors are retried 1 min, 5 min, 15 min, 1 h and 6 h apart - 6 attempts in all. Any other 4xx, or a redirect, dead-letters the event at once.
  • Five dead-lettered events in a row auto-disable the destination. Events are still captured while it is disabled, and re-enabling resumes delivery on its own for events from the last 24 hours; older backlog and dead-lettered events need a replay.
  • Events are kept 30 days. Replay queues one more attempt (202) rather than delivering inline.
  • Rotating a signing secret has no overlap window: the old secret stops working immediately.
Upgrade the CLI to 0.23.0 or later if you use triggers from the terminal
  • engini triggers create no longer requires --connection for connection-less trigger types (engini_schedule, http_webhook). On 0.20.0-0.22.x it exited 2 without one.
  • Ctrl-C now stops engini triggers listen and exits 0. On 0.20.0-0.22.x an idle stream ignored the interrupt.
Docs correctionThe webhook examples on Triggers in the SDKs passed arguments to verify_webhook / verifyWebhook in the wrong order. The signature has always been (payload, signature_header, secret) - raw body first. Code copied from the old example raises EnginiWebhookSignatureError on every delivery. The TypeScript create example also showed a positional slug (it takes one object, { triggerSlug, ... }), and the destination examples used max_failures / maxFailures where the parameter is max_consecutive_failures / maxConsecutiveFailures. All fixed.
Client/SDK/CLI 0.24.0 - field options on the OAuth access-token poll
engini-client 0.11.0 · @engini/sdk 0.24.0 · @engini/cli 0.24.0 · engini (Python) 0.24.0 (Python: 2026-09-30).AccessTokenResponse gains field_options / fieldOptions: a per-field map of dynamically-discovered legal values, returned on the successful poll of GET /v1/connections/access-token/{state} for fields with no static list (Salesforce’s ApiVersionUrl is the first case). ApplicationConnectionField gains maskInLogs, mirroring AppConnectionField.MaskInLogs - a secret field that isn’t a password; test isPassword || maskInLogs to catch both. See Field options discovered after sign-in for the orientation warning (the map’s key is the value to save, not the label) and Field options in need fields and need auth for the CLI’s need:"fields"/need:"auth" shape.Since 0.23.0, engini connect merges --field with OAuth-derived values instead of dropping it on the OAuth path. 0.24.0 adds client-side validation of a supplied field against its discovered options (exit 2 on an illegal value, before any request reaches the server) and a numbered picker in a TTY.
Client/SDK/CLI 0.23.0 - connection-less triggers, Ctrl-C, and OAuth resume hints
engini-client 0.10.0 · @engini/sdk 0.23.0 · @engini/cli 0.23.0 · engini (Python) 0.23.0 (Python: 2026-09-22).
  • engini triggers create no longer requires --connection. Omit it for a trigger type whose application needs no connection (engini_schedule, http_webhook) and the server supplies its own. For a type that does need one, omitting the flag now reaches the server and comes back 400 EMPTY_QUERY, which the CLI rewrites to name --connection and exits 5 - not the 2 of 0.20.0-0.22.x. An explicit empty value (--connection "") still exits 2.
  • Ctrl-C now stops engini triggers listen. It closes the stream and any in-flight --forward POST, prints the usual summary line, and exits 0 - even if some forwards had failed. On 0.20.0-0.22.x an idle stream never noticed the interrupt at all. The TypeScript SDK’s client.triggers.subscribe() grows a matching signal option (an AbortSignal) that the CLI wires to this handler; aborting returns the generator normally rather than throwing.
  • engini connect’s OAuth resume string is now replayable. It carries --auth <id> and echoes back any --field values you originally passed (a secret’s value is shown as <value>), instead of dropping them on the OAuth path. A resume replayed after state_expires_in_seconds (300s) now fails fast with exit 4, rather than polling out the full timeout.
Client/SDK/CLI 0.22.0 - shim removal, OIDC publishing
engini-client 0.10.0 · engini / @engini/sdk 0.22.0 · @engini/cli 0.22.0.Behaviour-neutral release: the Python package drops the OAuth-state/helpUrl compatibility shims now that engini-client 0.10.0 carries them natively, and the npm packages switch to publishing via OIDC. No API, SDK, or CLI surface changed.
Client/SDK/CLI 0.21.0 - breaking: OAuth2 connections require a sign-in state
engini-client 0.10.0 · engini / @engini/sdk 0.21.0 · @engini/cli 0.21.0.Breaking. Creating an OAuth2 connection now requires the sign-in state from GET /v1/connections/get-sign-in-url - pass it as oauth_state / oauthState on create. Two new error codes: OAUTH_STATE_REQUIRED (400, the state was omitted) and OAUTH_STATE_EXPIRED (400, the state was issued but is stale or already used). Re-authenticating an existing connection follows the same shape via connections update --oauth-state <state>.
CLI/SDK 0.20.0 - the triggers surface ships in the packages, ahead of the API
engini-client 0.9.0 · engini / @engini/sdk 0.20.0 · @engini/cli 0.20.0.This release adds a triggers surface to both SDKs and the CLI: a client.triggers resource and an engini triggers command group, for creating trigger instances from a catalog of trigger types, managing the destinations their events are delivered to, and reading the event log.
Enabled on production on 2026-09-24 - see the entry above. At the time of this release the API was not yet switched on, and every /v1/triggers/* path returned 404; the packages shipped first and the server followed.
Everything else in 0.20.0 is unchanged from 0.19.0 — no exit code was added, and no existing endpoint, method, or flag changed behaviour.
CLI/SDK 0.19.0 - manage On-prem agents over the API, SDKs, and CLI
engini-client 0.8.0 · engini / @engini/sdk 0.19.0 · @engini/cli 0.19.0.Your On-prem agents - the Engini component a customer installs behind their firewall to reach SQL Server, Oracle, Priority ERP, and file shares - are now manageable outside the engini.io UI: a new /v1/on-prem-agents group, a client.opa resource in both SDKs, and an engini opa command group. Provision an agent, read or rotate its token, change its runtime settings, pull its logs, and fetch the current installer.Two breaking changes
  • kind=OPA is no longer accepted on GET /v1/connections or GET /v1/applications. It returns 400 (INVALID_ENUM_VALUE) naming /v1/on-prem-agents; the CLI exits 5. Previously it was accepted and returned an empty page forever - on-prem agents were never served by those endpoints, so no caller can have been receiving data from the old behaviour. kind=MCPClient is unchanged and still accepted.
  • GET /v1/connections/{connectionId} now returns 404 for an on-prem agent (and for an MCPClient connection), matching what the list endpoint has always excluded. Before this, detail-by-id answered for connections the list would never show.
New endpoints
  • Every per-agent route returns 404 when the id is unknown, belongs to another account, or is not an on-prem agent. The three are deliberately indistinguishable so the endpoint cannot be used to probe which ids exist.
  • GET /v1/on-prem-agents uses the standard paginated envelope; top is clamped to 1-100.
An agent is polled, never pushed toThe agent polls Engini for work; Engini never connects to it. Two consequences a caller must plan for:
  • PATCH /v1/on-prem-agents/{id} always persists the change, then tries to hand it to the running agent. The response carries appliedToAgent and dispatchedTaskIds. appliedToAgent: false is not an error - the agent was not Online (a freshly created agent is WaitingForConnection until it first polls), and it will pick the new settings up when it connects. The write already committed, so retrying is safe but unnecessary.
  • GET /v1/on-prem-agents/{id}/logs needs the agent Online. Otherwise it returns 409 OPA_AGENT_OFFLINE naming the agent’s actual status; if the agent does not answer within the server-side bound it returns 504 OPA_AGENT_TIMEOUT. The CLI exits 1 and 124 respectively.
Settings bounds come from the connector, not from youparallelTasks and pullPeriodSeconds are validated against the range the agent connector itself declares - currently 1-100 and a minimum of 10 seconds. A value outside it is rejected with 400 (CLI exit 5), never silently clamped. Every settings member is nullable: on PATCH, an omitted member is left unchanged, which is why 0 is treated as a mistake rather than a request. Neither the SDKs nor the CLI duplicate the numbers client-side; the API is the one authority.PATCH merges, it does not replaceUnlike PUT /v1/connections/{id} (and unlike the connections/workflows lists on /v1/mcpservers), an omitted member here is simply left alone. {"settings": {"logLevel": "Debug"}} changes the log level and nothing else.The agent token is a credential
  • POST /v1/on-prem-agents returns the token in the create response, so a box can be provisioned in one round-trip. That means it lands in shell history and CI logs; pipe it to a secret store, or fetch it later with GET /v1/on-prem-agents/{id}/token instead.
  • Rotation is destructive to a running agent. The old token stops working the instant the call returns, and the agent is down until someone reconfigures it with the new one. The API requires ?confirm=true; the CLI confirms on a TTY and demands --force otherwise.
  • Not-found errors echo the agent id, which is not a secret - unlike MCP server tokens.
The installer manifestGET /v1/on-prem-agents/releases/latest is anonymous and returns {version, platform, url, sha256, sizeBytes, releasedAt, degraded}. degraded: true means the published release manifest could not be read and the result was reconstructed from the installer URL alone - sha256, sizeBytes, and releasedAt are then null. A caller that requires integrity verification should refuse a degraded manifest. The checksum proves the download was not corrupted; it does not prove authenticity, since the manifest lives beside the installer.CLI: the engini opa group
  • Full reference: On-prem agents. Exit codes and output shapes: machine contract.
  • No exit code was added. OPA_AGENT_OFFLINE exits 1 (a retriable operational state, not a caller error); OPA_AGENT_TIMEOUT exits 124; an out-of-range setting or a missing applicationSlug exits 5.
  • logs and download write to a file and print a JSON receipt. Neither ever writes binary to stdout. download verifies the sha256 when the manifest carries one and refuses to write on a mismatch.
SDKs: client.opa
  • Five new typed errors in both SDKs, each a subclass of the status-mapped error it refines so existing catch blocks keep working: EnginiOpaAgentNotFoundError (404), EnginiOpaAgentOfflineError and EnginiOpaApplicationAmbiguousError (409 - the latter carries candidate_slugs/candidateSlugs), EnginiOpaAgentTimeoutError (504), EnginiOpaAgentDispatchError (502).
  • Every API error now exposes the wire errorCode and details - err.error_code / err.details in Python, err.errorCode / err.details in TypeScript. Messages are unchanged. This was needed because three OPA conditions share a status and differ only by code; it applies to every error the SDKs raise, not just OPA ones.
  • rotate_token() / rotateToken() send confirm=true unconditionally - the human gate lives in the CLI, not in library code.
  • The Python SDK now requires engini-client>=0.8.0,<0.9.0; @engini/sdk pins @engini/client 0.8.0. 0.7.0 has none of the generated /v1/on-prem-agents operations.
CLI/SDK 0.18.0 - maintain your MCP servers over the API, SDKs, and CLI
engini-client 0.7.0 · engini / @engini/sdk 0.18.0 · @engini/cli 0.18.0.The MCP servers your account exposes are now readable and configurable outside the engini.io UI: a new /v1/mcpservers group, a client.mcp resource in both SDKs, and an engini mcp command group.This is the endpoint an MCP client points at - not the MCPClient connection kind, which is Engini consuming someone else’s MCP server. See Connect via MCP.New endpoints
  • Servers are account-wide, not per-user: an API key sees every server on the account.
  • GET /v1/mcpservers uses the standard paginated envelope. top is silently clamped to 1-100 and a negative offset is treated as 0 - neither is an error, so read offset/top back off the response rather than assuming what you asked for.
  • Deactivated servers still appear in list, reporting isActive: false. Deactivating is a reversible on/off switch, not a soft delete.
No create, no delete - by design
  • There is no POST /v1/mcpservers, no DELETE, no engini mcp create, and no engini mcp delete. Unlike toolsets, authoring and destroying an MCP server stay in the engini.io UI, which issues the SSE URL and owns the anonymous-access setting.
  • isAnonymousAllowed is read-only here and is preserved by every update.
  • To take a server out of service, engini mcp deactivate / PATCH {"isActive": false}.
connections and workflows replace, they do not merge
  • On PATCH, an omitted member is left unchanged, but a supplied connections or workflows list replaces the entire set. A body carrying one connection drops every other connection on that server.
  • Calling the API directly, you must read the server, modify the list you got back, and send all of it. Every connection entry needs an explicit connectionId - MCP servers are account-wide, so there is no per-user default connection to resolve one from.
  • The CLI and SDK CLI-side helpers do that read-modify-write for you, which is why the surface is add/rm rather than a single --connections flag. It is last-writer-wins (the API has no ETag/If-Match), so preview with --dry-run, which prints the full resulting set plus what was added, removed, or re-scoped.
  • A workflow entry is keyed by workflowToken, not workflowId.
The server token is secret-equivalent
  • mcpServerToken is the {server-token} component of the server’s SSE URL (https://mcp.engini.io/{server-token}/sse). Keep it out of shared logs, CI output, screenshots, and tickets, and redact it as you would an API key.
  • It is not a bearer credential on its own - an authenticated client still runs the OAuth 2.1 flow - but it names a live endpoint, so it does not belong in anything you publish.
  • The SDKs and CLI deliberately do not echo the token back in a not-found error (Unknown MCP server token.).
CLI: the engini mcp group
  • Full reference: MCP servers. Output shapes and per-failure exit codes: machine contract.
  • No exit code changed. The group reuses the existing table - 2 usage, 4 unknown server or a binding that is not on it, 5 a server-side rejection.
SDKs: client.mcp
  • update() replaces connections/workflows rather than merging them; there is no create() or delete().
  • An unknown token raises EnginiMcpServerNotFoundError (new, in both SDKs), which the CLI maps to exit 4.
  • The Python SDK now requires engini-client>=0.7.0,<0.8.0; @engini/sdk pins @engini/client 0.7.0. 0.6.0 has none of the generated /v1/mcpservers operations.
CLI/SDK 0.17.0 - client version telemetry and update notices
@engini/cli, @engini/sdk, and engini (PyPI) 0.17.0. Every request now identifies the calling client and runtime on the wire, and the CLI tells you when a newer version is out.Versioned User-Agent
  • Every API call now sends a User-Agent identifying the SDK and runtime, e.g. engini-cli/0.17.0 engini-sdk-ts/0.17.0 node/20.11.0 darwin-arm64. The Python SDK sends the equivalent engini-sdk-python/0.17.0 python/3.11.6 darwin.
  • If you embed the SDK in your own product, prepend your own token with userAgentPrefix (TS) / user_agent_prefix (Python) on the Engini constructor - see the TypeScript SDK and Python SDK READMEs.
CLI: “update available” notice
  • On an interactive terminal, the CLI checks once a day for a newer release on your current channel and prints a one-line notice to stderr - see Upgrading.
  • Never shown under --json, --quiet, in CI, when piped, or on a usage error (a typo’d command stays silent). Opt out entirely with ENGINI_NO_UPDATE_CHECK=1.
Connection kind - Keys over the API, SDKs, and CLI
engini-client 0.6.0 · engini / @engini/sdk 0.16.0 · @engini/cli 0.16.0.The Connections page’s Keys tab is now reachable from the API, both SDKs, and the CLI. A Key is not a new resource - it is a connection whose application takes an API key rather than an OAuth grant - so this adds a discriminator and a filter rather than a parallel surface. See Keys vs Integrations.kind on responses
  • Connection and application objects now carry kind: one of Regular, Key, OPA, or MCPClient.
  • kind is always present on /v1 responses and is required by the generated clients. If you build fixtures or mocks from the examples in these docs, include it.
?kind= filter
  • GET /v1/connections?kind=Key and GET /v1/applications?kind=Key. Values are case-insensitive, so ?kind=key works.
  • An unrecognised value is a 400 listing the accepted ones - the filter is never silently ignored.
  • On /v1/connections, ?kind=OPA and ?kind=MCPClient are accepted but return an empty page: that endpoint serves only Regular and Key connections.
Behaviour change - GET /v1/applications returns more rowsGET /v1/applications previously hid any application that was not MCP-enabled. Key applications are now exempt from that filter, because a Key application exists so a workflow can hold a credential for it and need not expose a single tool - excluding them made Keys undiscoverable. A caller that paginates the catalogue will see a larger totalCount. Nothing is removed, and ?kind=Key remains a true subset of the unfiltered list. This is the only behavioural change in this release; everything else is additive.New CLI commands
  • engini connections update <id> - change a connection’s name, description, credential fields, or metadata.
  • engini connections replace --workflow <id> --from <id> --to <id> - point one workflow at a different connection.
  • engini connections defaults / set-default <id> / clear-default <app> - manage per-application defaults.
  • engini connections list --kind <kind> - filter by kind; --kind Key is the web UI’s Keys tab.
engini connections update --field and --metadata replace their entire map - they do not merge. Rotating one credential with a single --field clears every other field on the connection. Read the current values first, or preview with --dry-run (secrets are masked in the preview).
New SDK methods (Python and TypeScript)
  • connections.update(...) - same replace-not-merge semantics as the CLI.
  • connections.replace(...) - workflow-scoped, not a global swap.
  • connections.list(..., kind=...) / connections.list(application, kind).
No exit codes changed. See exit codes.
Clients on these versions expect kind on every connection and application response. Point them at an API that emits it; if you self-host or target an environment that has not been updated yet, stay on the previous client release until it has.
CLI 0.13.0 - login prompt and interactivity
@engini/cli and @engini/sdk 0.13.0. Fixes the interactive engini login prompt and tightens when the CLI will prompt at all.engini login
  • The key prompt is now visible and gives feedback: masked input (one * per character) plus a confirmation of what was captured (✓ Received 44 characters — eng_…wxyz). Previously the prompt label was erased by the terminal on the first keypress and nothing was echoed, so there was no way to tell whether a paste had registered.
  • A key must start with eng_. Empty, malformed, and line-broken values are rejected on both --api-key and the interactive prompt; the prompt re-asks up to three times. Nothing is written on any rejection.
Behaviour changes worth checking if you script the CLI
  • Prompting now requires a terminal on both stdin and stdout. Previously only stdout was checked, so engini login < /dev/null walked into the prompt and exited 0 having stored nothing.
  • engini login --api-key <malformed> now exits 2 instead of 0. It previously stored the value and reported success.
  • engini connections delete and engini toolsets delete exit 2 instead of 0 when stdin is piped and --force is absent.
  • engini connect emits its need:* payload and exits 5 - instead of exiting 2 - when stdout is a terminal but stdin is piped.
  • Ctrl-D now aborts a prompt (exit 2) rather than being ignored.
See exit codes for the full table, including the prompt-specific cases.
Initial public release
The Engini Developer API v1 is available to early adopters, alongside the Python and TypeScript SDKs, the engini CLI, and MCP access.Surface
  • Applications - browse the connector catalog and each application’s authentication methods
  • Connections - create and manage connections (direct credentials or OAuth), defaults, object selection
  • Tools - discover tools, read their JSON Schemas, execute them
  • Toolsets - bundle connections and permitted tools to scope an agent
  • GET /v1/auth/whoami - verify a credential
Conventions
  • JSON is camelCase throughout; applications and tools are addressed by slug, connections by integer id, toolsets by GUID
  • Offset-based pagination (offset / top) with a { items, totalCount, offset, top } envelope
  • One error envelope for every non-2xx response, with field-level details on validation failures
  • POST /v1/tools/{toolSlug}/execute returns 200 with isSuccess: false for tool-runtime failures - always branch on isSuccess
Authentication
  • API keys (x-api-key: eng_…) or Bearer JWTs; OAuth 2.1 for apps acting on behalf of Engini users, including MCP clients
SDKs & CLI
  • Python: engini 0.11.0 (PyPI) · TypeScript: @engini/sdk 0.12.0 (npm) - typed errors, auto-pagination, OAuth helpers, LLM tool-schema providers
  • CLI: @engini/cli 0.12.0 (npm - the only CLI distribution; pip install engini is the SDK and ships no CLI) - includes toolsets management with --dry-run diffs