Module ↔ API protocol
Boundary and status
SD Module REST 1.0 · Current development contract · 1 October 2026. This is the first version of the module/API REST protocol. Earlier Bridge RPC documents were exploratory drafts and impose no compatibility or migration requirements. The endpoints described here still need implementation; outstanding engineering details are listed below.
The module is a platform adapter. It has no LLM, agent harness or MCP server. Agentfy calls Store Deputy MCP; Store Deputy verifies authority, translates tools into this protocol and calls the module. The module validates the connection and local permissions, applies platform rules, and returns evidence. The shop remains the source of truth.
The harness runs the agent. The model proposes business arguments, not credentials or permissions.
catalog.setBasePriceThe receipt travels back to the chat. The shop owns the data.
Connection lifecycle
An authorized administrator starts a single-use pairing challenge. The browser carries a reference and CSRF state, never store secrets.
pendingPairing and agent creation are separate stages. A provisioning retry must not create a duplicate.
- An administrator with OpenCart
modifypermission starts Connect. The module creates a single-use, expiring pairing challenge; the browser carries only the pairing reference and CSRF state. - Hosted login/signup selects the team and explicitly confirms the shop. The service verifies membership, ownership proof, exact shop endpoint and allowlisted return URL. Credentials never travel in redirect query strings.
POST /pairingsis the module-side bootstrap resource under the REST/v1base. A module-trusted backend key authenticates this server-to-server exchange before connection credentials exist. The module validates and atomically consumes the one-time pairing challenge, binds the confirmed connection/store, and provisions connection credentials. Derive separate directional signing keys with an agreed KDF; bootstrap request/response schemas, signing/KDF vectors and key IDs must be specified before rollout. This bootstrap exception must not authorize business operations.- The API calls the authenticated REST manifest, verifies connection identity and supported protocol/capabilities, then provisions the agent in Agentfy with the central MCP endpoint. Pairing success and agent provisioning success are separate states. Retrying provisioning must reuse the same connection identity, never create duplicate agents.
- Local Pause blocks business reads/writes immediately; signed manifest, operation-status and revocation calls remain available. Revocation rejects old credentials, cancels pending dispatch and invalidates shopper bindings. Already applied writes are not undone. Rotation uses explicit key IDs and a bounded overlap; a revoked key is never accepted during overlap.
Persist pending → paired → ready, plus paused, revoked, incompatible, provisioningFailed. A failed manifest check cannot become ready. The user sees a recoverable provisioning failure without repeating pairing.
Transport and authentication
HTTPS with verified certificates is required outside isolated local fixtures. JSON is UTF-8, uncompressed for this profile; responses use Cache-Control: no-store. Logical routes below are relative to the registered module base URL, e.g. https://shop.example/storedeputy.php/v1. PATH_INFO/front-controller routing must pass hosting tests; no fallback to an unrestricted OpenCart admin router. Redirects are rejected. DNS and destination validation must prevent SSRF, including private/loopback/link-local targets, rebinding and redirected destinations.
Proposed authentication profile: RFC 9421 HTTP Message Signatures, hmac-sha256, per-connection directional keys; RFC 9530 Content-Digest with SHA-256 over the exact transmitted bytes (including empty content). Cover @method, @target-uri, content-digest, x-sd-protocol, x-sd-connection, x-sd-store, x-request-id. For mutations also cover idempotency-key. Require signature parameters keyid, created, expires, nonce; key ID fixes the allowed algorithm and direction, never trust an arbitrary supplied algorithm.
Proposed bounds: expiry no more than 60 seconds after creation, clock tolerance 30 seconds; reject future/expired signatures outside tolerance. Atomically reserve each nonce per key/direction until expiry + tolerance. Every retry gets a new signature/nonce/request ID but the same operation key and payload. Repeated headers, duplicate query keys and unsupported encodings are rejected before execution. The reverse proxy must preserve the registered public target URI, with explicitly trusted proxy configuration.
Responses are signed too: cover status, digest, connection, store, request ID and the originating request method/target using RFC 9421 request-bound components. Verify correlation, expiry, digest, signature and JSON schema before trusting a receipt. Missing/invalid signatures or HTML gateway errors mean unknown outcome, not a successful write. Golden request/response signing vectors in PHP and Node are a release gate; these rules are not implemented by existing experimental code.
Discovery and capabilities
GET /manifest is authenticated. It returns connection/store identity, protocol/module/platform versions, enabled capabilities, limits, pause state, currency/price semantics and compatibility warnings. The example is a future capable module, not today's manifest. A declared capability is an implementation promise backed by conformance tests, not a roadmap item.
Effective access is the intersection of implemented capability, local owner permissions, user/team role, agent scope and plan entitlement. The API can restrict access but cannot enable a capability absent from the module. Revalidate manifest after update/reconnect; every write still checks current local permissions. OpenCart multi-store data can be shared: a base-price change may affect more than the selected storefront. The module reports all affected stores and rejects the write unless authority and approval cover them.
{
"data": {
"protocol": "1.0", "connectionId": "conn_demo", "storeId": "0",
"moduleVersion": "1.0.0", "platform": { "name": "opencart", "version": "3.0.3.5" },
"paused": false,
"capabilities": ["catalog.products.read@1", "catalog.basePrice.write@1", "operations.read@1"],
"limits": { "maxPageSize": 100, "maxBodyBytes": 262144, "maxExecutionMs": 10000 },
"price": { "currency": "USD", "scale": 4, "taxIncluded": false, "scope": "sharedProduct" },
"warnings": []
},
"meta": { "requestId": "req_demo", "protocol": "1.0" }
}Resources and MCP mapping
Paths are relative to the module /v1 base. Store selection is the signed X-SD-Store, restricted by the connection. GET never modifies commerce data. There is no generic SQL, PHP, admin-route or arbitrary HTTP execution tool.
Stage A read contract: list parameters are cursor (opaque, optional), limit (1–100, default 50), sku (exact optional filter), language (advertised locale). Stable order is product ID; return {data: Product[], page:{nextCursor:null|string}, meta}. A cursor is scoped to connection/store/filter; it is not a snapshot promise. IDs are opaque strings; decimal money is a string with advertised scale; currency is an ISO code. Product reads return id, sku, name, basePrice, currency, revision, affectedStoreIds, and warnings about specials/options. SKU may match multiple products: never select the first silently. Deleted/missing detail returns 404. A strong base-price revision must reflect external admin changes, not just module writes; if that cannot be proven, disable writes.
Product creation, inventory and review replies are extension capabilities. Their own schemas, permissions, concurrency and verification tests must exist before they are exposed. The first implementation does not imply every admin action is available.
| MCP tool / purpose | REST resource | Capability / stage |
|---|---|---|
| Adapter discovery | GET /manifest | Required / A |
catalog.listProducts | GET /products | catalog.products.read@1 / A |
catalog.getProduct | GET /products/{id} | catalog.products.read@1 / A |
catalog.getBasePrice | GET /products/{id}/base-price | catalog.products.read@1 / A |
Approved catalog.setBasePrice | POST /operations (kind=product.basePrice.set) | catalog.basePrice.write@1 / controlled write |
operation.get | GET /operations/{operationId} | operations.read@1 / A |
cart.get | GET /shopper-bindings/{bindingId}/cart | cart.read@1 / B |
Authorized cart.setItemQuantity | POST /operations (kind=cart.item.quantity.set) | cart.write@1 / B, schema pending |
GET /products/{id}/base-price → 200, ETag: "price-r17":
{"data":{"productId":"42","amount":"19.9900","currency":"USD","revision":"price-r17","affectedStoreIds":["0"]},"meta":{"requestId":"req_demo","protocol":"1.0"}}Controlled writes
The owner reviews product 42, its current revision, affected stores and the proposed base price. A different change needs a new approval.
19.9900 → 21.5000 USDOne approved intent → one operation ID, preserved across retries.
The API resolves connectionId, store, actor, run and approval from authenticated state outside the prompt. The model supplies business arguments, never a credential, role, shopper identity or approval flag. For a protected write, show the owner exact resource IDs, before/after values, affected stores and warnings; store immutable approval tied to that action and its revision. A change to action data requires new approval. Validate team membership, entitlement, revocation and approval expiry again immediately before dispatch.
POST /operations creates one operation for one resource, not a hidden batch transaction. The API chooses an operation UUID once per approved intent and uses it as Idempotency-Key. Body operationId must match. The signed body includes the trusted actor and approval decision; the module trusts the paired API to authenticate the employee, but independently enforces connection/store scope, local permissions/Pause, exact approved action hash and approval expiry. This does not protect against a compromised trusted API; local scopes and Pause bound that trust.
actionHash is SHA-256 of RFC 8785 canonical JSON of {connectionId, storeId, kind, target, expected, set, affectedStoreIds}. Revision and affected stores are bound too. The module recomputes it and compares to the approval grant. The grant cannot contain broader wildcards. Reject unknown mutation fields, invalid decimals, unsupported currency, expired approval, scope mismatches and no-op changes before starting a write. expected.revision is a domain precondition on the target price, not If-Match on the operation collection; a mismatch returns 409 REVISION_CONFLICT with no write.
Module execution: atomic journal claim → validate current resource/precondition → conditional write preserving unrelated data → read back → required cache invalidation → persist receipt. Platform hooks and external side effects have separate status; do not claim they completed just because the database did. Do not overwrite full product forms or silently replace specials/discounts/options. No automatic rollback: reversal is a new approved operation against the current revision.
POST /operations · Idempotency-Key: 4faf6b8a-887e-4c97-93b0-a5476ae95a75
{
"operationId": "4faf6b8a-887e-4c97-93b0-a5476ae95a75",
"connectionId": "conn_demo", "storeId": "0",
"kind": "product.basePrice.set", "target": { "productId": "42" },
"expected": { "revision": "price-r17", "amount": "19.9900" },
"set": { "amount": "21.5000", "currency": "USD" },
"affectedStoreIds": ["0"],
"context": { "actorId": "user_demo", "actorKind": "employee", "runId": "run_demo" },
"approval": { "id": "apr_demo", "actionHash": "<sha256-hex>", "expiresAt": "2026-10-01T12:05:00Z" }
}201 Created · Location: https://shop.example/storedeputy.php/v1/operations/4faf6b8a-887e-4c97-93b0-a5476ae95a75
{
"data": {
"operationId": "4faf6b8a-887e-4c97-93b0-a5476ae95a75",
"state": "succeeded", "actionHash": "<sha256-hex>",
"target": { "productId": "42" },
"before": { "amount": "19.9900", "revision": "price-r17" },
"after": { "amount": "21.5000", "currency": "USD", "revision": "price-r18" },
"verifiedAt": "2026-10-01T12:00:01Z",
"effects": { "cache": "completed", "hooks": "notRequired" }, "warnings": []
},
"meta": { "requestId": "req_demo", "protocol": "1.0" }
}Retries, conflicts and unknown outcomes
GET /operations/{id}Read the journal before decidingA terminal record proves success, conflict or failure. Return its actual outcome; do not execute the operation again.
succeeded / conflict / failedDo not issue a new key or retry blindly. A matching price alone is not proof of our write.
The journal has an atomic unique key (connectionId, storeId, operationId) and an immutable hash of the full semantic operation body. The same key/body returns the stored result with fresh transport authentication; a different body returns 409 IDEMPOTENCY_MISMATCH. An in-flight duplicate returns 202 with the same operation URL and Retry-After, never launches another worker. Terminal replay returns 200. Validate connection authorization before returning even a cached result; an expired approval cannot start or resume new effects.
GET /operations/{id} returns {data: Operation, meta} where state is pending, running, succeeded, conflict, failed, or unknown. Successful operations include the receipt above; conflict/failed include a stable code and evidence of no applied effect; unknown includes known partial effects and a reconciliation reason. A paused module allows an authorized status read. Missing retained identity returns 404 OPERATION_NOT_FOUND; it is not proof that no historical effect occurred.
After a timeout, gateway 5xx, process death or missing signature, query status first. No new operation key and no blind retry. A retained tombstone prevents re-execution after receipt archival. Keep operation IDs/hashes indefinitely for REST 1.0; redact/archive detailed personal content separately. Loss of the journal or restore from backup invalidates its completeness: halt writes until reconciled. If a never-seen key is resubmitted, the original worker and retry must race through the same atomic claim.
currentPrice == requestedPrice alone does not prove our operation applied: an administrator may have made the same change. A stale running record does not authorize another writer. Reconcile using durable operation evidence; otherwise remain unknown and request investigation. No exactly-once promise across a price write, journal and extension hooks unless the platform implementation proves the crash boundary. Where the product table is non-transactional, capabilities stay read-only until a safe recovery method is demonstrated. Multi-product work is a set of independent operations with per-item results, never a promised all-or-nothing batch.
Shopper and cart extension
The widget calls the module on the shop origin. The module derives guest or customer identity; raw cookies stay in the shop.
same-origin bootstrapLogin, logout or cart merging requires binding refresh or revocation. Stage B extension.
Stage B has a separate protocol profile; do not enable it from an employee credential alone. The widget calls a same-origin module endpoint protected against CSRF. The module derives identity from the live storefront session and exchanges an authenticated attestation with Store Deputy. API returns an opaque binding and short-lived, conversation-scoped chat access. Raw session cookies and reusable admin credentials never leave the shop for the browser widget or Agentfy.
The binding is server-side and scoped to connection, store, session generation, customer/guest, conversation, permitted cart actions and expiry. Both API and module validate it on every call. The model cannot pick a binding or customer ID. Login/logout, expiry and guest-cart merging revoke/rebind it; another tab changing the cart invalidates the expected revision. The module uses the real OpenCart cart and platform totals, not a separate API-session cart.
Prefer desired quantity (set quantity to 3) over an unbounded additive retry. Cart mutations require an operation key, expected cart revision and explicit shopper intent bound to exact items/options; Admin approval is not a substitute for shopper ownership. Return item validation, confirmed cart revision and recalculated totals for storefront refresh. Bootstrap/refresh request schemas, session invalidation hooks and cart-operation receipts must be frozen in Stage B before enabling cart.*. Checkout, payments and order placement are excluded.
Errors and limits
Error content type is application/problem+json following RFC 9457, with stable extensions code, requestId, operationId when known. Clients branch on status/code, not translated prose. No SQL, stack traces, secrets or session identifiers in errors. Validate body size before parsing; reject unknown mutation fields and invalid types. API and module both rate-limit per connection; proposed manifest limits are ceilings, not billing quotas. Honor Retry-After; retry reads with bounded backoff, reconcile writes first. 202 is accepted/in progress, never completed. A 200 with an invalid schema is not success.
| HTTP | Stable code | Meaning / next step |
|---|---|---|
| 400 | INVALID_REQUEST | Malformed transport/JSON; fix request |
| 401 | AUTHENTICATION_FAILED | Invalid/expired/replayed signature; no effect |
| 403 | SCOPE_DENIED, CONNECTION_REVOKED, STORE_PAUSED, APPROVAL_REQUIRED | No new effect; restore authority explicitly |
| 404 | RESOURCE_NOT_FOUND, OPERATION_NOT_FOUND | Check identity; do not infer write outcome |
| 409 | REVISION_CONFLICT, IDEMPOTENCY_MISMATCH | No new write; inspect conflict |
| 413 | BODY_TOO_LARGE | Reduce request size |
| 422 | VALIDATION_FAILED, CAPABILITY_UNAVAILABLE | Fix data or disable tool |
| 429 | RATE_LIMITED | Respect Retry-After |
| 503 | TEMPORARILY_UNAVAILABLE | Read back operation before write retry |
{"type":"about:blank","title":"Conflict","status":409,"detail":"The approved price revision is no longer current.","code":"REVISION_CONFLICT","requestId":"req_demo","operationId":"4faf6b8a-887e-4c97-93b0-a5476ae95a75"}Compatibility and implementation checklist
Freeze the contract as OpenAPI + JSON Schemas and shared fixtures in STOR-17 before parallel wire implementation. This page defines the target semantics, not a completed SDK or conformance suite. New optional response fields are additive; unknown capability names are ignored/disabled. Unknown operation states must be treated as unresolved, never success. Breaking semantics require a new major; REST 1.0 has no legacy RPC compatibility mode.
Release gates:
- PHP/Node signing and canonical action-hash vectors; mutated method/path/query/body/status fails verification; nonce races, clock skew, rotation and revocation tests.
- OpenCart 3.0.3.5 and 3.0.5.1 fixtures, renamed admin folder and realistic hosting routing; unsupported versions stay disabled. OpenCart 4 is a separate adapter/test matrix.
- Cross-team/store denial, shared-product scope, local Pause, expired approval, prompt-supplied identity and malicious catalog text.
- Catalog pagination/duplicate SKUs; exact decimals; manual edit conflict, including edit-away-and-back (ABA); preserve product associations and effective-price warnings.
- Concurrent identical requests produce one claim; changed payload with reused key fails; kill the process before/after write, read-back, cache and receipt. Demonstrate unknown-state recovery rather than relabeling uncertainty as success.
- Logs correlate connection, run, request and operation IDs with redaction; API audit records approval, request hash and receipt. Module events do not serve as independent Agentfy billing counters.
- Stage B separately proves guest/customer cart ownership, logout/merge/revocation, multiple tabs, duplicate quantity changes and visible cart refresh.
Ownership: Artem Belikov implements module/PHP; yevhen.kariakin owns API, MCP adapter and shared contract. Both sides consume the same fixtures. Remaining freeze decisions: exact signing/KDF vectors, public routing profile, safe revisions/recovery for non-transactional shops, required hook policy and Stage B schemas. Do not advertise writes until these gates pass.
Sources and related work
- ADR 002.
- STOR-16 · STOR-17.
- RFC 9421 — HTTP Message Signatures, RFC 9530 — Content-Digest: signing and digest foundations; the profile and timing limits above are Store Deputy proposals.
- RFC 9457 — Problem Details, RFC 9110 — HTTP semantics: error shape and HTTP behavior; application codes/states are our contract.
- RFC 8785 — JSON Canonicalization Scheme: canonical JSON for action hashes; shared PHP/Node fixtures remain required.