﻿# Alad for coding agents

> Served at https://pay.lubaak.com/agents.md. Copy into AGENTS.md, CLAUDE.md,
> or a Cursor rule so your agent can integrate without reading the whole docs site.

**Alad** (`pay.lubaak.com`) does two things:

1. **Verify** - Ethiopian bank/wallet receipts at the source. You send a receipt
   URL or a reference; we fetch from the institution and return one normalized
   JSON envelope across ~25 banks and wallets.
2. **Checkout** - merchants create timed payment sessions; customers pay into
   the merchant’s settlement accounts and confirm with a receipt reference.
   Paid only after a live match on amount + settlement account.

It is **not** a card gateway. Customers transfer via Telebirr, CBE, BOA, etc.

Use verify when you need the bank’s receipt, not a number a user typed.
Use checkout when you want hosted (or API-driven) collection into your own
accounts, with a webhook when the receipt clears.

## Base URL and auth

Base URL: `https://pay.lubaak.com`

Mint API keys in the dashboard: https://pay.lubaak.com/dashboard/tokens  
Send: `Authorization: Bearer <token>`

Never put the key in a browser. It is a server-side credential.
Anonymous `/api/v*/verify` and the public `/verify` UI exist for demos
(IP-limited ~10/hour). Production should always use a key.

```bash
curl -X POST https://pay.lubaak.com/api/v2/verify \
  -H "Authorization: Bearer $ALAD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://transactioninfo.ethiotelecom.et/receipt/ABCD1234EF"}'
```

---

## Verify - POST /api/v2/verify (preferred)

Also `GET` with the same query params.  
`/api/v1/verify` still works; v1 uses `suffix` where v2 uses `account_number`.

Provide **exactly one of** `url` or `reference` (+ `bank` when the bank cannot
be inferred from the URL host):

| Field | Type | Notes |
|---|---|---|
| `url` | string | Full receipt URL from SMS/app. Bank auto-detected from host when possible. Prefer this. |
| `reference` | string | Transaction / FT / receipt id. Usually needs `bank`. |
| `bank` | string | Institution code (see below). |
| `account_number` | string | **Sender’s** account when the bank needs it (CBE FT refs, BOA). Not your settlement account. |
| `phone` | string | Required for CBE Birr (`cbebirr`) when verifying by reference. |
| `settlement_account` | string | Optional. **Your** account/phone - checks funds landed with you (`settlement_match`). |
| `expected_amount` | number | Optional. Adds `amount_match` against `sent_amount` / `total_paid`. |

### Success shape

Same envelope for every institution. Switch on `bank` if you must; do not invent
per-provider field trees - amounts and party fields live under `data`.

```json
{
  "valid": true,
  "status": "verified",
  "bank": "telebirr",
  "logo_url": "https://pay.lubaak.com/images/banks/telebirr.svg",
  "cached": false,
  "data": {
    "sender_name": "...",
    "sender_account": "...",
    "receiver_name": "...",
    "receiver_account": "...",
    "sent_amount": "100 ETB",
    "service_charge": "0 ETB",
    "tax_vat": "0 ETB",
    "total_paid": "100 ETB",
    "transfer_date": "...",
    "transaction_id": "..."
  },
  "confirmation": {
    "confirmed_count": 1,
    "first_confirmed_at": "...",
    "last_confirmed_at": "..."
  },
  "settlement_match": {
    "matched": true,
    "match_type": "exact",
    "match_confidence": "high"
  },
  "amount_match": {
    "matched": true,
    "parsed_amount": 100.0,
    "expected_amount": 100.0
  }
}
```

- `status`: `verified` | `invalid` | `pending`
- `cached: true` → prior success reused (no credit charge for authenticated callers)
- `settlement_match` only when you passed `settlement_account` and the receipt is valid
- `amount_match` only when you passed `expected_amount`
- `record_id` may appear for authenticated callers when a `Transaction` was persisted
- Amounts in `data` are strings like `"100 ETB"` - parse before arithmetic; use
  `amount_match` when you already know the expected number

Header on responses: `X-Verify-Cache: HIT|MISS`. Pending / backoff paths often
include `Retry-After`.

### By-reference examples

```bash
# Telebirr reference
curl -X POST https://pay.lubaak.com/api/v2/verify \
  -H "Authorization: Bearer $ALAD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"bank":"telebirr","reference":"TB7G9K2P"}'

# CBE FT - sender account_number required when the FT is not FT…+8digits
curl -X POST https://pay.lubaak.com/api/v2/verify \
  -H "Authorization: Bearer $ALAD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"bank":"cbe","reference":"FT25252XXXXXX","account_number":"1000123456789"}'

# CBE Birr - phone required with reference
curl -X POST https://pay.lubaak.com/api/v2/verify \
  -H "Authorization: Bearer $ALAD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"bank":"cbebirr","reference":"…","phone":"0911234567"}'
```

### Provider codes (`bank`)

