# Reference adapters - custom platform (early access)

Two small, dependency-free examples of what a custom shop or ERP must do on its **server** to take a CutOptim Configurator order safely. They are reference code to read and copy, not a hosted service: CutOptim does not collect payment and does not run your shop.

| | Node (`node/`) | PHP (`php/`) |
|---|---|---|
| Verifier | `cutoptim-verify.mjs` (Node 18+, `node:crypto` only) | `CutOptimVerify.php` (PHP 7.4+, no Composer, no framework) |
| Endpoint example | `example-server.mjs` (`node:http`) | `example-webhook.php` |
| Self-test | `npm run test:cfg-reference-adapters` | `php php/selftest.php <fixture.json>` |

## What they do

1. **Quote token.**
   - `verifyQuoteTokenLocal`: structure, the eleven claims exactly, the payload equal to the canonical (RFC 8785) form of its own claims, expiry, your tenant id, and the HMAC-SHA256 when you pass the key.
   - `verifyQuoteTokenRemote`: `POST /cfg/v1/quote/verify` with your **secret** key (`Authorization: Bearer cfg_sk_...`, JSON body `{ "quoteToken": "..." }`). **This is the authoritative signature check**: the token key is derived on the CutOptim side and is not handed to merchants, so a local check without a key says "well formed, yours, not expired", never "signed by CutOptim". Treat anything but a 200 with `valid: true` as invalid.
2. **Webhook.** Header `CutOptim-Signature: t=<unix s>,v1=<hex>[,v1=<hex>]`, MAC = HMAC-SHA256(secret, `"<t>.<raw body>"`) over the **raw bytes**; constant-time compare (`timingSafeEqual` / `hash_equals`); a 300 s window either way; two `v1=` values while a secret is being rotated; **idempotency on the event id** (`CutOptim-Event-Id`, equal to the body `id`; a retry repeats it, `CutOptim-Delivery-Id` differs). A duplicate answers 200, a handler failure answers 500 and releases the claim so the retry is processed.

## Secrets

All secrets in this folder are placeholders (`cfg_sk_REPLACE_ME`, `whsec_REPLACE_ME`). Real ones live in your environment, never in a page, a repository or a log. The webhook secret comes from the admin route (`POST /cfg/v1/admin/webhooks/<id>/secret`).

## Proof

`npm run test:cfg-reference-adapters` signs samples with the real repository signers and runs the Node example (valid accepted; one changed byte, wrong secret, stale or future timestamp refused; replay is a duplicate), writes the same cases as a fixture and runs the PHP example on them when `php` is installed. The box CI container has no PHP, so `.github/workflows/cfg-reference-adapters.yml` runs `php -l` and `selftest.php` on GitHub. Guide: dev-docs `23_configurator/custom-platform`.
