# Getting Started

Go from zero to retrieving bills in 6 steps with the official `billerapi` Node SDK. This guide uses the sandbox environment with test data — no real accounts needed.

> **Tip — Working with an agent?**
> Have it read [llms.txt](/llms.txt) first. It may run the account-free, read-only
> CLI demo and summarize the public biller catalog without handling credentials or
> consent. Never let it create an account,
> handle a password, choose consent, or use a production key on your behalf.

## 1. Create a Developer Account

Sign up at the Client Portal. There's no email round-trip to get started — creating your account signs you in immediately and drops you on the dashboard with a working sandbox key already minted.

1. Go to the **Client Portal** and click **Sign Up**
2. You're signed in right away and land on your developer dashboard
3. Your sandbox `bak_test_` key is ready to copy — start building

> **Note**
> Email verification happens later, in-app, and only gates **production** access — it never blocks signing in or using the sandbox. Verify your email (and complete the go-live checklist) when you're ready to send live traffic.

## 2. Get Sandbox Credentials

Your sandbox key is on the dashboard the moment you sign up. Grab it from the **Keys** page (or the “Make your first API call” card on the dashboard). It's a self-contained Bearer token, so it's the only credential you need.

> **Note**
> Sandbox keys have the `bak_test_` prefix (production keys use `bak_live_`). The SDK reads the prefix and targets the matching environment (`https://sandbox.api.billerapi.com` for sandbox) automatically — you never set a base URL.

![Client portal dashboard after signup, with the sandbox API key ready to copy](/marketing/demos/portal-dashboard.png)

## 3. Install the SDK & List Billers

Install the SDK, then list available billers — the simplest call to verify your key works.

**Install**

**npm**

```bash
npm install billerapi
```

**pnpm**

```bash
pnpm add billerapi
```

**yarn**

```bash
yarn add billerapi
```

**List billers**

**Node SDK**

````typescript
import { BillerApi, BillerApiError } from 'billerapi';

export async function listBillers(
  apiKey: string,
  log: (line: string) => void = console.log,
): Promise<void> {
  const billerapi = new BillerApi(apiKey);
  try {
    for await (const biller of await billerapi.billers.list()) {
      log(`${biller.id} ${biller.name} ready_for_bills=${biller.ready_for_bills}`);
    }
  } catch (error) {
    if (error instanceof BillerApiError) {
      log(`${error.code} request_id=${error.requestId ?? "unknown"}`);
    }
    throw error;
  }
}
````

### HTTP reference (no SDK)

**cURL**

```curl
curl https://sandbox.api.billerapi.com/v1/billers \
  -H "Authorization: Bearer $BILLERAPI_API_KEY"
```

> **Tip**
> In sandbox, you'll see 6 test billers: **Sandbox Utility**, **Sandbox Power**, **Sandbox Gas**, **Sandbox Water**, **Sandbox Electric**, and **Sandbox Telecom**.

## 4. Link a Test Account

Linking is a two-part flow: your server creates a link token, then the browser SDK handles the user experience and returns a public token you exchange server-side.

### Server: Create a link token

**Node SDK**

```typescript
import { BillerApi } from 'billerapi';

export async function createLinkToken(apiKey: string) {
  const billerapi = new BillerApi(apiKey);
  return billerapi.links.createToken({
    client_user_id: 'user_123',
    biller_id: 'sb_utility',
    consents: ['bills:read'],
  });
}

// Send link_token to Connect. Keep link_token_id on your server.
```

### HTTP reference (no SDK)

**cURL**

```curl
curl -X POST https://sandbox.api.billerapi.com/v1/link-tokens \
  -H "Authorization: Bearer $BILLERAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "client_user_id": "user_123",
    "biller_id": "sb_utility",
    "consents": ["bills:read"]
  }'
```

### Server: Inspect continuation state

Keep `link_token_id` on your server. This status is authoritative: use `next_action`, `blocking_reason`, and `retryable` instead of inferring progress from elapsed time.

**Node SDK**

```typescript
import { BillerApi, type LinkTokenStatusResult } from 'billerapi';

export async function inspectLinkToken(
  apiKey: string,
  linkTokenId: string,
): Promise<LinkTokenStatusResult> {
  const billerapi = new BillerApi(apiKey);
  const continuation = await billerapi.links.retrieveTokenStatus(linkTokenId);

  if (continuation.next_action === 'RETRY' && !continuation.retryable) {
    throw new Error('Connect retry was not approved');
  }
  return continuation;
}
```

### Client: Open the Connect flow

**Browser SDK**

```typescript
import { BillerApiElements } from 'billerapi-js';

export function openConnect(clientId: string, linkToken: string): void {
  const elements = new BillerApiElements({ clientId, environment: 'sandbox' });
  elements.connect({
    linkToken,
    onSuccess: async (publicToken, metadata) => {
      if (metadata.completion_mode === 'update') return;
      await fetch('/api/exchange-token', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ public_token: publicToken }),
      });
    },
    onExit: (error) => {
      if (error) {
        console.error(error.code, error.blocking_reason, error.next_action, error.retryable);
      }
    },
  }).open();
}
```

> **Tip — Sandbox test credentials**
> Use account number `4242424242` with any username and password. This always succeeds. See the sandbox reference table below for other test scenarios.

### Server: Exchange the public token

**Node SDK**

```typescript
import { BillerApi } from 'billerapi';

export async function exchangeToken(apiKey: string, publicToken: string) {
  const billerapi = new BillerApi(apiKey);
  return billerapi.links.exchangeToken({ public_token: publicToken });
}
```

### HTTP reference (no SDK)

**cURL**

