> ## Documentation Index
> Fetch the complete documentation index at: https://docs.barker.money/llms.txt
> Use this file to discover all available pages before exploring further.

# Headless API integration

> Build your own deposit/withdraw UI on top of the Barker REST API + BarkerEngine contract.

In **headless mode** you keep full control of the UI and wallet connection. Barker provides:

* **REST API** for product metadata, APY history, fee stats, user position, vault health
* **Standard ERC-4626 contract** (`BarkerEngine`) for deposit / withdraw — call it directly from your frontend with any wallet library
* **Webhooks** for APY changes, TVL alerts, vault pauses

If you'd rather drop in a ready-made UI, see [Embed](/embed) instead.

## End-to-end flow

```mermaid theme={null}
sequenceDiagram
    participant U as User wallet
    participant FE as Your frontend
    participant API as Barker API
    participant E as BarkerEngine (on-chain)
    participant V as Underlying vault

    FE->>API: GET /api/partner/products
    API-->>FE: products[] (slug, engine_contract_address, asset_address, latest_metrics)
    FE->>API: GET /products/:slug/yield-calc?amount=10000
    API-->>FE: projected annual / monthly yield
    U->>FE: clicks "Deposit $10,000"
    FE->>U: request signature: USDC.approve(engine, 10000e6)
    U->>E: approve tx (signed in wallet)
    FE->>U: request signature: engine.deposit(10000e6, user)
    U->>E: deposit tx (signed in wallet)
    E->>V: forwards assets to underlying vault
    E-->>U: mints engine shares
    FE->>API: GET /products/:slug/position?address=0x...
    API-->>FE: shares, assets, usd_value
```

## 1. List products and pick one

```ts theme={null}
const res = await fetch("https://api.barker.money/api/partner/products", {
  headers: { "X-Api-Key": process.env.BARKER_API_KEY! },
});
const { data: products } = await res.json();

const product = products.find((p) => p.slug === "acme-usdc-base");
// product.engine_contract_address  → call deposit/redeem here
// product.asset_address             → USDC contract for approve()
// product.asset_decimals            → 6
// product.chain_id                  → switch chain check
// product.latest_metrics.net_apy_for_user → display APY
// product.risk_label                → "conservative" | "balanced" | "aggressive" | null (optional, for multi-tier UIs)
```

Cache this response for \~60 seconds — products change rarely.

## 2. Connect the user's wallet

Use whatever you already have — Wagmi / RainbowKit / Privy / your own connector. Barker doesn't care. The only requirement: by the time the user clicks "Deposit", their wallet is connected to **the same `chain_id` as the product** (e.g. Base = 8453).

```ts theme={null}
import { useAccount, useSwitchChain } from "wagmi";

const { address, chainId } = useAccount();
const { switchChain } = useSwitchChain();

if (chainId !== product.chain_id) {
  await switchChain({ chainId: product.chain_id });
}
```

## 3. Deposit

`BarkerEngine` is a standard ERC-4626 vault. Two transactions: **approve** the underlying token, then **deposit**.

<CodeGroup>
  ```ts viem theme={null}
  import { parseUnits } from "viem";
  import { erc20Abi } from "viem";
  import { writeContract, waitForTransactionReceipt } from "@wagmi/core";
  import { engineAbi } from "./abi/BarkerEngine"; // see Smart Contracts page

  const amount = parseUnits("10000", product.asset_decimals); // 10,000 USDC

  // 1. approve
  const approveTx = await writeContract(config, {
    address: product.asset_address,
    abi: erc20Abi,
    functionName: "approve",
    args: [product.engine_contract_address, amount],
  });
  await waitForTransactionReceipt(config, { hash: approveTx });

  // 2. deposit
  const depositTx = await writeContract(config, {
    address: product.engine_contract_address,
    abi: engineAbi,
    functionName: "deposit",
    args: [amount, address], // receiver = the same user
  });
  await waitForTransactionReceipt(config, { hash: depositTx });
  ```

  ```ts ethers v6 theme={null}
  import { Contract, parseUnits } from "ethers";

  const usdc = new Contract(product.asset_address, erc20Abi, signer);
  const engine = new Contract(product.engine_contract_address, engineAbi, signer);
  const amount = parseUnits("10000", product.asset_decimals);

  const approveTx = await usdc.approve(product.engine_contract_address, amount);
  await approveTx.wait();

  const depositTx = await engine.deposit(amount, await signer.getAddress());
  await depositTx.wait();
  ```
</CodeGroup>

After the deposit confirms, refresh the user's position (next step).

