# REST API – `/api/v1`

Envelope: `{"success":true,"data":…}` or `{"success":false,"error":{"code":"…","message":"…"}}`. Always HTTPS.

**Auth** – plugin endpoints use a per-website key: `Authorization: Bearer <key_id>.<secret>`. Only a SHA-256 of the secret is stored. The tenant (client, website) is derived from the key; any `client_id`/`website_id` in a body is ignored.
**Idempotency** – send `Idempotency-Key: <≤80 chars>` on POSTs; the stored response is replayed (`Idempotent-Replay: true`).
**Rate limits** – public 20/min/IP, authenticated 120/min/key, webhooks 600/min/IP.

| Method & path | Auth | Notes |
|---|---|---|
| POST `/client/register` | public | `name, business_name, email, phone, password(≥10)`. Free credits are granted on **email verification**, once. |
| GET `/client/me` | key | business, website status, wallet summary. (`POST /client/login` is intentionally not provided: no API-based login is needed.) |
| POST `/websites/connect` | pairing code | `pairing_code, site_url, plugin_version?, wp_version?, wc_version?` → `api_key`, `callback_secret`, `verification_token` (shown once). 403 `approval_required` when the website slot is used. |
| POST `/websites/verify` | key (pending ok) | server fetches `/wp-json/voiceorder/v1/ownership`, activates on match. |
| GET `/websites` · POST `/websites/disconnect` | key | |
| POST `/websites/additional-request` | pairing code | `pairing_code, site_url, note?` → request id/status. |
| GET `/wallet` · GET `/wallet/transactions` · POST `/wallet/recharge-request` | key | `price_per_call` is returned so clients can show the price before manual calls. |
| POST `/calls` | key | manual call: `wc_order_id, phone, customer_name?, language?`. |
| GET `/calls` · GET `/calls/{id}` | key | own website's calls only (404 otherwise). `{id}` is the public call id. |
| POST `/calls/{id}/retry` | key | creates a **new** call (new reservation/charge). |
| POST `/orders/events` | key | automatic call for an order. Duplicate → 200 `duplicate:true`. 402 `insufficient_balance`, 422 `invalid_phone`, 503 `no_provider`. |
| POST `/orders/{id}/status` | key | `wc_status`; cancelled/refunded/failed drops queued calls and releases the reservation. |
| GET/PUT `/settings` | key | voice settings, validated against admin limits. |
| GET `/plugin/update-check?version=` | key | returns short-lived signed `download_url` + `sha256`. |
| GET `/plugin/download/{release}/{site}` | signed URL | published releases only. |
| POST `/webhooks/voice/{provider-slug}` | HMAC | provider callbacks, verified by the provider's own adapter. |
| POST `/webhooks/payment/{gateway}` | gateway | server-to-server callbacks only; amount must equal the recharge request. |

## SaaS → plugin push
`POST {site}/wp-json/voiceorder/v1/call-result` (falls back to `/?rest_route=`), body `{event,call_id,wc_order_id,result,call_status,attempts,charge,funding}`;
headers `X-VoiceOrder-Timestamp`, `X-VoiceOrder-Nonce`, `X-VoiceOrder-Signature: sha256=hmac_sha256(callback_secret, "<ts>.<nonce>.<body>")`. `result ∈ confirmed|cancelled|no_answer|busy|failed|rejected|completed|…`.
The plugin enforces a 5-minute window and one-time nonces.
