POST Create Identity / Login

Find-or-create an identity and sign in to a catalog in one call — with a password or an entry from a connected vault.

POSThttps://api.anakin.io/v1/wire/login

Create-and-sign-in in a single call: this endpoint finds-or-creates an identity for the catalog and signs it in, returning a credential_id you can immediately use with POST /v1/wire/task.

There are two ways to supply the login:

  • Classic params — pass the catalog's username/password (and any other login fields) directly.
  • Vault locator — pass a source_id + source_ref pointing at an entry in a connected identity source, and Wire reads the login fields from your own vault at sign-in time. No password is typed into Wire. The locator is provider-agnostic: 1Password and Azure Key Vault take the same shape.

The endpoint:

  1. Resolves the catalog by slug
  2. Finds-or-creates an identity for you (auto-named from params if you don't supply identity_name; an explicit identity_name is required when using a locator)
  3. Resolves the login fields — from params, or from the vault entry referenced by source_id + source_ref
  4. Runs the catalog's login wheel against those fields
  5. Persists a cookies-only credential (the password is never stored)
  6. Returns everything needed to call authenticated actions

The password is discarded after a successful sign-in. Only the issued cookies are stored, encrypted. When the cookies expire (see expires_at), call /login again to mint fresh ones.

For browser-based catalogs (auth_types includes browser_state but not credentials), use the interactive connect flow instead.


Request Body

{
  "catalog_slug": "neb",
  "identity_name": "work-account",
  "params": {
    "email": "alice@example.com",
    "password": "..."
  }
}
ParameterTypeDescription
catalog_slug requiredstringCatalog slug, e.g. neb. Find it in GET /v1/wire/catalog.
identity_namestringOptional label for the identity. If omitted, derived from params (priority: email > username > user > login > account_id > account, falling back to "default"). Same name re-uses the same identity row across calls.
paramsobjectWheel-defined login fields. Shape varies per catalog — discover it by reading login_input_schema from GET /v1/wire/catalog/{slug}. Required unless you supply a source_id + source_ref locator.
source_idstring (UUID)Optional. A connected identity source from GET /v1/wire/identity-sources. When paired with source_ref, the engine resolves the login fields from your vault instead of params.
source_refobjectOptional. Where in the vault the login fields live — see The source_ref locator below. Used together with source_id as an alternative to params.

identity_name is required when using the locator. With a locator there are no params to auto-derive a name from, so you must pass an explicit identity_name. You also cannot send both params and a locator — pick one.

A locator request body looks like:

{
  "catalog_slug": "neb",
  "identity_name": "work-account",
  "source_id": "f1e2d3c4-0000-0000-0000-000000000000",
  "source_ref": { "entry": { "container": "abcd1234", "key": "wxyz5678" } }
}

The source_ref locator

source_ref says where each login field lives; the catalog's login action schema says what the wheel needs. Wire joins the two — you never state which vault field maps to which login parameter.

(a) One entry supplies every field. The common case, and what the dashboard's identity picker emits:

{ "entry": { "container": "abcd1234", "key": "wxyz5678" } }

container is a container ID from list containers; key is an entry ID from list entries.

(b) Per-field refs. For secrets kept apart — a username and password stored as two separate Key Vault secrets:

{
  "fields": {
    "username": { "container": "https://my-vault.vault.azure.net", "key": "acme-user" },
    "password": { "container": "https://my-vault.vault.azure.net", "key": "acme-pass" }
  }
}

(c) Both. fields overrides specific fields and inherits container / key / version from entry wherever they're omitted — e.g. an Azure secret holding the password, with the username carried in a tag:

{
  "entry":  { "container": "https://my-vault.vault.azure.net", "key": "acme" },
  "fields": { "username": { "property": "tag:username" } }
}
FieldTypeDescription
containerstringThe container holding the entry
keystringThe entry within the container. Required on entry, and on a fields ref when there is no entry to inherit one from
propertystringA named field inside the entry — a 1Password field name, or tag:<name> for an Azure secret tag. Defaults to the whole entry
versionstringA specific version of the entry. Omit for "latest" — which is what makes a password rotated in your vault take effect on the next /login with no change here

Wire matches the vault's field names to the login action's parameter names, falling back to a small alias table (usernameuser / login / email, passwordpass / secret, and so on). If a required login field can't be resolved, the call fails with SOURCE_FIELD_MISSING and names the fields, so you fix them in one pass.

Each parameter the action declares is resolved in this order:

  1. An explicit per-field ref in fields — inheriting container, key and version from entry wherever they're omitted
  2. An exact name match inside the entry
  3. An alias match, per the table above
  4. The entry's own property, applied last so it can never shadow a real name match

A distinct entry is read once per sign-in however many parameters it supplies, so pointing four fields at one item costs one round trip to your vault.

username and email travel together. When one of the pair resolves and the other doesn't, Wire fills in the missing one with the same value — so a login action declaring only email still receives username, and vice versa. Passwords are never duplicated, and this is the only pair.

It exists for wheels whose published schema is narrower than what they actually read: before this endpoint resolved fields from a schema, every sourced login was sent username, email and password regardless, and an action declaring email while indexing username worked precisely because both were always present. Expect to see the extra key when inspecting what a login received.

The pre-canonical 1Password shape — { "vault_id": "...", "item_id": "...", "fields": ["username","password"] } — is still accepted, and credentials bound with it keep working. New integrations should use the shape above.


Discovering the params shape

Different catalogs require different fields. Always read the schema first:

curl https://api.anakin.io/v1/wire/catalog/neb \
  -H "X-API-Key: your_api_key"

The response includes login_input_schema — an array of {name, type, required, description} field descriptors. Send exactly those fields in params.


Response

201 Created
{
  "status": "verified",
  "identity_id": "7c3f1a2b-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
  "identity_name": "alice@example.com",
  "credential_id": "11111111-2222-3333-4444-555555555555",
  "catalog_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "catalog_slug": "neb",
  "expires_at": "2026-05-29T08:09:23Z"
}
FieldTypeDescription
statusstringAlways "verified" on success
identity_idstring (UUID)The identity this credential is attached to. Stable across re-runs with the same name.
identity_namestringThe name used (explicit or derived)
credential_idstring (UUID)Pass this as credential_id in subsequent POST /v1/wire/task calls.
catalog_idstring (UUID)The catalog's UUID — saves you another lookup
catalog_slugstringEchoed back
expires_atstringRFC3339 UTC. When the session expires. Capped at 30 days; defaults to 24h if the wheel doesn't supply one. Re-call /login to refresh.

Idempotent re-runs

Calling /login again with the same identity_name (or the same derived name) re-uses the existing identity row and refreshes its credential in place — no duplicate rows accumulate. Pass an explicit identity_name if you want multiple identities for the same catalog under one account (e.g. work + personal).


Error Responses

All errors return JSON of the form { "status": "error", "error": { "code": "...", "message": "...", "missing_fields"?: [...] } }. The wheel's raw error text is never echoed; only the typed code is surfaced.

Validation / setup — request rejected before the wheel runs:

CodeHTTPWhen
INVALID_BODY400Request body isn't valid JSON
INVALID_INPUT400catalog_slug missing; OR neither params nor source_id+source_ref supplied; OR both supplied; OR locator used without identity_name
INVALID_PARAMS400One or more required: true fields from login_input_schema are missing, null, or blank. Response includes missing_fields: [...]. The wheel is not invoked — failures here cost nothing.
CATALOG_NOT_FOUND400No catalog matches catalog_slug
IDENTITY_BINDING_MISMATCH409The named identity already exists bound to a different source/credential than the request
LOGIN_NOT_SUPPORTED400The catalog's auth_types doesn't include credentials, OR the catalog has no published login action
LOGIN_NOT_AVAILABLE503Credentials-mode login isn't configured on this engine (rare; infrastructure issue)
LOGIN_TRANSPORT_ERROR502Could not reach the wheel runner

Locator-only — the binding was rejected before the wheel ran:

CodeHTTPWhen
INVALID_INTEGRATION_REF400source_ref isn't a JSON object. (Supplying only one half of the locator is reported as INVALID_INPUT above, not this)
SOURCE_NOT_FOUND400The referenced source_id doesn't exist
FORBIDDEN403The referenced source belongs to another user
SOURCE_INACTIVE400The referenced source is not active — reconnect it
PROVIDER_NOT_SUPPORTED_BY_CATALOG400The catalog doesn't list this source's provider in supported_sources. See GET /v1/wire/catalog/{slug}
CATALOG_MISSING_REQUIRED_AUTH_TYPE400The catalog has no login wheel that can consume this provider's credentials
PROVIDER_CREDENTIAL_TYPE_MISMATCH400The provider resolves to a different credential type than this request creates
SOURCE_FIELD_MISSING400The entry can't supply a required login field. Response includes missing_fields: [...]
SOURCE_AUTH_FAILED400The provider rejected the stored credential — rotate or reconnect the source
SOURCE_FORBIDDEN400The source has no access to that container. Grant it access at the provider, then retry
SOURCE_ITEM_NOT_FOUND400The referenced container or entry no longer exists
SOURCE_THROTTLED429The provider is rate-limiting us. Retry shortly
SOURCE_UNREACHABLE502Could not reach the provider
SOURCE_NOT_AVAILABLE503Source resolver not configured on this engine

Wheel ran (HTTP 200, status: "error") — the request was well-formed and the wheel ran, but sign-in didn't complete:

CodeHTTPWhen
BAD_PASSWORD200Sign-in rejected by the site — wrong credentials
MFA_REQUIRED200Account requires multi-factor auth (not yet supported)
CAPTCHA_REQUIRED200Site is showing a captcha challenge
ACCOUNT_LOCKED200Site reports the account is locked
LOGIN_TIMEOUT200Sign-in took too long
LOGIN_PAGE_CHANGED200Site's login flow may have changed
LOGIN_NO_COOKIES200Sign-in completed but no session cookie was issued
LOGIN_INFRASTRUCTURE_ERROR200Transient Browser Connect / upstream issue. Retry.
LOGIN_FAILED200Generic fallback — sign-in failed for an unclassified reason

HTTP 200 with status: "error" is used for "the request was well-formed and the wheel ran, but the sign-in itself didn't complete." Treat any status != "verified" as failure regardless of HTTP code.

Example INVALID_PARAMS response:

{
  "status": "error",
  "error": {
    "code": "INVALID_PARAMS",
    "message": "Missing required login fields: password",
    "missing_fields": ["password"]
  }
}

Code Examples

# (a) classic — username/password in params
curl https://api.anakin.io/v1/wire/login \
  -X POST \
  -H "X-API-Key: your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "catalog_slug": "neb",
    "params": {
      "email": "alice@example.com",
      "password": "your_password"
    }
  }'

# (b) vault locator — fields resolved from your own vault; identity_name required
curl https://api.anakin.io/v1/wire/login \
  -X POST \
  -H "X-API-Key: your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "catalog_slug": "neb",
    "identity_name": "work-account",
    "source_id": "f1e2d3c4-0000-0000-0000-000000000000",
    "source_ref": {
      "entry": { "container": "abcd1234", "key": "wxyz5678" }
    }
  }'

Rate limit

10 requests per minute per user (each call triggers a real sign-in run against the target site).