Plaid Integration skill for Codex

Bank connections that sync cleanly and stay connected. Codex wires Plaid Link through a server-created link token, keeps access tokens encrypted and out of logs, and syncs transactions from a stored cursor applied in one database transaction. Webhooks are verified with Plaid's signed JWT and body hash, ITEM_LOGIN_REQUIRED routes users through update mode instead of new Items, and everything is proven in Sandbox first.

  • Integrations
  • $plaid-integration
  • 187 lines
  • 2 files
  • ~2,050 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- plaid-integration

Installs to ~/.agents/skills/plaid-integration/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $plaid-integration, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Integrates Plaid for read-only financial data - Link token creation, public token exchange, encrypted access token storage, Transactions Sync with cursors, Auth, Balance and Identity, signed webhook verification, update mode for ITEM_LOGIN_REQUIRED, OAuth institution redirects and Sandbox testing. Use when connecting bank accounts with Plaid Link, syncing transactions, verifying account ownership, handling Plaid webhooks or errors, or preparing a Plaid integration for production.

Use it when

  • Connecting bank accounts with Plaid Link in a web or mobile app
  • Importing and keeping transactions in sync with a cursor
  • Verifying and handling Plaid webhooks
  • Items keep breaking or duplicating after users reconnect

Use something else when

  • You are taking card payments rather than reading bank data: use stripe-payments
  • You need modelling or forecasting from the data rather than the integration: use financial-modeling

How to check the result

A Sandbox Item goes through Link, exchange, fired webhook and repeat sync without duplicate transactions, and reset_login is repaired by update mode without a new item_id.

The mistake it prevents

Creating a brand-new Item when a login breaks instead of using update mode, which duplicates history and billing.

~/.agents/skills/plaid-integration/SKILL.md

--- name: plaid-integration description: Integrates Plaid for read-only financial data - Link token creation, public token exchange, encrypted access token storage, Transactions Sync with… license: MIT ---

Done well means: access tokens never reach the browser or logs, transactions sync incrementally from a stored cursor and survive restarts, webhooks are verified before they are trusted, broken logins are repaired through update mode, and the integration only reads data. This skill does not cover moving money (Transfer, payment initiation) or giving financial advice.

Inspect first

  1. Read AGENTS.md; search with rg -n "plaid|link_token|access_token" --glob '!node_modules'.
  2. Note the environment (sandbox or production; the old development environment is gone), the products enabled on the Plaid dashboard, and the countries the app serves.
  3. Find where secrets live. PLAID_CLIENT_ID and PLAID_SECRET are per environment and server only.
  4. Confirm the data the product needs. Request the fewest products: each one adds consent text, cost and review scope.

Flow

browser: request link_token  ->  server: /link/token/create
browser: open Link (token)   ->  user connects bank
browser: onSuccess(public_token) -> server: /item/public_token/exchange -> access_token + item_id
server: store encrypted access_token, then fetch data / wait for webhooks

Server setup (Node)

import { Configuration, PlaidApi, PlaidEnvironments, Products, CountryCode } from 'plaid';

export const plaid = new PlaidApi(new Configuration({
  basePath: PlaidEnvironments[process.env.PLAID_ENV ?? 'sandbox'],
  baseOptions: { headers: {
    'PLAID-CLIENT-ID': process.env.PLAID_CLIENT_ID!,
    'PLAID-SECRET': process.env.PLAID_SECRET!,
  } },
}));

export async function createLinkToken(userId: string) {
  const { data } = await plaid.linkTokenCreate({
    user: { client_user_id: userId },              // your stable internal ID, not email
    client_name: 'Acme Budget',
    products: [Products.Transactions],
    country_codes: [CountryCode.Us],
    language: 'en',
    webhook: 'https://api.acme.example/webhooks/plaid',
    redirect_uri: process.env.PLAID_REDIRECT_URI,  // required for OAuth banks; must be registered
  });
  return data.link_token;                          // short-lived; create per session
}

export async function exchange(userId: string, publicToken: string) {
  const { data } = await plaid.itemPublicTokenExchange({ public_token: publicToken });
  await db.items.upsert({
    userId, itemId: data.item_id,
    accessToken: encrypt(data.access_token),       // KMS or app-level envelope encryption
    cursor: null, status: 'active',
  });
}

Python uses plaid-python with the same request names (LinkTokenCreateRequest, ItemPublicTokenExchangeRequest). Installing either SDK needs network approval in the sandbox.

import { usePlaidLink } from 'react-plaid-link';

const { open, ready } = usePlaidLink({
  token: linkToken,
  onSuccess: (public_token, metadata) =>
    fetch('/api/plaid/exchange', { method: 'POST', body: JSON.stringify({ public_token }) }),
  onExit: (err, metadata) => { if (err) report(err.error_code, metadata.link_session_id); },
});
  • OAuth institutions redirect back to redirect_uri; on that page re-initialise Link with the same link token and receivedRedirectUri: window.location.href. Store the token in session storage before opening Link.
  • Log link_session_id and request_id values; Plaid support asks for them.

