# Products (https://dev.cosmospay.lat/docs/api/products/)

> CRUD for your product catalog.

Base URL: `https://api.cosmospay.lat/cosmos-api`

## List the consumer's products

`GET /v1/products`

Newest first, as `{ data, total, take, skip }` with `take` up to 100 (default 100). Inactive products (`active: false`) are included.

Requires the `products:read` scope.

**Query parameters**

- `take` (number, optional): Page size: how many items to return (1 to 100).
- `skip` (number, optional): How many items to skip before this page (offset).

**Responses**

- `200` (`ProductListEntity`): OK
  - `data` (ProductEntity[], required): The products on this page, newest first.
  - `total` (number, required): Total products matching the query, not the page size.
  - `take` (number, required): Page size applied (1 to 100).
  - `skip` (number, required): Number of products skipped before this page.
- `400`: ValidationFailed (see Shared responses)
- `401`: Unauthorized (see Shared responses)
- `403`: Forbidden (see Shared responses)
- `500`: InternalError (see Shared responses)

## Create a product

`POST /v1/products`

A catalog record: `name`, an optional decimal `amount` (up to 7 decimals; omit for a customer-set price), `assetCode` (omitted, `XLM` or `native` store `native`), `kind` (`one_time` by default, `recurring` or `link`, informational) and `active` (default `true`). Creating one builds no payment link or intent.

Requires the `products:write` scope.

**Request body** (`application/json`, required): `CreateProductDto`

- `name` (string, required): Product name (1 to 120 characters).
- `description` (string, optional): Free-text description of the product (at most 500 characters).
- `amount` (string, optional): Decimal price (≤ 7 decimals). Omit for a customer-set amount.
- `assetCode` (string, optional): Asset code the price is in. Omit for native lumens (XLM).
- `kind` (string, optional): Informational classification: `one_time` (default), `recurring` or `link`. One of: `recurring`, `one_time`, `link`.
- `active` (boolean, optional): Whether the product is active. `false` retires it and keeps its record.
- `reference` (string, optional): Your own SKU or reference for the product (at most 120 characters).

**Responses**

- `201` (`ProductEntity`): Created
  - `id` (string, required): Product id.
  - `name` (string, required): Product name.
  - `description` (object, required): Description of the product; null when none was given.
  - `amount` (object, required): Price as a decimal string in `asset`; null for a customer-set amount.
  - `asset` (string, required): Asset code, or "native" for XLM.
  - `kind` (string, required): Informational classification: `recurring`, `one_time` or `link`. One of: `recurring`, `one_time`, `link`.
  - `active` (boolean, required): Whether the product is active; inactive products are kept and still listed.
  - `reference` (object, required): Your own SKU or reference; null when none was given.
  - `createdAt` (string (date-time), required): When the product was created (ISO 8601).
  - `updatedAt` (string (date-time), required): When the product last changed (ISO 8601).
- `400`: ValidationFailed (see Shared responses)
- `401`: Unauthorized (see Shared responses)
- `403`: Forbidden (see Shared responses)
- `500`: InternalError (see Shared responses)

## Get a product by id

`GET /v1/products/{id}`

One of the consumer’s products, active or not.

Requires the `products:read` scope.

**Path parameters**

- `id` (string, required)

**Responses**

- `200` (`ProductEntity`): OK
- `400`: ValidationFailed (see Shared responses)
- `401`: Unauthorized (see Shared responses)
- `403`: Forbidden (see Shared responses)
- `404`: NotFound (see Shared responses)
- `500`: InternalError (see Shared responses)

## Update a product

`PATCH /v1/products/{id}`

Changes only the fields sent; `assetCode` is normalized as on create. Set `active: false` to retire a product and keep its record.

Requires the `products:write` scope.

**Path parameters**

- `id` (string, required)

**Request body** (`application/json`, required): `UpdateProductDto`

- `name` (string, optional): Product name (1 to 120 characters).
- `description` (string, optional): Free-text description of the product (at most 500 characters).
- `amount` (string, optional): Decimal price (≤ 7 decimals). Omit for a customer-set amount.
- `assetCode` (string, optional): Asset code the price is in. Omit for native lumens (XLM).
- `kind` (string, optional): Informational classification: `one_time` (default), `recurring` or `link`. One of: `recurring`, `one_time`, `link`.
- `active` (boolean, optional): Whether the product is active. `false` retires it and keeps its record.
- `reference` (string, optional): Your own SKU or reference for the product (at most 120 characters).

**Responses**

