# MenuMatic Provisioning API v1 Lets your CRM create, update, suspend, resume, delete and purge food outlet accounts. Base URL: `https://YOUR-DOMAIN//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 `. Fallback if your host strips that header: `X-Api-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":""}` | | 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.