> **Tip — single-transaction UX**: the underlying USDC contract on most chains supports EIP-2612 `permit`. If you collect a `permit` signature off-chain you can replace the `approve` tx with a no-cost signature, then bundle `permit + deposit` in a single transaction. See [Smart Contracts → permit flow](/contracts-reference#permit-flow).

## 4. Show the user's position

```ts theme={null}
const res = await fetch(
  `https://api.barker.money/api/partner/products/${product.slug}/position?address=${address}`,
  { headers: { "X-Api-Key": process.env.BARKER_API_KEY! } },
);
const { data: position } = await res.json();
// position.shares     → engine shares (string, 18 decimals)
// position.assets     → underlying assets (string, asset_decimals)
// position.usd_value  → display string, e.g. "10500.12"
```

For real-time updates, also read `engine.balanceOf(user)` directly from the chain — the API serves the same value but with \~30s edge cache.

## 5. Withdraw / redeem

Two ERC-4626 entrypoints:

* `engine.redeem(shares, receiver, owner)` — burn N shares, receive whatever assets they're worth
* `engine.withdraw(assets, receiver, owner)` — receive exactly N assets, burn whatever shares are required

Most UIs show "Withdraw $X" → use `redeem` after converting `$X → shares`via`engine.previewWithdraw(assets)\`.

```ts viem theme={null}
const sharesToBurn = await readContract(config, {
  address: product.engine_contract_address,
  abi: engineAbi,
  functionName: "previewWithdraw",
  args: [parseUnits("5000", product.asset_decimals)], // user wants $5,000 back
});

const tx = await writeContract(config, {
  address: product.engine_contract_address,
  abi: engineAbi,
  functionName: "redeem",
  args: [sharesToBurn, address, address],
});
await waitForTransactionReceipt(config, { hash: tx });
```

`redeem` is one transaction — no `approve` needed (engine shares are owned by the user already).

## 6. Listen for on-chain events (optional)

If you want server-side reconciliation rather than polling, subscribe to engine events:

| Event                                                             | When           | Useful for                                     |
| ----------------------------------------------------------------- | -------------- | ---------------------------------------------- |
| `Deposit(sender, owner, assets, shares)`                          | User deposits  | Confirm deposit landed, update internal ledger |
| `Withdraw(sender, receiver, owner, assets, shares)`               | User redeems   | Confirm withdrawal landed                      |
| `FeeAccrued(barkerFeeShares, partnerFeeShares, totalAssets, ...)` | Yield is split | Reconcile partner fee earnings                 |

ABI fragments and full event signatures: [Smart Contracts → events](/contracts-reference#events).

## 7. Subscribe to webhooks (optional)

For events Barker pushes — lifecycle (`deposit.confirmed`, `redeem.confirmed`, `redeem.requested`) and operational (`apy_change`, `tvl_alert`, `vault_pause`) — see [Webhooks](/webhooks) and [Deposit/Redeem Lifecycle](/deposit-redeem-lifecycle). Not strictly required for a working UI (you can watch chain logs from the client — see the reference implementation below), but **strongly recommended for backend reconciliation**: DB writes, accounting, ops alerts.

## Reference implementation — Next.js + wagmi

The snippets above show each step in isolation. Here's the same flow as a single Next.js 16 App Router page that you can copy and adapt — three files, runnable end to end.

**Architecture in one sentence**: server component fetches product metadata with the API key (which **never leaves the server**); client component drives wallet + writes + uses wagmi's `useWatchContractEvent` for instant UI updates; a separate Route Handler receives Barker webhooks for **backend** reconciliation (DB, accounting). The two channels are independent — your UI doesn't block on webhook delivery, and your backend doesn't depend on the user keeping the tab open.

```
app/
  yield/[slug]/
    page.tsx                ← server component, fetches product
    yield-card.client.tsx   ← 'use client', wagmi tx + chain log watcher
  api/webhooks/barker/
    route.ts                ← HMAC verify, dedupe, hand off to your DB