```curl
curl -X POST https://sandbox.api.billerapi.com/v1/link-tokens/public-sandbox-.../exchange \
  -H "Authorization: Bearer $BILLERAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

## 5. Retrieve Bills

Use the `link_id` from Step 4 to fetch bills for the linked account. Bills are scoped to your API key — you pass the link, not the access token.

Read money and dates from the canonical fields: `total_amount` (an ISO 20022 object with `value` and `currency`) and `due_date_iso` (`YYYY-MM-DD`). The REST plane still returns the deprecated `amount` / `currency` / `due_date` trio alongside them for back-compat; the 2.0 Node SDK `Bill` type no longer exposes it.

**Fetch bills**

**Node SDK**

```typescript
import { BillerApi, type Bill } from 'billerapi';

export async function listBills(apiKey: string, linkId: string): Promise<Bill[]> {
  const billerapi = new BillerApi(apiKey);
  const bills: Bill[] = [];
  for await (const bill of await billerapi.bills.list({ account_link_id: linkId })) {
    bills.push(bill);
  }
  return bills;
}
```

### HTTP reference (no SDK)

**cURL**

```curl
curl "https://sandbox.api.billerapi.com/v1/bills?account_link_id=$LINK_ID" \
  -H "Authorization: Bearer $BILLERAPI_API_KEY"
```

## 6. Listen for Webhooks

Register a webhook for the durable confirmation chain. Connect completion does not mean bills are visible yet: wait for `connection.ready`, then consume `bill.created` and `bill.updated`.

### Register a webhook endpoint

Register endpoints with the SDK (the signing secret is returned once — persist it immediately), or from the **Client Portal** under **Developer → Webhooks**.

**Node SDK**

```typescript
import { BillerApi } from 'billerapi';

export async function createWebhook(apiKey: string) {
  const billerapi = new BillerApi(apiKey);
  const endpoint = await billerapi.webhookEndpoints.create({
    url: 'https://your-server.com/webhooks/billerapi',
    events: ['connection.ready', 'bill.created', 'bill.updated'],
  });
  if (!endpoint.secret) throw new Error('Webhook signing secret was not returned');
  return { id: endpoint.id, secret: endpoint.secret };
}
```

### Verify incoming events

Verify and parse each event in one call with `billerapi.webhooks.constructEvent()`, which throws if the HMAC signature or timestamp doesn't check out.

**Node SDK**

```typescript
import { BillerApi } from 'billerapi';

interface BillCreatedEvent {
  id: string;
  object: 'event';
  type: 'bill.created';
  api_version: string;
  created: number;
  data: { object: { id: string; object: 'bill'; user_id: string; biller_id: string } };
  request: { id: string | null; idempotency_key: string | null };
  producer_service: string;
  correlation_id: string | null;
}

export function verifyWebhook(
  apiKey: string,
  rawBody: string | Buffer,
  signatureHeader: string | undefined,
  webhookSecret: string,
): BillCreatedEvent {
  const billerapi = new BillerApi(apiKey);
  return billerapi.webhooks.constructEvent<BillCreatedEvent>(
    rawBody,
    signatureHeader,
    webhookSecret,
  );
}
```

> **Note**
> For the exact signature header, retry policy, and all event types, see the [Webhooks](/docs/guides/webhooks) guide.

### Develop locally with the CLI

No public URL yet? Use the BillerAPI CLI to stream live sandbox events straight to your local server — each event arrives as an HMAC-signed POST, exactly like production.

**Terminal**

```bash
# Authenticate the CLI with a sandbox key
billerapi login --env sandbox

# Forward live events to your local server (HMAC-signed)
billerapi listen --forward-to http://localhost:4242/webhooks

# In another terminal, fire a test event into the sandbox
billerapi trigger bill.created
```

## Sandbox Reference

Use these magic account numbers in the Connect flow to trigger specific scenarios.

| Account Number | Scenario | Behavior |
| --- | --- | --- |
| `4242424242` | Success | Always succeeds. Returns a $127.50 pending bill. |
| `4000000001` | Auth Failure | Returns INVALID_CREDENTIALS error. |
| `4000000002` | Unavailable | Returns BILLER_UNAVAILABLE error. |
| `4000000003` | MFA Required | Prompts for MFA. Use code `123456`. |
| `4000000004` | Locked | Returns ACCOUNT_LOCKED error. |
| `4000000005` | Past Due | Returns overdue bills ($245.00) with late fees. |
| `4000000006` | Auto-Pay | Returns a scheduled $89.99 bill with auto-pay enabled. |
| `4000000007` | Payment Plan | Returns 3 installments of $150.00 each. |
| `4000000008` | Multi-Account | Account discovery returns multiple accounts. |

### Test Billers

| Biller ID | Name | Type |
| --- | --- | --- |
| `sb_utility` | Sandbox Utility | Utility |
| `sb_power` | Sandbox Power | Utility |
| `sb_gas` | Sandbox Gas | Utility |
| `sb_water` | Sandbox Water | Utility |
| `sb_electric` | Sandbox Electric | Utility |
| `sb_telecom` | Sandbox Telecom | Telecom |

## Next Steps

- [API Reference](/docs/api/billers) — Complete endpoint documentation
- [Elements SDK](/docs/guides/elements-sdk) — Hosted flow reference and options
- [Webhooks](/docs/guides/webhooks) — Event types, signatures, and testing
- [Authentication](/docs/guides/authentication) — Security, rate limits, and best practices

## Related API Reference

- [Billers API](/docs/api/billers) — Search and list billers
- [Link API](/docs/api/link-sessions) — Token creation and exchange
- [Bills API](/docs/api/bills) — Retrieve and manage bills