Products and endpoints

NeedEndpointNotes
Transactions/transactions/syncCursor-based; replaces /transactions/get
Account and routing numbers/auth/getShow masked numbers only
Real-time balance/accounts/balance/getBilled per call and slow; cache, do not poll
Account holder name, email, address/identity/getPersonal data; minimise and protect
Item health/item/getCheck error and consent expiry

Transactions sync

export async function syncItem(itemId: string) {
  const item = await db.items.get(itemId);
  let cursor = item.cursor ?? undefined;
  let added = [], modified = [], removed = [];
  for (;;) {
    const { data } = await plaid.transactionsSync({ access_token: decrypt(item.accessToken), cursor, count: 500 });
    added.push(...data.added); modified.push(...data.modified); removed.push(...data.removed);
    cursor = data.next_cursor;
    if (!data.has_more) break;
  }
  await db.tx(async (t) => {                       // apply all pages and the cursor together
    await t.transactions.upsertMany(added.concat(modified));     // keyed by transaction_id
    await t.transactions.deleteMany(removed.map((r) => r.transaction_id));
    await t.items.update(itemId, { cursor });
  });
}
  • Trigger sync on the SYNC_UPDATES_AVAILABLE webhook (webhook_type TRANSACTIONS) and once after exchange. Run one sync per item at a time (lock on item_id).
  • If a page fails with TRANSACTIONS_SYNC_MUTATION_DURING_PAGINATION, restart from the cursor you stored before the loop.
  • Pending transactions are replaced by posted ones with a new transaction_id (pending_transaction_id links them); the removal arrives in removed.
  • Amounts are positive for money leaving the account; check the sign convention before display.

Webhooks

Verify the Plaid-Verification header before acting:

  1. Decode the JWT header without verifying; require alg = ES256 and read kid.
  2. Fetch the key with /webhook_verification_key/get (key_id: kid); cache keys by kid.
  3. Verify the JWT signature with that JWK and reject if iat is older than 5 minutes.
  4. Compare the claim request_body_sha256 to the SHA-256 of the raw request body (constant time).

Then enqueue and return 200 quickly. Handle at least:

WebhookAction
TRANSACTIONS / SYNC_UPDATES_AVAILABLERun syncItem
ITEM / ERROR with ITEM_LOGIN_REQUIREDMark item needs_reauth, prompt the user
ITEM / PENDING_EXPIRATION or PENDING_DISCONNECTPrompt the user to re-consent before data stops
ITEM / USER_PERMISSION_REVOKED / USER_ACCOUNT_REVOKEDStop syncing, delete data per policy
ITEM / NEW_ACCOUNTS_AVAILABLEOffer update mode with account selection

Update mode

For ITEM_LOGIN_REQUIRED and expiring consent, create a link token with the existing access_token (and no products), open Link, and on success keep the same access token; no exchange is needed. Do not create a new Item for the same login; duplicates cost money and split transaction history.

Removing an Item

/item/remove invalidates the access token permanently and cannot be undone. Call it only when the user disconnects an account or deletes their profile, after explicit confirmation, and then delete or anonymise the stored data according to the retention policy.

Sandbox testing

  • Link credentials user_good / pass_good; MFA and error cases have documented test users.
  • Skip Link in automated tests: /sandbox/public_token/create with an institution such as ins_109508, then exchange as normal.
  • Force events with /sandbox/item/fire_webhook (for example SYNC_UPDATES_AVAILABLE) and /sandbox/item/reset_login to test update mode.
  • Expose the webhook URL locally with $webhook-testing-tools.

Verification

  • Sandbox Item end to end: Link, exchange, initial sync, fired webhook, second sync with no duplicates.
  • reset_login then update mode restores the Item without creating a new item_id.
  • A webhook with a modified body or an old iat is rejected.
  • rg over logs and error reports finds no access-sandbox- / access-production- strings.
  • Restarting the worker mid-sync resumes from the stored cursor.

Compliance notes

  • Show data use clearly in the UI before Link opens; Plaid’s production access review checks this.
  • Encrypt tokens and financial data at rest, restrict access, and document retention and deletion.
  • Do not present derived insights as investment, tax or credit advice; regulatory questions belong to the user’s counsel and compliance team.

Output

Report: products and environment, endpoints used, token storage method, sync and webhook design, update-mode handling, and sandbox test results.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Plaid Integration"
  short_description: "Bank connections that sync cleanly and stay connected"
  default_prompt: "$plaid-integration Let users connect their bank with Plaid and import transactions"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem