> ## Documentation Index
> Fetch the complete documentation index at: https://docs.capcells.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Create a configuration

> Write the terms Loans are issued under, and receive the stored configuration with the version number an issuance names.

A Product configuration is the set of terms Loans are issued under: one Archetype, and the values that Archetype admits. This call writes version 1 and answers `201` with the stored configuration.

A configuration is never edited. A later change is a new version through `POST /v1/tenants/{tenant_id}/product-configurations/{id}/versions`, and a Loan keeps the version it was issued under.

<Info>
  The body is submitted whole and rejected whole. The first failure stops the walk and names its field, so a body carrying two problems reports one.
</Info>

## Path parameters

<ParamField path="tenant_id" type="string" required>
  The Tenancy the configuration belongs to. 1–64 characters of `a–z A–Z 0–9 - _`. A configuration belonging to another Tenancy answers `404`, the same as one that never existed.
</ParamField>

## Body

<ParamField body="archetype" type="string" required>
  The repayment shape, fixed at creation and never changed by a later version. One of `instalment`, `bnpl`, `payday`. The set is closed.
</ParamField>

<ParamField body="values" type="object" required>
  The terms themselves. Every field is optional on the wire; the Archetype's frame decides which are required, and refuses a missing one by name rather than supplying a default.

  <Expandable title="required of every Archetype" defaultOpen="true">
    <ParamField body="principal_min" type="object" required>
      The floor a Loan's principal falls above. An amount: `{"minor_units": 100000, "currency": "MXN"}`. A bare number or a decimal in `minor_units` is refused.
    </ParamField>

    <ParamField body="principal_max" type="object" required>
      The ceiling a Loan's principal falls below. Below `principal_min` it is refused.
    </ParamField>

    <ParamField body="rate_min" type="string" required>
      The floor of the corridor the Loan's ordinary rate is picked from. A decimal percentage in a string, quoted annually: `"36"` is 36% per year. A JSON number is refused.
    </ParamField>

    <ParamField body="rate_max" type="string" required>
      The ceiling of that corridor. 0 to 2,000% per year.
    </ParamField>

    <ParamField body="penalty_rate_min" type="string" required>
      The floor of the corridor the Loan's penalty rate is picked from, quoted annually. Required of all three Archetypes: a configuration that cannot say what happens when it is not paid does not describe a sellable product.
    </ParamField>

    <ParamField body="penalty_rate_max" type="string" required>
      The ceiling of that corridor. 0 to 2,000% per year.
    </ParamField>

    <ParamField body="day_count_basis" type="string" required>
      How interest counts days. One of `actual_360`, `actual_365_fixed`, `30e_360`.
    </ParamField>

    <ParamField body="schedule" type="object" required>
      The cadence. `{"kind": "anchored_monthly"}`, `{"kind": "anchored_every_n_days", "interval_days": 14}`, or `{"kind": "semi_monthly_grid", "grid_days": [1, 16]}`. A parameter belonging to another kind is refused: an interval on `anchored_monthly`, grid days on `anchored_every_n_days`.
    </ParamField>
  </Expandable>

  <Expandable title="optional on every Archetype">
    <ParamField body="origination_fee" type="object">
      A fee charged at issuance. An amount.
    </ParamField>

    <ParamField body="late_fee" type="object">
      A flat charge once a Period stands unpaid past its grace. An amount. Absent means no late fee, not a default.
    </ParamField>

    <ParamField body="grace_days" type="integer">
      Days after the due date before a Period counts overdue. 0 to 90. Absent means no grace.
    </ParamField>

    <ParamField body="first_period_gap_days" type="integer">
      Days between the anchor and the first due date. 0 to 365.
    </ParamField>

    <ParamField body="early_money_treatment" type="string">
      What unelected money past every matured Period does. `reduces_principal` or `advances_instalments`. Absent is `reduces_principal` — the statutory prepayment, the law's answer rather than a value the Platform picked.
    </ParamField>
  </Expandable>

  <Expandable title="decided by the Archetype">
    <ParamField body="terms" type="integer[]">
      The menu of period counts a Loan is issued for. <Badge color="green">Required</Badge> on `instalment` and `bnpl`. <Badge color="red">Locked</Badge> on `payday`, which is a single period and carries no instalment menu. 1 to 480 periods, at most 24 entries, no entry repeated.
    </ParamField>

    <ParamField body="minimum_instalment" type="object">
      The payment below which the product is not sold. <Badge color="blue">Optional</Badge> on `instalment` and `bnpl`. Not a field of a `payday` product. An issuance whose quoted payment falls under it answers `422`; nothing adjusts the term or the principal to clear the floor.
    </ParamField>

    <ParamField body="merchant_commission_bps" type="integer">
      The commission between tenant and store, in basis points, signed. <Badge color="blue">Optional</Badge> on `bnpl` only. −100% to 100%. Signed because it runs both ways, and nothing at issuance or in servicing reads it.
    </ParamField>

    <ParamField body="rollover_limit" type="integer">
      How often a Loan under this product rolls. <Badge color="blue">Optional</Badge> on `payday` only. 0 to 52.
    </ParamField>

    <ParamField body="rollover_fee" type="object">
      What one roll costs. <Badge color="blue">Optional</Badge> on `payday` only. An amount.
    </ParamField>

    <ParamField body="payment_at_purchase" type="object">
      <Badge color="red">Locked</Badge> on `bnpl`. The collection at the purchase is the Archetype's own invariant, so the field exists only to be refused; a BNPL issuance carries a `down_payment` instead.
    </ParamField>
  </Expandable>
