Item Lifecycle

A Plaid item is a user’s connection to one institution; it holds the access_token used for every Plaid call about that institution. A user may have several items, and one account across them is the main account used for float disbursement and subscription collection. This page is the canonical reference for item statuses and every item operation; mining is in Plaid Mining.

Item Statuses

Status Meaning Set by

ACTIVE

Connected; Plaid reports no error.

Add, prequal, and Layer creation; miner status refresh and list enrichment when Plaid reports no error

ERROR

Plaid reports an error on the item (item_error_code); still counted as active for listing and main-account rules.

Miner status refresh and list enrichment when Plaid reports an error

REMOVED

Disconnected; the access token is revoked at Plaid.

Remove endpoints; miner on USER_PERMISSION_REVOKED; feeder on TTL expiry; list enrichment when Plaid reports the item does not exist

Source: pkg/dynamo/item.go

[ACTIVE] ──Plaid reports error──► [ERROR]
[ERROR]  ──Plaid clears error──► [ACTIVE]
[ACTIVE] or [ERROR] ──remove, revoke, TTL expiry──► [REMOVED]

"Active" items throughout this page means status ACTIVE or ERROR.

Main-Account Rules

Add v2, update v2, and prequal apply the same rules to the requested account_id before saving. Conditions are evaluated in order.

Condition Result

User has no active items

Continue; activation needed

account_id empty and no active item has a main account

400

account_id empty

Continue; no changes

An active item has a main account and the user has an active float (Float Service)

412 PRECONDITION_FAILED

account_id differs from the existing main account

Clear main_account_id on every other active item; activation needed

account_id equals the existing main account

Continue

Source: pkg/api/items_v2.go

Post-Save Actions

Run after an item is saved by add (v1 and v2), update v2, and prequal.

  1. main_account_id non-empty: publish new_bank_account_added (Events). Best effort; never fails the request.

  2. Register the webhook URL on the item at Plaid. Failure returns 500 from add and prequal; update v2 logs it.

  3. Add paths only: enqueue a synthetic INITIAL_UPDATE webhook (source=FROM_ADD_ITEM) when either onboarding_v3 (build and install id) or txn.plaid_initial_update.synthetic_webhook (user) is enabled — GrowthBook Flags.

Add Item

POST /{user_id}/transactions/items-v2 (v2):

  1. Exchange public_token for an access token and item id (Plaid); institution name from institutions.

  2. is_reactivating: set ttl to now plus REACTIVATION_ITEM_TTL_MINS.

  3. Apply Main-Account Rules.

  4. Save the item as ACTIVE.

  5. Run Post-Save Actions.

  6. Not reactivating and activation needed: activate the user (User Service). 404, 409, and 400 from the User Service are logged; any other non-202 returns 500.

  7. 201.

POST /{user_id}/transactions/items (v1) skips Main-Account Rules, activates the user before Post-Save Actions, and activates whenever neither is_reactivating nor skip_activation is set.

Prequal Item

POST /{user_id}/transactions/items/prequalify creates a temporary item for underwriting before the user commits.

  1. Exchange the token and build the item as in add.

  2. Set ttl to now plus PREQUAL_ITEM_TTL_DAYS; set item_source=PREQUAL.

  3. Apply Main-Account Rules; the previous main item’s main_account_id is cleared immediately.

  4. Save the item as ACTIVE; run Post-Save Actions without the synthetic webhook.

  5. 201 with item_id. No user activation.

The caller persists the item (Persist Item) once the user proceeds; otherwise it expires (TTL Expiry). Prequal items are ACTIVE, so they appear in item lists; consumers resolving a main account skip item_source=PREQUAL.

Plaid Layer

  1. POST /transactions/layer/session returns a Plaid Layer session token.

  2. POST /transactions/layer/session/account resolves the session’s account at Plaid and stores a Layer stub (item id and access token pairs, 7-day TTL) under the public_token.

  3. POST /{user_id}/transactions/layer/session/account creates one ACTIVE item per stub entry (no main account) and enqueues a synthetic HISTORICAL_UPDATE (source=FROM_LAYER_STUB) for each.

