The control plane of one store's module installs: what the store may install, what it has installed, and the four verbs that move an install along — install, configure, update, uninstall (with verify, enable, reroll and logs around them). A module is a connector or a feature the platform does not ship, packaged as a manifest plus code on the module's own host; the platform calls it with signed requests and the module calls back on a scoped token of its own. The full contract — the manifest, the envelope, the install token, events with retries, packages — is the Custom modules reference; the console walkthrough is Modules.
Every route rides the store token's module grant — its own root key in the access tree, next to admin and public (managers hold module: true; editors catalog, list and logs only). The reads (catalog, list, logs) answer any holder of their verb; every mutation is additionally manager-only — owner or master in the handler, so an editor token is 403 even when its tree names the verb. Uploading a package is not on this group: POST /system/module/upload is master-only at the core and the console carries the multipart.
One exception runs the other way: shipment/events is the one route a module token calls — a carrier's push tracking, minted into the install token when the manifest provides a carrier. It belongs to a later phase; the route is listed for the shape.
The store's module catalog (.dev/plans/custom-modules.md 5.4 "Registry", 6) - the platform packages visible to every store (visibility = all) plus the packages published to the store's organization, and for a master caller (the console's platform admins) also the platform-only packages (visibility = platform, installable by master only); published versions only, plus the version the store has installed. category filters by the module category (D21), q matches the id, names and tags. Each item carries key (the registry key install takes - {id} for a platform package, {org}~{id} for an organization's), id, owner, the latest published manifest, category, tags, latest, versions[] ({version, state, published_at, grants_changed, notes}), installs_count, installed (this store's install, when any), visibility and admitted (phase 8 - false when the store's plan does not admit the package's hosting; the install is then 403 tier_required). Read access - any member with the module.catalog route.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Preferred in header, but store ID can be also used in request body. |
category | string | optional | One module category (shipping, payments, accounting, data, products, orders, customers, mailing, marketplaces, booking, content, forms, analytics, ai, compliance, tools). |
q | string | optional | Free-text filter over id, names and tags. |
Response
object — {items: [...]}
Sample request
{
"category": "tools"
}The store's installed modules (.dev/plans/custom-modules.md 6) - every store/{sid}/modules/{id} install record with its health, has_secrets, update_available, drift and grants_pending. Read access - any member with the module.list route.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Preferred in header, but store ID can be also used in request body. |
Response
object — {items: ModuleInstall[]}
Install a published package version on the store (.dev/plans/custom-modules.md 5.13.3; master or owner). A SEQUENCE across three projects, never one transaction - the registry version record is read (main; version defaults to the key's latest published one, anything else is 409 version_not_published; a key the store may not see is 403 not_visible), the grants must be accepted explicitly (accept_grants = true, else 400 accept_grants_required with the trees the token would carry), a store that already has a module of the same id from another key is 409 id_installed, the tier gate runs (at most 10 modules per store - 403 modules_limit; the store's effective plan must admit the hosting - 403 tier_required; master is NOT exempt), then for a connected module the D23 handshake proves the endpoint before any credential leaves - the keys stage (POST {endpoint}/install {stage = keys, module_key, inbound_key, nonce} signed with the fresh module_key; the x-drop-ack HMAC compared in constant time) - the install token is minted (role service, revealable false, access intersected with the module maximum, the manifest's scope) and custodied (admin-eur3, never the install doc), the token stage follows ({stage = token, token, session_key, config}; a non-200 revokes the token). A handshake failure at either stage is 422 handshake_failed {stage, status, body_head} with NOTHING stored. Mounts follow - the managed setup/hooks entries (module_{id}, signed with module_key), the manifest's content collections + schemas (tagged module), the derived public setup/modules doc - and the install doc store/{sid}/modules/{id} is written LAST, then the registry's install index (modules/{key}/installs/{sid}); verify runs when the manifest asks for it. Phase 1 admits connected modules only (400 hosting_unavailable otherwise). An elements module (hosting = none, .dev/plans/modules/elements-modules.md 2.4) needs no accept_grants and no modules plan feature - it counts against element_modules_max (402 element_modules_limit) - and gets no handshake, token, custody, hook entries or setup/modules - only its kits, the runtime doc (the kits' global css + js) and translations are mounted, then the install doc (no token_id / hook_id).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Preferred in header, but store ID can be also used in request body. |
key | string | required | The registry key from the catalog - {id} (platform) or {org}~{id} (organization). |
version | string | optional | A published version of the key; default the latest published one. |
accept_grants | boolean | required | Must be true - the caller has seen the token's access + scope trees. |
Response
object — {install: ModuleInstall}
Sample request
{
"key": "my-org~echo",
"accept_grants": true
}Move an install to another published version of its key - a diffed re-mount (.dev/plans/custom-modules.md 5.13.5; master or owner). An older published version is a rollback through the same verb; a yanked version is 409 version_yanked, any other non-published state 409 version_not_published. A version that drops a provider kind the store has selected (setup/invoice.provider, setup/order.processor, an enabled delivery or payment method naming the module) is 409 provider_kind_removed {kind, module} (.dev/plans/invoice-providers.md S4). Widened grants (a leaf of the new access/scope trees the installed version did not grant) need accept_grants = true, else 409 grants_pending {version, access, scope} and grants_pending is stamped on the install; narrowed or equal grants proceed. The token's access/scope are rewritten in place (the raw token does not change, no re-handshake); a changed endpoint is a reroll against the new endpoint (fresh keys + a fresh token; the old token is revoked only after the new endpoint proved itself). Hook entries are re-upserted (removed events deleted), collections added (never removed - an update never deletes data), custody params replaced from the new version record, the install doc's version / manifest / report rewritten with update_available cleared and updated_by/at stamped, setup/modules rewritten, verify run again. config, secrets, state, records, outbox and health are untouched. An elements module (hosting = none) is re-mounted only - no grants, custody, token, hooks, collections or setup/modules; a version that changes the hosting is 409 hosting_changed (for every hosting, checked before anything else) and stamps update_blocked {version, error, from, to, at} on the install, which parks the auto-update sweep on that version.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Preferred in header, but store ID can be also used in request body. |
id | string | required | The installed module id. |
version | string | required | The target version (published for the install's key). |
accept_grants | boolean | optional | Required (true) when the new version widens the token's grants. |
Response
object — {install: ModuleInstall}
Sample request
{
"id": "echo",
"version": "1.1.0",
"accept_grants": true
}Write a module's configuration (.dev/plans/custom-modules.md 5.6; master or owner). config carries the manifest's non-secret fields (an unknown key or a secret-kind key is 400 config_field_unknown; an empty value clears the field) and lands on the install doc; secrets carries the secret-kind fields (write-only - stored as module_{id}_{key} in the store's secrets, has_secrets.{key} flips; an empty value deletes it). The derived public doc setup/modules.{id} (the public = true fields only) is rewritten. Secrets never ride the envelope - the module reads its own through /secure/secret, fenced to its module_{id}_* keys. An elements module (hosting = none) has no config - 409 not_applicable {hosting = none}.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Preferred in header, but store ID can be also used in request body. |
id | string | required | The installed module id. |
config | object | optional | Non-secret field values keyed by field key. |
secrets | object | optional | Secret field values keyed by field key (write-only). |
Response
object — {install: ModuleInstall}
Sample request
{
"id": "echo",
"config": {
"erp_url": "https://erp.example.com"
},
"secrets": {
"api_key": "..."
}
}Call the module's verify point with the current non-secret config (.dev/plans/custom-modules.md 5.13.3 step 6; master or owner). A 200 {ok = true} stamps verified_at on the install; anything else clears it and reports the reason. The response's x-drop-module-version is compared with the installed version - a difference stamps drift (the endpoint runs another manifest version; upload that package to update). An elements module (hosting = none) has no verify point - 409 not_applicable {hosting = none}.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Preferred in header, but store ID can be also used in request body. |
id | string | required | The installed module id. |
Response
object — {ok, message?, install}
Sample request
{
"id": "echo"
}Enable or disable an installed module (.dev/plans/custom-modules.md 5.13.6; master or owner). Disabled - every gateway call is refused as module_disabled, the notifier skips it, the storefront answers 404; the token, custody, hook entries and data are kept. Enabling a yanked version is 409 version_yanked (update to a published one first). retry = true clears health after a failing streak. A disable is never refused but answers warnings - what will fail closed once the provider module stops answering (.dev/plans/invoice-providers.md S4 - shipments_open, payments_open, order_processor_active, invoice_provider_active, provider_pending - each {code, module, orders?, count?}); dry_run = true answers {install, warnings} without writing (the console's pre-flight for its confirm dialog).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Preferred in header, but store ID can be also used in request body. |
id | string | required | The installed module id. |
enabled | boolean | required | The new state. |
retry | boolean | optional | Clear health (a failing point) too. |
dry_run | boolean | optional | Answer {install, warnings} without writing. |
Response
object — {install: ModuleInstall, warnings?: [{code, module, orders?, count?}]} - warnings on a disable
Sample request
{
"id": "echo",
"enabled": false,
"dry_run": true
}Reroll a module's identity (.dev/plans/custom-modules.md 5.8; master or owner) - fresh module_key / session_key / inbound_key and a fresh install token handed over through the D23 handshake against the installed endpoint; custody and the hook-signing secrets are replaced, the old token is revoked only after the token stage succeeded (a failed handshake is 422 handshake_failed and the old identity stays in force). An elements module (hosting = none) has no identity - 409 not_applicable {hosting = none}.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Preferred in header, but store ID can be also used in request body. |
id | string | required | The installed module id. |
Response
object — {install: ModuleInstall}
Sample request
{
"id": "echo"
}Uninstall a module - total and ordered (.dev/plans/custom-modules.md 6, 7 item 8; master or owner) - disable, remove the managed hook entries + their signing secrets, revoke the install token(s), delete custody, delete the module's own module_{id}_* secrets, with purge = true also delete its data (the content collections it registered - every document in every locale, the shared schema, the setup/data row last; its private state / records / outbox / logs subtree; the modules.{id} field on the orders / products / customers the manifest listed), drop the registry's installs entry, and delete the install doc LAST. Without purge the collections, records and entity fields stay (the default keep). A provider module (.dev/plans/invoice-providers.md S4) is refused first while work it owns is open - 409 shipments_open {module, orders} (a carrier parcel in shipping / delivery), payments_open {module, orders} (a payment attempt still counter / requires_capture), order_processor_active / invoice_provider_active (the store still selects it), provider_pending {module, kind, orders} (provider calls not resolved); on a clean pass the delivery / payment methods naming it are disabled (never deleted) and an invoicer answers unpaid_invoices (its issued invoices whose paid events stop with the token). A connected module whose manifest declares routes.uninstall - true (D44, KB-A8) gets ONE best-effort signed call, point uninstall, input {purge}, right after its token is revoked (10 s, no retry; a failure is logged and never blocks or fails the uninstall). An elements module (hosting = none) removes only its own elements + runtime doc, its registry installs entry and the install doc - keep and purge are the same.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Preferred in header, but store ID can be also used in request body. |
id | string | required | The installed module id. |
purge | boolean | optional | Also delete the module's data (default false). |
Response
object — {status: uninstalled, module, purged, unpaid_invoices?}
Sample request
{
"id": "echo",
"purge": true
}The admin leg - forward an admin action / card / form submit to the module with the caller's member block (.dev/plans/custom-modules.md 5.7; master or owner). Phase 4 - registered, answers 501 not_available until then. An elements module (hosting = none) has no admin leg - 409 not_applicable {hosting = none}.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Preferred in header, but store ID can be also used in request body. |
id | string | required | The installed module id. |
path | string | required | The admin path declared by the manifest's action / card / form. |
body | object | optional | The call input. |
entity | object | optional | {kind, id} of the entity the action runs on. |
Response
object — {status, body}
A module's delivery log (.dev/plans/custom-modules.md 6) - the outbox rows awaiting or past retry ({label, endpoint, headers, attempts, next_at, last_status, last_error, created_at, dead}, newest first), the install's health, and the runner log rows (phase 5 - empty until then). Read access - any member with the module.logs route. An elements module (hosting = none) answers empty lists.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Preferred in header, but store ID can be also used in request body. |
id | string | required | The installed module id. |
limit | number | optional | Outbox rows to return (default 50, max 200). |
Response
object — {outbox: [...], health, logs: []}
Sample request
{
"id": "echo",
"limit": 20
}The deploy state of a deploy: managed module (.dev/plans/custom-modules.md 5.9a, D38; phase 7) - the store/{sid}/modules/{id}/state/_deploy doc ({state - queued | deploying | ready | failed | deleting, key, version, project_id, region, function, url, identity - sealed | pending | failed, then, error, started_at, updated_at, polls}) plus the function read back live from the store's space cloud ({state, url, version}, null when absent; error when the cloud does not answer). A managed install answers 202 {status - deploying, deploy} and runs once the function is ready - the console polls this every 15 s meanwhile. Read access - any member with the module.deploy route.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Preferred in header, but store ID can be also used in request body. |
id | string | required | The module id. |
Response
object — {deploy: ModuleDeployState | null, function?: {state, url, version} | null, error?}
Sample request
{
"id": "echo"
}A module's documentation (docs/modules.md 7.1 "Documentation") - the package's localized docs/{locale}.md for the asked language (cs, en, or a store locale such as cs_CZ), else its README.md, as markdown. By installed id (the installed version) or by registry key (+ version, default the latest published) for a catalog entry before install - behind the same visibility rule as install (403 not_visible). An installed version the registry no longer lists answers with the last documentation still readable - its own package objects, else the newest version the record carries (the answer's version says which). Every package uploaded since the rule carries documentation; an older version answers 404 docs_missing. Read access - any member with the module.docs route.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Preferred in header, but store ID can be also used in request body. |
id | string | optional | An installed module id (or give key). |
key | string | optional | A registry key ({id} or {org}~{id}) - the catalog's docs before install. |
version | string | optional | A version (default - the installed one, or the key's latest published). |
locale | string | optional | The reader's language (cs, en, cs_CZ); falls back to README.md. |
kind | string | optional | elements - the package's element guide docs/elements.md (the storefront contract for building elements on the module - runtime API, /m/ routes, events) instead of the documentation; 404 docs_missing when the package carries none. |
Response
object — {markdown, locale, path, version, key, languages}
Sample request
{
"id": "reservations",
"locale": "cs"
}The elements of an installed module (.dev/plans/module-owned-elements.md) - every kit element the module mounted with its state (pristine - as shipped; customized - the store edited it, module updates skip it; conflict - a doc the module does not own sits at the kit id and was left alone; missing), the module version each body is based on, the store's own elements that need the module (derived - a duplicate or a detached element; custom - built new), and the runtime doc elements/{id}_runtime (present / disabled / size). Two queries on the store's element library, no package read. Read access - any member with the module.elements route.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Preferred in header, but store ID can be also used in request body. |
id | string | required | The installed module id. |
Response
object — {module, version, enabled, elements: [{id, name, state, version}], dependents: [{id, name, kind, forked}], runtime: {present, disabled, bytes}, stamped}
Sample request
{
"id": "reservations"
}Reset one module element to the module's version (.dev/plans/module-owned-elements.md; master or owner) - the element is re-mounted from the INSTALLED version's package (a full replace - origin {module, version, hash} and module re-stamped), so a customized element follows module updates again. With force = true a store-authored element sitting at the kit id (a conflict) is replaced by the module's; an element owned by another module is never replaced (409 element_conflict). 404 element_not_in_package when the installed version ships no such element.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Preferred in header, but store ID can be also used in request body. |
id | string | required | The installed module id. |
element | string | required | The element doc id ({moduleId}-...). |
force | boolean | optional | Replace a store-authored element at that id. |
Response
object — {element, status} - status as /module/elements answers
Sample request
{
"id": "reservations",
"element": "reservations-calendar"
}A carrier provider module's tracking push (.dev/plans/custom-modules.md 5.5a item 6) - called with the module's own install token (the module id comes from the token's metadata, else 403 module_token_required; the grant module.shipment.events is minted only for a manifest that provides a carrier). The order must be the module's own shipment (shipping.service = module:{id}, else 403 not_own_shipment) with a created shipment (else 409 no_shipment); the events go through the one tracking sink.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Preferred in header, but store ID can be also used in request body. |
order | string | required | The order id the shipment belongs to. |
events | list | required | TrackingEvent rows. |
Response
object — {ok: true, applied, status, changed}
An invoicer provider module's channel back (.dev/plans/invoice-providers.md A5) - called with the module's own install token (module id from the token's metadata, else 403 module_token_required; the grant module.invoice.events is minted only for a manifest that provides an invoicer). Body {events} (at most 50, else 400). issued {order, key, result} reports a document the platform's invoice.issue call got no answer for: accepted only for an order.invoicing entry with that key whose provider is the caller (either state; else skipped not_pending) and a valid IssueResult (number ^[A-Za-z0-9][A-Za-z0-9_.-]{0,39}$, ref ^[A-Za-z0-9_:-]{1,128}$, https urls); the invoice doc is created and the entry cleared in one transaction (invoice_number_taken when another sale holds the number). paid | unpaid | cancelled | restored {number, paid_on?, amount?, currency?} change an invoice the module issued (provider.id, else skipped invoice_not_owned); paid / unpaid also settle / unsettle an offline-paid order (cash, on_delivery, transfer - a card order is left as it is); paid with an amount differing from the invoice total by more than 0.01 records the warning amount_mismatch. amount is in MAJOR units, paid_on YYYY-MM-DD.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Preferred in header, but store ID can be also used in request body. |
events | list | required | InvoiceEvent rows. |
Response
object — {ok: true, applied, skipped: [{index, reason}]}
A payment provider module's channel back (.dev/plans/invoice-providers.md G4) - called with the module's own install token (module id from the token's metadata, else 403 module_token_required; the grant module.payment.events is minted only for a module that provides payment). Each event is {order, key, state} - key is the attempt's idempotency key {order}:p{n} the platform sent in payment.create, state a PaymentState {status: pending | requires_capture | succeeded | canceled | denied | expired | refunded, amount?, currency?, partial?, ref?, method?, reason?, paid_on?} with amounts in integer MINOR units. Ownership is the attempt's provider record, not the order's current payment, so an event for an older attempt or a closed order is still accepted. A paid state must carry amount + currency equal to the attempt's (else the attempt is marked error and the order gets payment_alert: amount_mismatch); refunded raises payment_alert: refunded and never changes the order's payment status; a paid attempt the platform never recorded (the create's answer was lost) raises payment_alert: paid_unrecorded when the key is one the order issued to this module and the state carries ref. The module should query its gateway after every notification and report what the query says. Errors - 400 events_invalid (not an array, or more than 50); per-event refusals are listed in skipped (state_invalid, key_invalid, not_own_payment, stale).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Preferred in header, but store ID can be also used in request body. |
events | list | required | PaymentEvent rows: {order, key, state}. |
Response
object — {ok: true, applied, skipped: [{index, reason}]}
An invoicer provider module's recovery list (.dev/plans/invoice-providers.md A5) - the caller's unresolved invoicing entries (orders whose providers_pending holds invoicer:module:{id}), each with the IssueRequest rebuilt by the platform (paid, due, vendor, covers, delivery as of now), so the module issues under the entry's key and reports through invoice/events issued. A calling entry younger than 120 s is left out (the platform's own call may still be running). Module install token only (403 module_token_required).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Preferred in header, but store ID can be also used in request body. |
limit | number | optional | At most 20. Default: 10 |
Response
object — {pending: [{order, period, key, state, attempts, at, request}]}
An order-system provider module's channel back (.dev/plans/invoice-providers.md O4) - called with the module's own install token (module id from the token's metadata, else 403 module_token_required; the grant module.order.events is minted only for a manifest that provides orders). Body {events} (1 to 50, else 400 events_invalid / events_too_many); a disabled install refuses the whole request (409 module_disabled). Each event {order, event, at (ms, required), note?, ref?, number?, url?, packages?, warning?}: event is a status event (accepted, packed, shipped, delivered, returned, cancelled - only those in the install's provides.reports, else skipped event_undeclared; only on a pushed order, else not_pushed), ref (completes a pending push exactly like an order.push answer, or on a pushed order replaces ref / number / url with a ref_changed ledger row; ^[A-Za-z0-9_:-]{1,128}$ else bad_ref) or warning (appended to processing.warnings, newest 10 of at most 200 chars). Per event: order_not_found, not_own_order (processing.provider is not the caller), bad_event, bad_at (outside now - 90 days .. now + 10 min), duplicate (the {event}:{at} key is already in the ledger), bad_packages (at most 20 of {number 1-64, carrier <= 40, url https, status shipped | delivered | returned, return}). Every accepted event is a processing ledger row; the newest status event above processing.status_at becomes processing.status / status_at (+ its packages, replaced whole). The sink never writes the order status - the order moves through processingStatusTransition (shipped: ready -> shipping; delivered: ready / shipping / delivery -> completed; returned: shipping / delivery -> returned) unless a platform shipment owns the order or setup/order.processor_auto_status is false.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
store | string | optional | Preferred in header, but store ID can be also used in request body. |
events | list | required | OrderProcessingEvent rows. |
Response
object — {ok: true, applied, skipped: [{index, reason}]}