- `200` (`ProductEntity`): OK
- `400`: ValidationFailed (see Shared responses)
- `401`: Unauthorized (see Shared responses)
- `403`: Forbidden (see Shared responses)
- `404`: NotFound (see Shared responses)
- `500`: InternalError (see Shared responses)

## Delete a product

`DELETE /v1/products/{id}`

Deletes the product for good. To keep its record, set `active: false` with `PATCH` instead.

Requires the `products:write` scope.

**Path parameters**

- `id` (string, required)

**Responses**

- `200` (`ProductDeletedEntity`): OK
  - `id` (string, required): Id of the deleted product.
  - `deleted` (boolean, required): Always true.
- `400`: ValidationFailed (see Shared responses)
- `401`: Unauthorized (see Shared responses)
- `403`: Forbidden (see Shared responses)
- `404`: NotFound (see Shared responses)
- `500`: InternalError (see Shared responses)

## Shared responses

- **ValidationFailed** (`ApiErrorBodyEntity`): The body, query or path failed validation. `message` is an array of the individual failures. Routes that reject a *valid* request for a reason of their own (an amount, a memo, a state) document the code for it on their own 400.
  - `statusCode` (number, required): Always the HTTP status of the response itself.
  - `code` (string, required): Stable machine-readable code. Branch on this, never on the message: messages are human-facing and may be reworded; codes are never renamed once published. Each response documents the subset it can return. One of: `insufficient_scope`, `no_authenticated_consumer`, `gateway_required`, `admin_console_only`, `not_found`, `validation_failed`, `payload_too_large`, `idempotency_conflict`, `operation_in_flight`, `invalid_state_transition`, `slippage_exceeded`, `insufficient_balance`, `no_path_found`, `trustline_missing`, `invalid_amount`, `invalid_memo`, `transaction_rejected`, `transaction_already_settled`, `recipient_not_registered`, `payment_transfer_mismatch`, `fee_transfer_mismatch`, `merchant_fee_transfer_mismatch`, `payer_mismatch`, `transaction_not_found`, `expired`, `network_unsupported`, `asset_unsupported`, `merchant_fee_unsupported`, `provider_error`, `provider_unavailable`, `quote_not_found`, `quote_expired`, `kyc_state_invalid`, `kyc_review_required`, `account_disabled`, `payload_expired`, `rate_limited`, `customer_wallet_taken`, `alias_taken`, `alias_name_invalid`, `alias_challenge_invalid`, `alias_signature_invalid`, `alias_recovery_invalid`, `alias_address_conflict`, `wallet_handshake_invalid`, `wallet_verifier_invalid`, `wallet_login_code_invalid`, `wallet_login_code_cooldown`, `wallet_session_invalid`, `wallet_signature_invalid`, `wallet_backup_invalid`, `wallet_backup_limit`, `wallet_account_mismatch`, `wallet_provider_unavailable`, `wallet_return_url_not_allowed`, `wallet_recovery_setup_refused`, `easy_recovery_not_found`, `easy_recovery_already_offered`, `easy_recovery_not_offered`, `easy_recovery_code_invalid`, `easy_recovery_burned`, `plugin_not_installed`, `plugin_consent_mismatch`, `plugin_rejected`, `plugin_quota_exceeded`, `plugin_failed`, `protected_payment_buyer_mismatch`, `protected_payment_signature_invalid`, `misconfigured`, `internal_error`.
  - `error` (string, required): The HTTP reason phrase for `statusCode`.
  - `message` (string | string[], required): Human-readable detail. An array when class-validator rejected the body.
  - `details` (object, optional): Machine-readable facts about this failure, present only on the codes whose example shows it: `easy_recovery_code_invalid` carries `{ "attemptsLeft": 4 }`. Absent everywhere else.
  - `path` (string, required): The path of the request that failed.
  - `timestamp` (string, required): When the envelope was produced, ISO 8601.
- **Unauthorized** (`ApiErrorBodyEntity`): `no_authenticated_consumer`: The gateway forwarded no API key identity
- **Forbidden** (`ApiErrorBodyEntity`): `insufficient_scope`: the API key does not hold the scope this route requires. The shared public API key also gets this on every route that is not marked `x-cosmos-public-key`, with a message saying so; more scopes will not help, the key is the wrong one. `gateway_required`: the request did not arrive through APISIX.
- **InternalError** (`ApiErrorBodyEntity`): Unexpected server error. The detail is logged server-side and never returned: there is nothing in the body to branch on beyond the code.
- **NotFound** (`ApiErrorBodyEntity`): No such resource, or it exists and belongs to another consumer. The two are deliberately the same answer: "exists but not yours" is an ownership oracle.
