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.
https://api.anakin.io/v1/wire/loginCreate-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_refpointing 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:
- Resolves the catalog by slug
- Finds-or-creates an identity for you (auto-named from
paramsif you don't supplyidentity_name; an explicitidentity_nameis required when using a locator) - Resolves the login fields — from
params, or from the vault entry referenced bysource_id+source_ref - Runs the catalog's login wheel against those fields
- Persists a cookies-only credential (the password is never stored)
- 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/loginagain 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": "..."
}
}| Parameter | Type | Description |
|---|---|---|
catalog_slug required | string | Catalog slug, e.g. neb. Find it in GET /v1/wire/catalog. |
identity_name | string | Optional 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. |
params | object | Wheel-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_id | string (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_ref | object | Optional. 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_nameis required when using the locator. With a locator there are noparamsto auto-derive a name from, so you must pass an explicitidentity_name. You also cannot send bothparamsand 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" } }
}| Field | Type | Description |
|---|---|---|
container | string | The container holding the entry |
key | string | The entry within the container. Required on entry, and on a fields ref when there is no entry to inherit one from |
property | string | A named field inside the entry — a 1Password field name, or tag:<name> for an Azure secret tag. Defaults to the whole entry |
version | string | A 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 (
username↔user/login/password↔pass/secret, and so on). If a required login field can't be resolved, the call fails withSOURCE_FIELD_MISSINGand names the fields, so you fix them in one pass.
Each parameter the action declares is resolved in this order:
- An explicit per-field ref in
fields— inheritingcontainer,keyandversionfromentrywherever they're omitted - An exact name match inside the entry
- An alias match, per the table above
- 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.
usernameandusername, 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,passwordregardless, and an action declaringusernameworked 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"
}| Field | Type | Description |
|---|---|---|
status | string | Always "verified" on success |
identity_id | string (UUID) | The identity this credential is attached to. Stable across re-runs with the same name. |
identity_name | string | The name used (explicit or derived) |
credential_id | string (UUID) | Pass this as credential_id in subsequent POST /v1/wire/task calls. |
catalog_id | string (UUID) | The catalog's UUID — saves you another lookup |
catalog_slug | string | Echoed back |
expires_at | string | RFC3339 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:
| Code | HTTP | When |
|---|---|---|
INVALID_BODY | 400 | Request body isn't valid JSON |
INVALID_INPUT | 400 | catalog_slug missing; OR neither params nor source_id+source_ref supplied; OR both supplied; OR locator used without identity_name |
INVALID_PARAMS | 400 | One 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_FOUND | 400 | No catalog matches catalog_slug |
IDENTITY_BINDING_MISMATCH | 409 | The named identity already exists bound to a different source/credential than the request |
LOGIN_NOT_SUPPORTED | 400 | The catalog's auth_types doesn't include credentials, OR the catalog has no published login action |
LOGIN_NOT_AVAILABLE | 503 | Credentials-mode login isn't configured on this engine (rare; infrastructure issue) |
LOGIN_TRANSPORT_ERROR | 502 | Could not reach the wheel runner |
Locator-only — the binding was rejected before the wheel ran:
| Code | HTTP | When |
|---|---|---|
INVALID_INTEGRATION_REF | 400 | source_ref isn't a JSON object. (Supplying only one half of the locator is reported as INVALID_INPUT above, not this) |
SOURCE_NOT_FOUND | 400 | The referenced source_id doesn't exist |
FORBIDDEN | 403 | The referenced source belongs to another user |
SOURCE_INACTIVE | 400 | The referenced source is not active — reconnect it |
PROVIDER_NOT_SUPPORTED_BY_CATALOG | 400 | The catalog doesn't list this source's provider in supported_sources. See GET /v1/wire/catalog/{slug} |
CATALOG_MISSING_REQUIRED_AUTH_TYPE | 400 | The catalog has no login wheel that can consume this provider's credentials |
PROVIDER_CREDENTIAL_TYPE_MISMATCH | 400 | The provider resolves to a different credential type than this request creates |
SOURCE_FIELD_MISSING | 400 | The entry can't supply a required login field. Response includes missing_fields: [...] |
SOURCE_AUTH_FAILED | 400 | The provider rejected the stored credential — rotate or reconnect the source |
SOURCE_FORBIDDEN | 400 | The source has no access to that container. Grant it access at the provider, then retry |
SOURCE_ITEM_NOT_FOUND | 400 | The referenced container or entry no longer exists |
SOURCE_THROTTLED | 429 | The provider is rate-limiting us. Retry shortly |
SOURCE_UNREACHABLE | 502 | Could not reach the provider |
SOURCE_NOT_AVAILABLE | 503 | Source 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:
| Code | HTTP | When |
|---|---|---|
BAD_PASSWORD | 200 | Sign-in rejected by the site — wrong credentials |
MFA_REQUIRED | 200 | Account requires multi-factor auth (not yet supported) |
CAPTCHA_REQUIRED | 200 | Site is showing a captcha challenge |
ACCOUNT_LOCKED | 200 | Site reports the account is locked |
LOGIN_TIMEOUT | 200 | Sign-in took too long |
LOGIN_PAGE_CHANGED | 200 | Site's login flow may have changed |
LOGIN_NO_COOKIES | 200 | Sign-in completed but no session cookie was issued |
LOGIN_INFRASTRUCTURE_ERROR | 200 | Transient Browser Connect / upstream issue. Retry. |
LOGIN_FAILED | 200 | Generic 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 anystatus != "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" }
}
}'import requests
response = requests.post(
'https://api.anakin.io/v1/wire/login',
headers={'X-API-Key': 'your_api_key'},
json={
'catalog_slug': 'neb',
'params': {
'email': 'alice@example.com',
'password': 'your_password',
},
},
)
data = response.json()
if data['status'] == 'verified':
credential_id = data['credential_id']
# Use credential_id with /v1/wire/task
else:
print(f"Login failed: {data['error']['code']} — {data['error']['message']}")
if data['error']['code'] == 'INVALID_PARAMS':
print(f"Missing: {data['error']['missing_fields']}")const response = await fetch('https://api.anakin.io/v1/wire/login', {
method: 'POST',
headers: {
'X-API-Key': 'your_api_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
catalog_slug: 'neb',
params: {
email: 'alice@example.com',
password: 'your_password',
},
}),
});
const data = await response.json();
if (data.status === 'verified') {
const credentialId = data.credential_id;
// Use credentialId with /v1/wire/task
} else {
console.error(`Login failed: ${data.error.code} — ${data.error.message}`);
if (data.error.code === 'INVALID_PARAMS') {
console.error('Missing:', data.error.missing_fields);
}
}Rate limit
10 requests per minute per user (each call triggers a real sign-in run against the target site).
Related
- GET /v1/wire/catalog/{slug} — discover
login_input_schema - GET /v1/wire/identities — list identities + credentials
- GET /v1/wire/identity-sources/{id}/containers — list containers for the locator
- GET /v1/wire/identity-sources/{id}/containers/{container_id}/entries — list entries for the locator
- POST /v1/wire/task — use the returned
credential_idto run an authenticated action - POST /v1/wire/identity-sources — connect a vault to bind against