| Code | Notes |
|---|---|
| `cbe` | CBE mobile / PDF. Aliases: `cbemobile` |
| `cbebirr` | CBE Birr wallet - needs `phone` with reference |
| `telebirr` | Telebirr |
| `boa` | Bank of Abyssinia - often needs sender `account_number` |
| `mpesa` | M-Pesa Ethiopia |
| `dashen` `awash` `zemen` `wegagen` | Bank channels |
| `nib` `siinqee` `abay` `amhara` `berhan` `hibret` `oromia` `ahadu` `zamzam` | More bank-native |
| `kaafiebirr` `ahaduebirr` `coopebirr` `nibebirr` `wegagenebirr` `siinqeeebirr` `ebirr` | eBirr tenants (`receipt.ebirr.com/...`). Aliases: `coop`/`coopay`→`coopebirr`, `kaafi`→`kaafiebirr` |

Full list + playground: https://pay.lubaak.com/docs  
Live outages: https://pay.lubaak.com/status

### Credits and limits

- Authenticated: first successful **uncached** verify may debit credits (plan-dependent; default 2). **Repeats of the same payment stay free** (`cached: true`).
- Invalid / pending / failed lookups do not debit.
- Anonymous: ~10 verifies / hour / IP. Authenticated rate limits follow the plan.
- Free-plan callers can still verify when balance is zero (plan rules apply).

### Verify errors

| HTTP | Meaning |
|---|---|
| 200 + `valid: false` | Upstream reachable; receipt not found / invalid. Body may include `error`. |
| 200 + `status: pending` | Upstream busy / backoff - respect `Retry-After`. |
| 402 | Insufficient credits (authenticated, live miss) |
| 422 | Bad input / bank could not be resolved - `{ "valid": false, "error": "…" }` |
| 429 | Rate or plan limit, or Telebirr upstream cooldown |
| 502 | Parse / upstream failure |

Treat a stretch of 502s or pending responses as a real outage rather than
retrying forever. Check https://pay.lubaak.com/status.

---

## Checkout - POST /api/v1/checkout/session

How *your customers* pay *you*. (Dashboard → Billing is how *you* buy credits
from Alad - different product surface.)

Sanctum required. Aliases: `POST /api/v1/orders`, `POST /api/v1/checkouts`.  
Throttle: 60/min. Snake_case preferred; camelCase aliases accepted.

### Required browser redirects

| Field | Role |
|---|---|
| `success_url` | Customer redirect after **paid** |
| `failure_url` | Fail / expire (`fail_url` / `error_url` also accepted). Cancel uses `cancel_url` if set, else `failure_url`. |

### Optional webhook (exactly one URL)

| Field | Role |
|---|---|
| `callback_url` | Server webhook - we **POST JSON** on `paid`, `failed`, `cancelled`, and `expired`. Alias: `notify_url`. **Arrays rejected.** |

### Amount

Supply `amount` / `amount_etb` / `totalAmount`, **or** `items[]` with `name`,
`price`, optional `quantity`.

### Settlement (where the customer must pay)

| Field | Notes |
|---|---|
| *(default)* | All **saved accounts** under Dashboard → Accounts |
| `saved_account_ids` | Subset of saved account ids |
| `settlement_accounts` | Inline for this order (`payment_accounts` / `accounts` also accepted) |

Each inline account needs `bank` (or `provider`) + `account_number` (or `account`).  
Optional: `account_name`, `name`, `label`.

**Checkout settlement methods:** same codes as verify - every supported bank and
wallet (`cbe`, `telebirr`, `boa`, `mpesa`, `dashen`, …). Customers pay into your
settlement account for that method; we verify the receipt against amount + account.

### Other optional fields

`name` / `phone` / `email` (customer), `nonce` (unique), `expires_minutes`
(1-60, default 5), `payment_methods`, `metadata`, `cancel_url`, `lang`.

### Example

```bash
curl -X POST https://pay.lubaak.com/api/v1/checkout/session \
  -H "Authorization: Bearer $ALAD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "success_url": "https://yoursite.com/thanks",
    "failure_url": "https://yoursite.com/failed",
    "callback_url": "https://yoursite.com/webhooks/alad",
    "name": "Customer",
    "phone": "0911234567",
    "email": "buyer@example.com",
    "nonce": "unique-id-123",
    "expires_minutes": 5,
    "settlement_accounts": [
      {"bank": "telebirr", "account_number": "0951410010", "account_name": "Shop Telebirr"},
      {"bank": "boa", "account_number": "146105677", "account_name": "Shop BOA"}
    ],
    "items": [{"name": "Order", "quantity": 1, "price": 5}]
  }'
```

### Create response

