🚧 Bloque documentation is under development
Before this
  • Connected SDK client
  • KYC (if enforced by your origin)
  • A browser to open the hosted Plaid Link page — or a frontend that can run Plaid Link
After this10 min
  • Link a US bank account via Plaid
  • Pull funds via ACH directly into DUSD on Kusama
  • Store a stable bank URN for repeat on-ramps

External US Bank (Plaid)

Link a US bank account using Plaid, then debit it on demand via Brale ACH. The proceeds land as DUSD on Kusama Asset Hub and are teleported straight to the Kreivo ledger account associated with the bank URN — one call, one swap order, no escrow hop.

Two flows

Pick one when you call create():

FlowWhenWhat you write
Hosted pageDefault. Web, mobile webview, email.Backend only. Open details.linkUrl in a browser. Bloque runs Plaid Link and exchanges the token on redirect.
Embedded Plaid LinkYou need full UI control inside your own app.Backend + frontend. Drive Plaid Link with details.linkToken, then exchange the public_token from your backend.

Pass returnUrl on create() (the SDK sends it in the medium input body). The server mints a short-lived session token, builds a hosted URL, and returns it as details.linkUrl (and may include details.jwt).

start-link.ts
const pending = await user.accounts.externalUsBank.create({
  label: 'My bank',
  ledgerId: pocket.ledgerId,
  returnUrl: 'https://app.example.com/wallet/plaid-return',
  state: 'user-session-xyz', // optional opaque correlator
});

if (!pending.details.linkUrl) {
  throw new Error('Expected linkUrl — check returnUrl origin allowlist');
}

// Send the user to the hosted page (redirect, email, deep link…)
redirectTo(pending.details.linkUrl);

The hosted page handles Plaid Link, exchanges the public_token, then redirects to:

https://app.example.com/wallet/plaid-return?status=success&state=user-session-xyz

status is success, cancelled, or error. When the user returns, read final state:

handle-return.ts
const linked = await user.accounts.get(pending.urn);

console.log(linked.details.linkStatus);          // 'active' | 'pending_link' | 'link_failed' | 'closed'
console.log(linked.details.bankName);
console.log(linked.details.bankAccountLast4);
Info

returnUrl origin must be allowlisted The server validates the origin of returnUrl against PLAID_LINK_RETURN_URL_ALLOWLIST. If it isn't allowlisted, the create call fails before any link token is minted.

Omit returnUrl. Use details.linkToken to run Plaid Link inside your own frontend, then call exchangePublicToken() from your backend.

start-link.ts
const pending = await user.accounts.externalUsBank.create({
  label: 'My bank',
  ledgerId: pocket.ledgerId, // optional: attach to an existing ledger pool
});

if (!pending.details.linkToken) {
  throw new Error('Expected Plaid linkToken');
}

// pending.details.linkToken           → pass to Plaid Link on the frontend
// pending.details.linkTokenExpiration → ISO 8601 expiry

Run Plaid Link with the linkToken. Plaid returns a public_token when the user completes the flow. Exchange it:

finish-link.ts
const linked = await user.accounts.externalUsBank.exchangePublicToken({
  urn: pending.urn,
  publicToken: '<PLAID_PUBLIC_TOKEN>',
});

console.log(linked.details.linkStatus); // 'active' | 'pending_link' | 'link_failed' | 'closed'
console.log(linked.details.bankName, linked.details.bankAccountLast4);

What to persist

pending.urn. Treat it as the stable identifier for the linked bank account in both flows.

Field reference (response details)

FieldTypeWhen present
linkStatus'pending_link' | 'active' | 'link_failed' | 'closed'always
linkTokenstringboth flows (use it for embedded Plaid Link)
linkTokenExpirationstring (ISO 8601)both flows
linkUrlstringhosted page flow only (returnUrl supplied at create)
jwtstringoptional; short-lived token for the hosted page when the server issues one
braleAccountIdstringonce Brale has provisioned the account
braleAddressIdstringafter public_token is exchanged
bankNamestringafter public_token is exchanged
bankAccountLast4stringafter public_token is exchanged
failureReasonstringwhen linkStatus === 'link_failed'

Pull funds (ACH debit → DUSD on Kusama)

Once linkStatus === 'active', call pull() to debit the bank via Brale ACH and swap the proceeds to DUSD on Kusama. The DUSD is teleported directly to the Kreivo ledger account associated with the bank URN — no intermediate escrow.

pull.ts
const order = await user.accounts.externalUsBank.pull({
  urn: linked.urn,     // active external-us-bank account URN
  amount: '100.00',    // USD as a decimal STRING — never a number
});

console.log(order.orderSig);  // "0x…" — correlate webhooks (swap.order.*)
console.log(order.status);    // "pending"
console.log(order.graphId);   // instruction graph executing the swap

Parameters

NameTypeRequiredDescription
urnstringyesURN of the linked bank. Must be on the external-us-bank medium and owned by the caller. Bank must be in linkStatus === 'active'.
amountstringyesUSD amount to debit, as a decimal string (e.g. "100.00"). Always pass as a string to avoid float precision loss. Must be positive.
idempotencyKeystringnoCaller hint. Informational — the server keys idempotency on the swap signature internally.

Response

interface PullExternalUsBankResult {
  orderSig?: string;   // stable handle for the swap order — use for webhook correlation
  graphId?: string;    // instruction graph executing the swap
  status?: string;     // initial swap status ("pending", "running", …)
  execution?: unknown; // raw swap.take execution payload (opaque)
  requestId?: string;  // mediums service request id (for support tickets)
}

Error handling

BloqueAPIError statusCauseResolution
400amount is not a positive decimal string, or urn is malformed/wrong mediumValidate input on the client.
401Caller is not authenticatedRefresh the user session.
403URN belongs to a different userThe bank account is not owned by the caller — fetch the correct URN with user.accounts.list().
404No address mapping for the URN, or the account has no ledgerPlaid Link hasn't completed; check linkStatus === 'active' and details.braleAddressId is populated.
503No swap rate available for external-us-bank → kusamaTransient. Retry after a short backoff.
with-error-handling.ts
import { BloqueAPIError } from '@bloque/sdk-core';

try {
  const order = await user.accounts.externalUsBank.pull({
    urn: linked.urn,
    amount: '100.00',
  });
  console.log('Order created:', order.orderSig);
} catch (err) {
  if (err instanceof BloqueAPIError) {
    if (err.status === 404) {
      console.error('Bank not linked yet — finish Plaid Link first.');
    } else if (err.status === 503) {
      console.error('No rate right now. Retry in a few seconds.');
    } else {
      console.error('Pull failed:', err.status, err.message);
    }
  }
  throw err;
}

Pre-flight check

check-ready.ts
const account = await user.accounts.get(linked.urn);

if (
  account.details.linkStatus !== 'active' ||
  !account.details.braleAddressId
) {
  throw new Error('Bank not ready for ACH pull — finish Plaid Link first.');
}

const order = await user.accounts.externalUsBank.pull({
  urn: linked.urn,
  amount: '100.00',
});

Tracking the swap to completion

The pull endpoint returns immediately after swap.take auto-executes the first node of the instruction graph (which resolves the Brale account/address). The actual ACH settlement and Kusama teleport happen asynchronously. Two ways to observe progress:

  • Webhooks — subscribe to swap.order.* and transfer.* events; match on order.orderSig.
  • Polling — query the swap service for the order by orderSig.

Webhooks

If you configured a webhookUrl during account creation, you can receive lifecycle and transfer events for this medium via your webhook endpoint.