</ParamField>

<Warning>
  Presence is read from the keys of `values`, not from what they decoded to. `"terms": []` and `"terms": null` both count as stated, so a locked field spelled empty is refused rather than quietly accepted.
</Warning>

## Response

<ResponseField name="id" type="string" required>
  The configuration's identifier. An issuance names it as `configuration_id`.
</ResponseField>

<ResponseField name="tenant_id" type="string" required>
  The Tenancy that holds it.
</ResponseField>

<ResponseField name="archetype" type="string" required>
  The Archetype the configuration was created under. A new version carries `values` alone, so this never changes.
</ResponseField>

<ResponseField name="version" type="integer" required>
  Counts from 1 and rises by one. An issuance names it as `configuration_version`; the word `latest` there is refused by name.
</ResponseField>

<ResponseField name="created_at" type="string" required>
  RFC 3339. When the row was written, not a day a fact is about.
</ResponseField>

<ResponseField name="values" type="object" required>
  The stored terms, echoed back in the form they will be read in.
</ResponseField>

## Refusals

Every refusal below is `422` and carries `{"error": "<sentence>"}`. There is no error code — branch on the status.

| Condition                                                                                | What the sentence names                        |
| ---------------------------------------------------------------------------------------- | ---------------------------------------------- |
| A field the Archetype does not carry                                                     | The field, and the Archetype it is not part of |
| A field the Archetype locks                                                              | The field, and the reason the shape fixes it   |
| A required field the frame declares and the body omits                                   | The missing field                              |
| A value outside a Platform bound                                                         | The field and the bound                        |
| A minimum above its maximum                                                              | The corridor or the principal range            |
| Amounts in two currencies inside one configuration                                       | The second currency                            |
| An empty term menu where the frame requires one, or a term offered twice                 | `terms`                                        |
| A rate quoted other than annually, or not an exact decimal percentage                    | The rate field                                 |
| A schedule carrying another kind's parameters                                            | The parameter and the kind                     |
| A `semi_monthly_grid` of fewer than two days, a day outside the month, or a repeated day | `grid_days`                                    |

Any query parameter on this route answers `400`: the route reads none.

<Accordion title="Why a locked field is refused rather than ignored">
  A value you believe is in force and is not is a dispute waiting for its Loan. A `payday` body naming `terms` is telling the Platform the loan repays over a menu of periods; a single-period product cannot honour that, and accepting the body quietly would let the Loan be issued under terms nobody agreed to. The message names both halves — the field, and the Archetype's own reason.
</Accordion>

<RequestExample>
  ```bash Create theme={null}
  curl -X POST "https://api.capcells.com/v1/tenants/$TENANT_ID/product-configurations" \
    -H "Authorization: Bearer $CAPCELLS_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "archetype": "instalment",
      "values": {
        "principal_min": { "minor_units": 100000, "currency": "MXN" },
        "principal_max": { "minor_units": 2000000, "currency": "MXN" },
        "rate_min": "36",
        "rate_max": "60",
        "penalty_rate_min": "36",
        "penalty_rate_max": "90",
        "day_count_basis": "actual_360",
        "schedule": { "kind": "anchored_monthly" },
        "terms": [6, 12, 18],
        "origination_fee": { "minor_units": 25000, "currency": "MXN" },
        "late_fee": { "minor_units": 15000, "currency": "MXN" }
      }
    }'
  ```

  ```json Refused body theme={null}
  {
    "archetype": "payday",
    "values": {
      "terms": []
    }
  }
  ```
</RequestExample>

<ResponseExample>
  ```json 201 theme={null}
  {
    "id": "d57bfbfb-7703-4d87-8a0e-94d4200d3645",
    "tenant_id": "t-0001",
    "archetype": "instalment",
    "created_at": "2026-08-18T22:26:41.407349Z",
    "version": 1,
    "values": {
      "principal_min": { "minor_units": 100000, "currency": "MXN" },
      "principal_max": { "minor_units": 2000000, "currency": "MXN" },
      "rate_min": "36",
      "rate_max": "60",
      "penalty_rate_min": "36",
      "penalty_rate_max": "90",
      "day_count_basis": "actual_360",
      "origination_fee": { "minor_units": 25000, "currency": "MXN" },
      "late_fee": { "minor_units": 15000, "currency": "MXN" },
      "terms": [6, 12, 18],
      "schedule": { "kind": "anchored_monthly" }
    }
  }
  ```

  ```json 422 theme={null}
  {
    "error": "product: field is fixed by the archetype: terms on a payday product: a payday product is a single period and carries no instalment menu"
  }
  ```
</ResponseExample>