```json
{
  "error": false,
  "msg": "No Errors",
  "success": true,
  "data": {
    "sessionId": "LK7K2M9Q4P",
    "paymentUrl": "https://pay.lubaak.com/pay/LK7K2M9Q4P",
    "cancelUrl": "https://pay.lubaak.com/pay/LK7K2M9Q4P/cancel",
    "totalAmount": 5,
    "currency": "ETB",
    "expires_at": "...",
    "token": "<public_token>"
  },
  "order_id": "LK7K2M9Q4P",
  "pay_url": "https://pay.lubaak.com/pay/LK7K2M9Q4P"
}
```

- Send customers to `data.paymentUrl` (hosted checkout at `/pay/{order_id}`).
- On **paid**: browser → `success_url` (query: `sessionId`, `order_id`,
  `order_number`, `nonce`, `status=paid`, `amount`, `currency`); server POST →
  `callback_url` with `status=paid`.
- On **fail / expire / cancel**: browser → `failure_url` or `cancel_url`
  (`status` = `failed` | `expired` | `cancelled`); server also POSTs the same
  status to `callback_url`.

### Webhook body (`callback_url` POST)

Fired for **`paid`**, **`failed`**, **`cancelled`**, and **`expired`** (queued,
retries on non-2xx). Same shape every time; read `status`.

```json
{
  "sessionId": "LK7K2M9Q4P",
  "order_id": "LK7K2M9Q4P",
  "order_number": "LK7K2M9Q4P",
  "nonce": "...",
  "status": "paid",
  "amount": 5,
  "totalAmount": 5,
  "currency": "ETB",
  "payment_method": "telebirr",
  "payment_reference": "...",
  "failure_reason": null,
  "paid_at": "...",
  "expires_at": "...",
  "customer": { "name": "...", "phone": "...", "email": "..." },
  "items": [],
  "metadata": {}
}
```

| `status` | Meaning |
|---|---|
| `paid` | Live receipt matched amount + settlement account |
| `failed` | Verify attempt failed (`failure_reason` set). Session stays open for retry until expire |
| `cancelled` | Customer or API cancelled the session |
| `expired` | Past `expires_at` without a successful payment |

Treat `paid` / `cancelled` / `expired` as terminal for fulfillment. `failed` is
an attempt signal - wait for a later `paid` or terminal status.

### Optional: drive checkout without the hosted UI

Use the session `token` or `sessionId` as Bearer (**not** the merchant API key):

| Method | Path | Purpose |
|---|---|---|
| POST | `/api/v1/form/payment-system-info` | Session + payment methods |
| POST | `/api/v1/form/create-deposit` | Select method (`payment_system`) |
| POST | `/api/v1/form/deposit-activate` | Confirm: `{ "reference", "account_number?" }` |
| GET | `/api/v1/form/deposit/status` | `Pending` / `Success` / `Failed` |

Most integrations only need `paymentUrl` + webhook.

### Cancel

`POST /api/v1/checkout/session/{token}/cancel` (merchant key or session token).

---

## Saved accounts

`GET|POST|PUT|DELETE /api/v1/accounts` (Sanctum). Merchant settlement address
book used by checkout when no inline `settlement_accounts` are sent.

---

## Health

Both are open, no key needed:

- `GET /up` - process alive.
- `GET /api/v1/status` - per-provider JSON for every upstream bank/wallet.
  Status values: `operational` | `slow` | `down`. Returns `503` when every
  upstream is `down`. Partial outages show as overall `slow`.
- `GET /status` - human HTML status page.

---

## Rules that are easy to get wrong

1. Prefer receipt `url` when the customer has a link - bank detection is more reliable.
2. Screenshots are not proof. Always verify with URL or reference.
3. Verify `account_number` = **sender**. Checkout settlement / `settlement_account` / webhook check = **your** account. Do not swap them.
4. v2 rejects `suffix` - use `account_number`. v1 still uses `suffix`.
5. CBE FT by short reference needs sender `account_number`. CBE Birr by reference needs `phone`.
6. eBirr wallet receipts (`receipt.ebirr.com/{tenant}/...`) use `*ebirr` codes (`wegagenebirr` ≠ `wegagen`).
7. Checkout settlement uses the **same bank codes as verify**. CBE/BOA confirm
   may ask the payer for their sender `account_number`; CBE Birr needs `phone`.
8. Cache hits on verify are free for authenticated callers. Failed lookups do not create a durable paid receipt.
9. `callback_url` must be one string, not an array. Conflicting alias pairs are rejected.
10. Form/checkout Bearer for `/api/v1/form/*` is the **session token / order id**, not the merchant API key.
11. Amount fields in `data` are strings. Never feed them straight into arithmetic - use `amount_match` or parse.
12. Treat receipt URLs like credentials: do not log them into public error streams or bug reports.

---

## Full docs

- Human docs + playground: https://pay.lubaak.com/docs
- Agent index: https://pay.lubaak.com/llms.txt
- Try verify without a key: https://pay.lubaak.com/verify
- Status: https://pay.lubaak.com/status
- Mint keys: https://pay.lubaak.com/dashboard/tokens
