Supercommerce API Docs
Vendor API

Settings Module — Vendor surface

Vendor self-service for reading and writing the active vendor's own settings. Settings are organized by scope (admin for staff-facing config like shipping/tax/payouts, store for…

Vendor self-service for reading and writing the active vendor's own settings. Settings are organized by scope (admin for staff-facing config like shipping/tax/payouts, store for the vendor's storefront customization), then by group (e.g. shipping, branding, contact), then by key. The module is registry-driven: every setting is declared as a SettingDefinition (group + key + zod type + flags like forAdmin) and the vendor surface reads/writes against that registry — there are no free-form key-value writes.

Source: api-modules/settings/src/controllers/vendor-settings.controller.ts.


Conventions

Authentication

All endpoints require a Better-Auth bearer session with an active vendor.

Authorization: Bearer <session-token>

The active vendor is resolved via resolveActiveVendorId(session). Sessions missing an active vendor are rejected with 403 Forbidden.

There is no platform RBAC permission on this surface — vendor users are not platform staff, and the active-vendor scoping is the only access gate.

Tenant scoping

Every read and write scopes to the active vendor's id. There is no way to read or write another vendor's settings from this surface — platform staff use /admin/vendors/:vendorId/settings/... (separate controller, platformVendorSetting:* permissions).

Admin-only keys

Some keys in the vendor registry are flagged forAdmin: true — the vendor can read them but cannot write them. The list of such keys per group is returned in readOnlyKeys on every list response so the UI can render those fields disabled. Writes whose payload includes a forAdmin: true key reject the whole call with 403 (no silent partial application).

Response envelope

{
  "data": <payload>,
  "message": "Success",
  "statusCode": 200,
  "metadata": { /* optional */ }
}

Error envelope

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR (unknown group/key, value fails registry zod, empty body)
401UNAUTHORIZED
403FORBIDDEN (no active vendor, or payload includes a forAdmin: true key)
404NOT_FOUND
500INTERNAL_SERVER_ERROR

Domain types

VendorScopeSettingsResponse

type VendorScopeSettingsResponse = {
  values: Record<string, Record<string, unknown>>; // groupkeyvalue
  readOnlyKeys: Record<string, string[]>;          // groupforAdmin keys
};

VendorGroupSettingsResponse

type VendorGroupSettingsResponse = {
  values: Record<string, unknown>;                 // keyvalue
  readOnlyKeys: string[];                          // forAdmin keys in this group
};

UpdateVendorSettingsInput

Bulk update payload, nested by group:

{
  "shipping": {
    "flat_rate_subunit": 4900,
    "free_above_subunit": 99900
  },
  "tax": {
    "provider_id": "flat"
  }
}

Top-level zod is permissive (Record<string, Record<string, unknown>>); the service validates each (group, key, value) triple against the vendor registry. The body must contain at least one group.

Known vendor groups

ScopeGroupOwner / purpose
adminshippingflat_rate_subunit, free_above_subunit, enabled_providers, provider configs (clickpost.*)
admintaxTax provider id + provider config
adminpayoutsBank account info, payout_hold flag (admin-only)
adminnotificationsVendor-side notification preferences
storebranding, contact, etc.Vendor's storefront customization

Admin scope

