# MenuMatic Provisioning API v1

Lets your CRM create, update, suspend, resume, delete and purge food outlet accounts.

Base URL: `https://YOUR-DOMAIN/<app_folder>/api/provision_api.php/v1` (or `/v1` if you add the rewrite below).
All requests and responses are JSON, UTF-8. All times are UTC (ISO 8601, `Z`).

## 1. Get a key
Run on the server (CLI only):
```
php create_api_client.php create "Company CRM" --scopes=outlets:read,outlets:create,outlets:update,outlets:suspend,outlets:delete --ips=203.0.113.10 --rate=60 --expires=2027-12-31
php create_api_client.php list
php create_api_client.php revoke 3
```
The key `mmk_<16hex>.<48hex>` is printed once. Only its hash is stored. Give it to the CRM as a secret, never in client-side code.
Add `outlets:purge` only if the CRM must permanently erase accounts.

Send it as `Authorization: Bearer <key>`. Fallback if your host strips that header: `X-Api-Key: <key>`.
Apache/shared hosting, in `.htaccess`:
```
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
RewriteRule ^v1/(.*)$ provision_api.php/v1/$1 [L]
```

## 2. Endpoints
| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | /v1/ping | read | Check key and connectivity |
| GET | /v1/plans | read | Plan codes with limits (categories, items, storage, admin users, orders per month) and features (promo codes, loyalty) |
| GET | /v1/outlets | read | List (filters: `status`, `crm_ref`, `include_deleted=1`, `page`, `per_page` max 100) |
| POST | /v1/outlets | create | Create an outlet (+ optional owner invite) |
| GET | /v1/outlets/{id} | read | Fetch one |
| PATCH | /v1/outlets/{id} | update | Change profile, theme, settings, hours |
| POST | /v1/outlets/{id}/suspend | suspend | Take the menu offline. Body `{"reason":"..."}` optional |
| POST | /v1/outlets/{id}/resume | suspend | Bring it back. Body `{"status":"active"}` optional |
| DELETE | /v1/outlets/{id} | delete | Soft delete (reversible) |
| POST | /v1/outlets/{id}/restore | delete | Undo delete. Stays suspended until resumed |
| POST | /v1/outlets/{id}/purge | purge | Permanent erase, only after the retention period (default 30 days). Body `{"confirm_slug":"<slug>"}` |
| POST | /v1/outlets/{id}/owner-invite | update | Issue a fresh owner invite link |

A key only sees outlets it created. Anything else returns 404.

## 3. Create an outlet
```
curl -X POST https://YOUR-DOMAIN/.../v1/outlets \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: crm-cust-1042-create" \
  -d '{
    "crm_ref": "CUST-1042",
    "name": "Burger Barn",
    "whatsapp_number": "+971 50 123 4567",
    "primary_color": "#5b2a86",
    "currency_code": "AED",
    "tax_pct": 5,
    "status": "trial", "trial_days": 14,
    "settings": {"delivery_fee": 10, "pickup_enabled": true, "min_order": 30},
    "hours": [{"weekday": 1, "opens": "09:00", "closes": "22:00"}],
    "owner": {"name": "Sam", "email": "sam@example.com"}
  }'
```
Required: `crm_ref`, `name`, `whatsapp_number`. Everything else has a default.
`crm_ref` is your CRM customer id (letters, digits `. _ : -`, max 64), unique per key, and cannot be changed later.
`plan` is a code from `GET /v1/plans` (default `basic`). An unknown or retired plan returns 422. Change it later with PATCH to upgrade or downgrade. Plan limits are enforced by the outlet's admin panel, not by this API.
`slug` is generated from the name (made unique automatically) unless you send your own.
`hours.weekday`: Sunday = 0.

Response `201`:
```
{"data": {"id": 17, "crm_ref": "CUST-1042", "slug": "burger-barn", "status": "trial",
  "menu_url": "https://.../landing_new.php?m=burger-barn", "settings": {...}, "hours": [...],
  "owner_invite": {"email": "sam@example.com", "url": "https://.../admin/accept_invite.php?token=...", "expires_at": "..."}}}
```
Store `id` against the customer in your CRM. **The invite `url` is shown once.** Email it to the owner so they set their own password; no passwords travel through the API. Lost it? Call `owner-invite` again (valid 7 days).

## 4. Retries and idempotency
Send `Idempotency-Key` (8-128 chars) on every POST. Repeating the same key and body returns the original response with `Idempotent-Replay: true` and does nothing twice (the invite url is blanked in replays). Same key with a different body gives `422 idempotency_key_reuse`. Keys are kept 24 hours. A network timeout on create is therefore safe to retry. If you do not use a key, a repeated create returns `409 already_exists` because of `crm_ref`.

## 5. Typical CRM lifecycle
- Customer signs up: POST /v1/outlets
- Invoice unpaid: POST /{id}/suspend; paid: POST /{id}/resume
- Customer cancels: DELETE /{id}. Changed their mind within 30 days: POST /{id}/restore, then /resume
- GDPR or final cleanup: POST /{id}/purge (needs `outlets:purge`)

## 6. Errors
Every error: `{"error":{"code":"...","message":"...","fields":{...}},"request_id":"..."}`. Quote `request_id` to support (also in the `X-Request-Id` header).

| HTTP | code | Meaning |
|---|---|---|
| 400 | https_required, invalid_json, invalid_idempotency_key | Transport or body problem |
| 401 | invalid_api_key | Missing, wrong, revoked or expired key |
| 403 | insufficient_scope, ip_not_allowed | Key not permitted |
| 404 | not_found | No such outlet (or it belongs to another key) |
| 405 / 413 / 415 | method_not_allowed, payload_too_large (64 KB), unsupported_media_type | |
| 409 | already_exists, slug_taken, email_in_use, outlet_deleted, not_eligible, request_in_progress | State conflict |
| 422 | validation_failed, idempotency_key_reuse | `fields` lists each bad field. Unknown fields are rejected |
| 429 | rate_limited | Honour `Retry-After` |
| 500 | server_error | Retry later with the same Idempotency-Key |

## 7. Security notes
- HTTPS only. Keys are random 192-bit secrets, stored as SHA-256, compared in constant time.
- Restrict each key by IP and by scope. Use one key per integration so one can be revoked alone. Rotate by creating a new key, switching the CRM, then revoking the old.
- Revoking a key never takes its outlets offline.
- Every call is logged in `api_audit_log` (who, route, outlet, status, IP; never bodies or secrets).
- Purge removes the database rows (cascade) and the outlet's upload folder `uploads/o{id}`.

## 8. CRM integration checklist
1. Create a key; confirm `GET /v1/ping`.
2. Store `outlet.id`, `slug`, `menu_url` on the customer record.
3. Use `crm_ref` = your customer id and an Idempotency-Key like `crm-{customer}-{action}-{version}`.
4. Treat 5xx and timeouts as retryable, 4xx as not.
5. Expect `accept_invite.php` (owner sets password from the token) on the admin side; see "Still to build" in the hand-off note.