A stub whose TTL expires before step 3 is disconnected at Plaid by the feeder — Feeder.

Update Item

PATCH /{user_id}/transactions/items-v2/{item_id} (v2):

  1. Fetch the item by id; 404 if absent.

  2. Apply Main-Account Rules.

  3. Set main_account_id; write a plaid-account-changes row holding the previous account; publish the audit-log event.

  4. Run Post-Save Actions without the synthetic webhook.

  5. 202.

PATCH /{user_id}/transactions/items/{item_id} (v1) performs step 3 only, then publishes primary_bank_account_changed when the new id is non-empty. It has no float guard and does not clear other items' main accounts.

Remove Item

DELETE /{user_id}/transactions/items-v2/{item_id} (v2):

  1. Fetch by user and item; not found or already REMOVED: 202.

  2. Item has a main account and the user has an active float: 412.

  3. Revoke the access token at Plaid; any Plaid error other than item-not-found returns 503.

  4. Set REMOVED; write a plaid-account-changes row; 202.

DELETE /{user_id}/transactions/items/{item_id}/remove (v1) adds a ban check between steps 2 and 3: a user who joined less than MINUTES_BEFORE_PLAID_ITEM_CHANGE ago and has employment detected (Insight Service) is banned (User Service, or logged only when LOG_ONLY_PLAID_ITEM_BAN), sent the Iterable event conAccts_plaid_item_changed, and gets 403.

POST /{user_id}/transactions/items/{item_id}/remove/force skips the float check and records item_removed_reason=MX.

Listing items enriches each from Plaid. An item Plaid reports as not found is set REMOVED during the GET.

Change Check

POST /{user_id}/transactions/items/{item_id}/change/check tells the app whether the user may switch banks. Conditions are evaluated in order.

Condition Response

Item not found, has no main account, or is REMOVED

200, can_be_changed=true

User has an active float

412 PRECONDITION_FAILED

ALLOW_ITEM_CHANGE_WITHOUT_EMPLOYMENT and Insight reports no employment

200, can_be_changed=true

No plaid-account-changes rows

200, can_be_changed=true

Last change plus DAYS_BETWEEN_PLAID_ITEM_CHANGES is in the future

200, can_be_changed=false, days_until_next_change, date_of_last_change

Otherwise

200, can_be_changed=true, date_of_last_change

Persist Item

PATCH /{user_id}/transactions/items/{item_id}/persist clears ttl on the item (404 if absent) and returns 202.

Remine

POST /{user_id}/transactions/items/{item_id}/remine enqueues a synthetic HISTORICAL_UPDATE (source=API_REMINE) for an existing item (404 if absent) and returns 202.

TTL Expiry

Items carry a ttl when added with is_reactivating or through prequal. When DynamoDB expires the row, the feeder sets REMOVED with item_removed_reason naming the incomplete flow and revokes the token at Plaid, unless it is a prequal item of a user who is already an active member — Feeder.

  • POST /{user_id}/transactions/link returns a Link token for a new connection. A missing body returns 400; a user whose status is DENIED, INVESTIGATE, INACTIVE, OTHER, or TEST (User Service) returns 400. With change_bank, the token is created in update mode against the user’s first active item.

  • New-connection tokens request transactions (with transactions.days_requested 180), optional auth, and additional consented liabilities. Plaid Layer sessions take their history depth from the Layer template on the Plaid dashboard.

  • GET /{user_id}/transactions/items/{item_id}/link returns an update-mode token for one item; the update query parameter requests account-selection update mode.

  • POST /{user_id}/transactions/admin/items/{item_id}/reset-login (test only) resets a sandbox item to ITEM_LOGIN_REQUIRED.