Base path: /vendor/settings/admin. Staff-facing config (the vendor's shipping rules, tax provider choice, notification preferences, …).

GET /vendor/settings/admin — Every admin-scope group

Response 200VendorScopeSettingsResponse for the active vendor.

{
  "data": {
    "values": {
      "shipping": {
        "flat_rate_subunit": 4900,
        "free_above_subunit": 99900,
        "enabled_providers": ["clickpost", "self-handled"]
      },
      "tax": {
        "provider_id": "flat"
      }
    },
    "readOnlyKeys": {
      "shipping": [],
      "payouts": ["bank_account_id", "payout_hold"]
    }
  },
  "message": "Success",
  "statusCode": 200
}

GET /vendor/settings/admin/:group — One admin-scope group

Path params

NameNotes
groupGroup identifier (e.g. shipping, tax)

Response 200VendorGroupSettingsResponse.

{
  "data": {
    "values": { "flat_rate_subunit": 4900, "free_above_subunit": 99900 },
    "readOnlyKeys": []
  }
}

Errors

StatusCodeWhen
404NOT_FOUNDUnknown group in this scope

PATCH /vendor/settings/admin — Bulk update

Same body shape as the platform-admin endpoints. Validates each (group, key, value) against the registry. Keys flagged forAdmin: true reject with 403 — the whole call rolls back rather than silently partial-applying.

BodyUpdateVendorSettingsInput

{
  "shipping": {
    "flat_rate_subunit": 4900,
    "free_above_subunit": 99900,
    "enabled_providers": ["clickpost", "self-handled"]
  }
}

Response 200 — updated VendorScopeSettingsResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORUnknown group/key, value fails registry zod, or empty body
403FORBIDDENNo active vendor, or payload includes a forAdmin: true key (use the platform-admin override surface)

Store scope

Base path: /vendor/settings/store. Storefront-facing config (the vendor's branding, contact details, etc.).

GET /vendor/settings/store — Every store-scope group

Response 200VendorScopeSettingsResponse.


GET /vendor/settings/store/:group — One store-scope group

Response 200VendorGroupSettingsResponse.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown group in this scope

PATCH /vendor/settings/store — Bulk update

Same shape and rules as PATCH /vendor/settings/adminforAdmin: true keys reject the whole call with 403.

Response 200 — updated VendorScopeSettingsResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORUnknown group/key, value fails registry zod, or empty body
403FORBIDDENNo active vendor, or payload includes a forAdmin: true key

Registry discovery

A read-only introspection surface that exposes the vendor-overridable settings registry. The vendor UI consumes this to render its settings page directly from the registry — adding a new vendor setting (or group) takes no frontend deploy.

Source: api-modules/settings/src/controllers/vendor-settings-registry.controller.ts.

Authentication

Same as the rest of /vendor/*: a Better-Auth session with an active vendor. Vendor id is resolved server-side via resolveActiveVendorId(session) — never from the client. Sessions missing an active vendor are rejected with 403 Forbidden. No platform RBAC permission applies.

Surfaces returned

The vendor discovery surface returns:

  • admin — every vendor-overridable admin sub-scope entry for the requested groups (e.g. shipping.flat_rate_subunit, payouts.commission_rate).
  • store — every vendor-overridable store sub-scope entry (currently empty — placeholder for future storefront-facing vendor keys).

Entries flagged forAdmin: true in the registry stay in the payload but carry isAdminOnly: true on the descriptor so the UI renders them as read-only — matching the existing /vendor/settings semantics, which returns the same keys and flags them via readOnlyKeys. Writes to those keys still reject with 403 on the existing PATCH surface.

Cascade hydration

currentValue for every entry is resolved through the vendor cascade:

vendor_setting row for (vendorId, scope, group, key)
  ↓ (if absent)
platform setting row at (scope, group, key)
  ↓ (if absent)
registry default

So a vendor that has never written its own shipping.flat_rate_subunit sees the platform default; a vendor that has written one sees its own value.

Domain types

VendorRegistryFieldsResponse

type VendorRegistryFieldsResponse = {
  admin: Record<string, FieldDescriptor[]>;  // groupfields (vendor-overridable admin sub-scope)
  store: Record<string, FieldDescriptor[]>;  // groupfields (vendor-overridable store sub-scope)
};

FieldDescriptor is the same shape as the admin discovery surface — { key, description, default, currentValue, isAdminOnly?, isPublic?, schema }. See admin/settings.md for the field-by-field breakdown.

The response is flatter than the admin one (no vendor.* nesting) because there is no platform-scope surface on the vendor side.


GET /vendor/settings/registry/groups — Group catalog

Returns the flat list of vendor-overridable groups across both sub-scopes. Group keys may collide between admin and store sub-scopes, so each entry carries its scope. settingCount is the total entries in the group (including forAdmin keys) — the UI renders those as read-only via the field-level flag, not by hiding them.

Response 200

{
  "data": {
    "groups": [
      { "scope": "admin", "key": "shipping", "description": "Per-vendor shipping configuration …", "settingCount": 8 },
      { "scope": "admin", "key": "tax",      "description": "Per-vendor tax provider selection …", "settingCount": 2 },
      { "scope": "admin", "key": "returns",  "description": "Per-vendor return-window override …", "settingCount": 3 },
      { "scope": "admin", "key": "payouts",  "description": "Per-vendor payout overrides …",       "settingCount": 3 }
    ]
  },
  "message": "Success",
  "statusCode": 200
}

Errors

StatusCodeWhen
403FORBIDDENNo active vendor on session

GET /vendor/settings/registry/fields — Field descriptors

For the requested set of group keys, returns render-ready field descriptors scoped to the active vendor's cascade.

Query

NameNotes
groupsComma-separated group keys (shipping,payouts). At least one required.

No vendorId parameter — the active vendor on the session is the only target.

Response 200

{
  "data": {
    "admin": {
      "shipping": [
        {
          "key": "flat_rate_subunit",
          "description": "Per-vendor flat shipping rate (subunits) charged to the customer …",
          "default": 0,
          "currentValue": 4900,
          "schema": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }
        },
        {
          "key": "clickpost.api_key",
          "description": "Vendor's ClickPost API key for outbound calls.",
          "default": null,
          "currentValue": null,
          "schema": { "type": "string", "minLength": 1, "maxLength": 500 }
        }
      ],
      "payouts": [
        {
          "key": "commission_rate",
          "description": "Per-vendor commission rate override (basis points). …",
          "default": null,
          "currentValue": null,
          "schema": { "anyOf": [{ "type": "integer", "minimum": 0, "maximum": 10000 }, { "type": "null" }] }
        },
        {
          "key": "payout_hold",
          "description": "Admin-flipped manually to freeze disbursements …",
          "default": false,
          "currentValue": false,
          "isAdminOnly": true,
          "schema": { "type": "boolean" }
        }
      ]
    },
    "store": {}
  },
  "message": "Success",
  "statusCode": 200
}

Errors

StatusCodeWhen
400VALIDATION_ERRORgroups missing or empty
403FORBIDDENNo active vendor on session

Unknown group keys are silently dropped — the response returns 200 with empty buckets so the UI tolerates tab-name lag.


  • shipping — reads admin.shipping.* (rate, free threshold, enabled providers). See shipping.md.
  • shipping-clickpost — reads its own ClickPost credentials block from admin.shipping.clickpost.* (typically via the dedicated vendor-clickpost-config controller — see shipping-clickpost.md).
  • tax-flat — reads admin.tax.*. See tax.md.
  • notifications — reads channel toggles from admin.notifications.*. See notifications.md.
  • admin-rbacplatformVendorSetting:read / :update gate the override surface (/admin/vendors/:vendorId/settings/...) that platform staff use to read/write any vendor's settings, including forAdmin: true keys. The admin-side discovery counterparts of /vendor/settings/registry/* are: GET /admin/settings/registry/* for the global admin page (gated on adminSetting:read; vendor.* is the shape of forAdmin entries only, no per-vendor hydration) and GET /admin/vendors/:vendorId/settings/registry/* for the per-vendor admin page (gated on platformVendorSetting:read; returns the full vendor surface hydrated through the cascade). See admin/settings.md.

On this page