# API Documentation — A&B Hub

Two distinct APIs live in this codebase, authenticated differently —
don't confuse them.

## 1. Internal API (`routes/web.php`, `routes/api.php`)

Every page's AJAX calls (creating a lead, sending a campaign, etc.)
use this — session-cookie authenticated, same-origin, CSRF-protected.
Not intended for third-party/external use. Response envelope, every
endpoint:

```json
{ "success": true, "message": "Lead created.", "data": { "...": "..." } }
{ "success": false, "message": "Validation failed.", "errors": { "email": ["..."] } }
```

Send `X-Requested-With: XMLHttpRequest` and `X-CSRF-Token: <token>`
(read from the `<meta name="csrf-token">` tag server-rendered on every
page) on every state-changing request. See `public/assets/js/
app-data.js`'s `abFetch()` for the reference implementation.

## 2. External API (`/api/v1/external/*`)

For third-party integrations (Zapier, custom scripts, another
system). Authenticated by API key, **not** the session cookie —
generate a key from Admin > API Keys, then:

```
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

No CSRF token needed on this API (CSRF is a cookie-based attack;
Bearer-token auth isn't vulnerable to it the same way).

### Endpoints

| Method | Path | Scope required | Description |
|---|---|---|---|
| GET | `/api/v1/external/leads` | `leads.read` | Paginated list. Query params: `page`, `per_page`, `business_line_id` |
| GET | `/api/v1/external/leads/{id}` | `leads.read` | Single lead |
| POST | `/api/v1/external/leads` | `leads.write` | Create a lead. Body: `first_name` (required), `last_name`, `email`, `phone`, `business_line_id` (required), `lead_stage_id` (required) |
| GET | `/api/v1/external/customers` | `customers.read` | Paginated list |
| GET | `/api/v1/external/customers/{id}` | `customers.read` | Single customer |

A key created with no scopes selected has full access to every
endpoint above. A key scoped to only `leads.read` gets a `403` on
anything else — verified live during development, including the
revocation case (a revoked key immediately gets `401` on its next
call, not just on new key generation).

### Error responses

| Status | Meaning |
|---|---|
| 401 | Missing/malformed `Authorization` header, or the key is invalid/revoked |
| 403 | The key is valid but lacks the required scope |
| 422 | Validation failed (see `errors` in the response body) |
| 404 | Record not found |

### Adding a new external endpoint

Add a method to `App\Controllers\ExternalApiController`, register the
route in `routes/api.php` with `middleware: ['api_key']`, and check
the required scope via
`(new ApiKeyModel())->hasScope($request->getAttribute('_api_key'), 'your.scope')`
at the top of the method — see any existing method in that controller
for the exact pattern.

## 3. Inbound Webhooks (public, no session, signature/token verified)

| Provider | Path | Verification |
|---|---|---|
| Twilio (voice) | `POST /voice/incoming` | `X-Twilio-Signature` header, HMAC-SHA1 per Twilio's documented algorithm (`TwilioClient::verifyWebhookSignature()`, verified against Twilio's own published test vector) |
| Twilio (voice) | `POST /voice/gather` | Same |
| Twilio (voice) | `POST /voice/status` | Same |
| Meta (Facebook/Instagram Lead Ads) | `GET /webhooks/meta` | Subscription handshake — echoes `hub.challenge` if `hub.verify_token` matches the configured token |
| Meta (Facebook/Instagram Lead Ads) | `POST /webhooks/meta` | Payload processed by `MetaIntegrationService`; every event is logged to `webhook_events` before processing, so a malformed payload is debuggable from the database, not just a provider dashboard |

To point Twilio at this app: buy a number in the Twilio console, set
its "A call comes in" webhook to `https://your-domain/voice/incoming`
(HTTP POST). To point Meta at this app: configure a Webhooks
subscription in the Meta App dashboard for the `leadgen` field,
pointing at `https://your-domain/webhooks/meta`, using the same
verify token entered in Admin > Integrations > Facebook.

## Rate limiting

Public, unauthenticated endpoints (`/widget/chatbot/*`) are
rate-limited via the `throttle:strict` middleware — see
`config/security.php` for the exact limits. Session-authenticated and
API-key-authenticated requests are not separately rate-limited beyond
normal login brute-force protection, since they're already
credentialed.
