Skip to main content

Merchant API contract

merchant-api owns the authenticated business operations used by the retail portal and POS clients: organizations, stores, catalog, inventory, staff, customers, payments, reporting, and related merchant workflows. Its generated OpenAPI document is the route and schema contract.

Sources of truth

The generated contract includes non-internal controller routes. It does not grant access, and a generated localhost server URL is not a production address.

Public routing

The API is reached through the public API load balancer. /merchant/* routes are forwarded to merchant-api with the prefix removed. The default API host also sends the merchant-owned direct routes to this service, so clients must use the deployment URL configured for their environment rather than deriving a host from the OpenAPI servers field.

Organization and store identifiers are part of many controller paths. They are authorization scope, not merely lookup keys. Callers must use the ID from their authenticated context and must not infer access from knowing another resource ID.

Authentication and authorization

A normal Merchant API request sends both:

Authorization: Bearer <firebase-id-token>
X-Peak-Authorization: Bearer <peak-authorization-token>

Obtain the Peak token from /auth/merchant/token after Firebase sign-in. The service also accepts explicitly supported support and account token types on routes intended for those principals. Token acceptance does not bypass the route's organization, store-membership, ownership, or permission checks.

Authorization is enforced in layers:

  1. Firebase authenticates the principal.
  2. The Peak token establishes the tenant and authorization context.
  3. Controller annotations require the named permission.
  4. Store-scoped handlers verify store membership or ownership where required.
  5. Service logic applies resource-specific invariants.

The default is deny. See the IAM contract for policy evaluation and current seeded roles.

Narrow authentication exceptions

The service security configuration explicitly permits a small set of bootstrap or callback paths. Examples include health/error handling, invite validation, gift-card lookup, payment-provider webhooks, selected public wallet routes, and provider callback surfaces with their own verification. Invite acceptance and support authorization require Firebase even when they do not yet require a Peak token.

Treat each exception as route-specific:

  • Verify the controller and security matcher before integrating.
  • Preserve webhook signature, OIDC, App Check, or token validation implemented for that route.
  • Do not describe a controller group as public because one operation is permitted anonymously.
  • Never expose /internal/ routes to a browser, mobile user, or external integration.

Reading the generated contract

Use the OpenAPI file for exact methods, paths, query parameters, request bodies, status codes, and schemas. When evaluating a proposed integration:

  1. Find the operation in the generated document.
  2. Confirm its organization/store path scope.
  3. Check the controller for the required permission and non-schema invariants.
  4. Confirm the caller can obtain the required Firebase and Peak tokens.
  5. Use an idempotency mechanism only where the operation documents one; do not assume all mutations are safely retryable.

The OpenAPI schema describes the transport contract. It does not replace service-layer rules such as ownership verification, provider state, inventory invariants, or ambiguous payment outcome handling.

Changing the contract

Start from implementation, not from the artifact:

  1. Change the controller, request/response DTOs, validation, and tests.

  2. Regenerate the artifacts:

    bazel run //scripts:sync_openapi_specs
  3. Review the generated Merchant API path and schema changes.

  4. Regenerate affected clients through tools/openapi-generator.

  5. Run the repository contract gate:

    ./scripts/test-local.sh contracts

Do not maintain copied endpoint inventories or DTO tables on this page. They drift from controller annotations and generated clients.