Discover how an application connects
Every application lists its authentication methods - each with anauthentication_id, a field list, and whether it’s OAuth:
Direct-credential connections (API keys, DB credentials…)
The OAuth flow
Three steps: get a sign-in URL, let the user authorize in a browser, then poll for the captured token and create the connection from it.Pass the sign-in’s
state as oauth_state (Python) / opts.oauthState (TypeScript) on create - the server looks up its own record of that sign-in and derives the connection’s credentials from it. Don’t copy connectionData into fields: values you send for server-derived keys (ApiKey, RefreshToken) are ignored, and ExternalOrgId is rejected outright with 400 INVALID_FIELD. Omitting oauth_state on an OAuth2 method returns 400 OAUTH_STATE_REQUIRED; a state the server can’t resolve - expired (past 5 minutes), unknown, or another account’s - returns 400 OAUTH_STATE_EXPIRED, deliberately identical for all three so it’s not an existence oracle. This applies to OAuth2 methods only - OAuth2WithoutSignIn (client-credentials) connectors need no state. The token record exists from the moment the sign-in URL is created, so every poll - including while the sign-in is pending - returns a body; while pending, status is an empty string and field_options/fieldOptions is {}. Poll on status, not on body presence: keep polling until status is success (or a failure status), not until the body is “no longer empty”. A null body means the same thing here as it does for create: the state can’t be resolved. It never turns non-null on a later poll, so treat it as a dead sign-in and start a new one - not as “still pending”.Requires
engini / @engini/sdk 0.21.0 or newer (0.20.0 and earlier cannot create OAuth2 connections - oauth_state/oauthState isn’t sent).Field options discovered after sign-in
Some connection fields have no static list of legal values - the org itself has to be asked. Salesforce’sApiVersionUrl is the example: it’s required, has no default, and the only source for it is the signed-in org’s own /services/data. The poll that ends the sign-in successfully (status is success, the same check as the polling loop above) carries a field_options / fieldOptions map for exactly this case, keyed by the field name:
field_options is an attrs model, not a dict. token.field_options supports "ApiVersionUrl" in fo and fo["ApiVersionUrl"], but has no .get() - use the in check above, not .field_options.get(...). Each value is an AccessTokenResponseFieldOptionsAdditionalProperty object; call .to_dict() on it to get the plain {key: label} dict shown in the comment.field_options is populated only on the successful poll (status is success); on every other poll it’s {}. On the successful poll, an empty map ({}) - for a field, or for the whole response - means there’s nothing to offer: either no dynamic options exist for that field, or the lookup couldn’t complete. Either way, fall back to list_values, if any, or free text; don’t read {} on a pending poll as a lookup result at all.
Requires
engini 0.24.0 or newer / @engini/sdk 0.24.0 or newer (earlier releases don’t declare field_options/fieldOptions on the access-token response at all).Listing and filtering by kind
Every connection carries akind - Regular, Key, or MCPClient. Filter the list to the subset the web UI’s Keys tab shows (see Keys vs Integrations):
MCPClient is accepted but returns nothing - this endpoint serves only Regular and Key connections. OPA is rejected with EnginiValidationError: on-prem agents are not connections on this surface. They have their own resource, client.opa, with their own lifecycle, token, settings, and logs.
Updating a connection
Change a name, description, credential fields, or metadata. Anything you leave out is omitted from the request, and the server leaves it unchanged.None/null: the SDKs leave an omitted member out of the request body entirely, so it is never mistaken for “clear this field”.
Re-authenticating an OAuth2 connection in place
update also takes oauth_state / oauthState - the state from a fresh get_sign_in_url sign-in. Passing it alone, with no other fields, is a valid edit: the server derives fresh credentials from that sign-in and applies them to the existing connection, so you never mint a new connection id or re-point workflows that reference it.
ApiKey, RefreshToken); everything else on update (renaming, editing other fields) needs no state. ExternalOrgId is rejected outright, with or without a state.
Pointing a workflow at a different connection
903’s activities are rewired, and every other workflow using connection 481 is untouched. The arguments are keyword-only (Python) and an options object (TypeScript) on purpose: three same-typed integers are easy to transpose.
Raises if an activity cannot be remapped, i.e. the target connection doesn’t expose the activity or object the workflow needs.
Refresh & object selection
Applications withsupports_object_selection expose objects (tables, entities) to choose from. Refresh is asynchronous - trigger it, then poll:
select_objects replaces the whole selection set. A completed refresh also proves the connection authenticates - it’s a stronger health signal than check (which can be inconclusive). A refresh that doesn’t finish in time raises EnginiTimeoutError.