```

<CodeGroup>
  ```tsx app/yield/[slug]/page.tsx theme={null}
  // Server component — API key stays on the server.
  import { YieldCard } from "./yield-card.client";

  type Params = { slug: string };

  export default async function YieldPage(
    { params }: { params: Promise<Params> },
  ) {
    const { slug } = await params;
    const res = await fetch(
      `https://api.barker.money/api/partner/products/${slug}`,
      {
        headers: { "X-Api-Key": process.env.BARKER_API_KEY! },
        next: { revalidate: 60 }, // cache product metadata for 60s
      },
    );
    if (!res.ok) throw new Error(`product fetch failed: ${res.status}`);
    const { data: product } = await res.json();
    return <YieldCard product={product} />;
  }
  ```

  ```tsx app/yield/[slug]/yield-card.client.tsx theme={null}
  "use client";

  import { useState } from "react";
  import {
    useAccount,
    useReadContract,
    useWriteContract,
    useWaitForTransactionReceipt,
    useWatchContractEvent,
    useSwitchChain,
  } from "wagmi";
  import { erc20Abi, formatUnits, parseUnits } from "viem";
  import { engineAbi } from "@/lib/abi/barker-engine";

  type Product = {
    slug: string;
    chain_id: number;
    engine_contract_address: `0x${string}`;
    asset_address: `0x${string}`;
    asset_decimals: number;
    asset_symbol: string;
    display_name: string;
    latest_metrics: { net_apy_for_user: string };
  };

  export function YieldCard({ product }: { product: Product }) {
    const { address, chainId } = useAccount();
    const { switchChain } = useSwitchChain();
    const [amount, setAmount] = useState("");
    const [pendingTx, setPendingTx] = useState<`0x${string}`>();

    // Read user position directly from the engine.
    const { data: shares = 0n, refetch: refetchPosition } = useReadContract({
      address: product.engine_contract_address,
      abi: engineAbi,
      functionName: "balanceOf",
      args: address ? [address] : undefined,
      chainId: product.chain_id,
      query: { enabled: !!address },
    });

    const { data: assets = 0n } = useReadContract({
      address: product.engine_contract_address,
      abi: engineAbi,
      functionName: "convertToAssets",
      args: [shares],
      chainId: product.chain_id,
      query: { enabled: shares > 0n },
    });

    // Watch this user's own Deposit logs → instant UI refresh.
    // (Backend reconciliation still flows through the Barker webhook.)
    useWatchContractEvent({
      address: product.engine_contract_address,
      abi: engineAbi,
      eventName: "Deposit",
      chainId: product.chain_id,
      args: address ? { owner: address } : undefined,
      onLogs: () => {
        refetchPosition();
        setPendingTx(undefined);
      },
      enabled: !!address,
    });

    const { writeContractAsync, isPending: isSigning } = useWriteContract();
    const { isLoading: isMining } = useWaitForTransactionReceipt({
      hash: pendingTx,
      chainId: product.chain_id,
    });

    async function handleDeposit() {
      if (!address) return;
      if (chainId !== product.chain_id) {
        await switchChain({ chainId: product.chain_id });
      }
      const value = parseUnits(amount, product.asset_decimals);

      const approveHash = await writeContractAsync({
        address: product.asset_address,
        abi: erc20Abi,
        functionName: "approve",
        args: [product.engine_contract_address, value],
      });
      setPendingTx(approveHash);

      const depositHash = await writeContractAsync({
        address: product.engine_contract_address,
        abi: engineAbi,
        functionName: "deposit",
        args: [value, address],
      });
      setPendingTx(depositHash);
    }

    const apyPct =
      (parseFloat(product.latest_metrics.net_apy_for_user) * 100).toFixed(2);

    return (
      <div className="rounded-lg border p-6 space-y-3">
        <h2 className="text-xl font-semibold">{product.display_name}</h2>
        <p>Net APY: {apyPct}%</p>
        <p>
          Your balance: {formatUnits(assets, product.asset_decimals)}{" "}
          {product.asset_symbol}
        </p>

        <input
          type="number"
          value={amount}
          onChange={(e) => setAmount(e.target.value)}
          placeholder={`Amount in ${product.asset_symbol}`}
          className="border rounded px-2 py-1 w-full"
        />
        <button
          onClick={handleDeposit}
          disabled={!address || !amount || isSigning || isMining}
          className="w-full rounded bg-blue-600 text-white py-2 disabled:opacity-50"
        >
          {isSigning ? "Sign in wallet…" :
           isMining ? "Confirming on-chain…" :
           !address ? "Connect wallet" : "Deposit"}
        </button>
      </div>
    );
  }
  ```

  ```ts app/api/webhooks/barker/route.ts theme={null}
  // Webhook receiver — HMAC verify + idempotent dedupe + hand-off.
  // Returns 200 quickly; do real work async if it would take >10s.
  import crypto from "node:crypto";
  import { NextRequest, NextResponse } from "next/server";
  import { db } from "@/lib/db"; // your DB client

  const SECRET = process.env.BARKER_WEBHOOK_SECRET!;

  export async function POST(req: NextRequest) {
    // 1. Read raw body (NOT req.json() — re-stringifying breaks the HMAC).
    const body = await req.text();
    const signature = req.headers.get("x-barker-signature") ?? "";
    const eventType = req.headers.get("x-barker-event") ?? "";

    // 2. Verify HMAC-SHA256 over the exact bytes.
    const expected = crypto.createHmac("sha256", SECRET).update(body).digest("hex");
    if (
      signature.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
    ) {
      return NextResponse.json({ error: "invalid signature" }, { status: 401 });
    }

    const payload = JSON.parse(body);

    // 3. Dedupe. Lifecycle events: (event_type, chain_id, tx_hash, log_index).
    //    Operational events: (event_type, slug, date) or (event_type, slug).
    //    A unique index + ON CONFLICT DO NOTHING is the race-safe pattern.
    const isLifecycle =
      eventType === "deposit.confirmed" ||
      eventType === "redeem.confirmed" ||
      eventType === "redeem.requested";

    const dedupeKey = isLifecycle
      ? `${eventType}:${payload.chain_id}:${payload.tx_hash}:${payload.log_index}`
      : `${eventType}:${payload.slug}:${payload.date ?? "active"}`;

    const inserted = await db.webhookEvents.create({
      data: { id: dedupeKey, eventType, payload },
      skipDuplicates: true,
    });
    if (!inserted) {
      return NextResponse.json({ ok: true, duplicate: true });
    }

    // 4. Hand off to your business logic. Return 200 quickly.
    switch (eventType) {
      case "deposit.confirmed":
        // Update your ledger, mark pending deposit as confirmed.
        // payload: { slug, chain_id, tx_hash, user_address, amount, shares, ... }
        break;
      case "redeem.confirmed":
        break;
      case "vault_pause":
        // Disable your "Deposit" CTA for this product.
        break;
      // Treat unknown event_type as no-op.
    }

    return NextResponse.json({ ok: true });
  }
  ```
</CodeGroup>

**Two channels, two purposes** — keep them straight:

| Channel                 | Lives in     | Triggers                                               | Purpose                                                   |
| ----------------------- | ------------ | ------------------------------------------------------ | --------------------------------------------------------- |
| `useWatchContractEvent` | Browser      | Engine `Deposit` / `Withdraw` log seen by user's RPC   | Instant UI refresh while the user is on the page          |
| Webhook Route Handler   | Your backend | Barker watcher confirms log at depth, signs + delivers | Durable backend state — DB writes, accounting, ops alerts |

If the user closes the tab mid-deposit, the UI watcher misses the event but the webhook still fires — your backend stays consistent. If your backend webhook endpoint is down, the UI still updates instantly — Barker retries the webhook for you. Don't try to make either channel do both jobs.

**What's omitted from the snippet** (kept tight on purpose; add as you go):

* Wagmi config + `<WagmiProvider>` setup — see [wagmi.sh/react/getting-started](https://wagmi.sh/react/getting-started)
* Permit-based single-tx UX — see [Smart Contracts → permit](/contracts-reference#permit-optional-saves-one-tx)
* Engine ABI import — see [Smart Contracts → minimal ABI](/contracts-reference#minimal-abi-fragment-copy-paste)
* Async-vault `redeem.requested` handling — see [Lifecycle → async vault sequence](/deposit-redeem-lifecycle#async-vault-sequence)
* Failure handling (RPC outage, tx revert, replacement) — see [Failure modes](/failure-modes)

## Sandbox checklist

Before you commit production engineering time, verify the headless flow against sandbox:

* [ ] `bk_test_*` key issued ([Portal → API Keys](https://portal.barker.money/api-keys))
* [ ] Sandbox product slug visible in `GET /api/partner/products`
* [ ] Sandbox USDC obtained from the [Sandbox page → Mint Test USDC](https://portal.barker.money/sandbox)
* [ ] `approve + deposit` round-trips on testnet, position endpoint reflects new balance within 30s
* [ ] `redeem` round-trips, position drops to 0
* [ ] Same code path works against `bk_live_*` key (only the key changes, base URL is identical)

## Production checklist

* [ ] Production engine deployed (status `active` in [Portal → Products](https://portal.barker.money/products))
* [ ] Frontend reads `chain_id` from the product response and switches chain before signing
* [ ] Error states surfaced: vault paused (poll `/health`), insufficient allowance, user rejected signature, network mismatch
* [ ] Slippage guard / minimum shares check on `deposit` (`previewDeposit` matches your displayed APY ± a tolerance)
* [ ] [Compliance disclosures](/compliance) shown to user before deposit

## What's next

* [Smart Contracts reference](/contracts-reference) — ABI, deployed addresses, event signatures
* [Multi-tier products](/multi-tier-products) — offer 3 risk profiles
* [Errors](/error-codes) — error code catalog
* [Webhooks](/webhooks) — push events from Barker → you
