<!-- x-generated: Generated by the Hyperscale artifact pipeline; do not edit by hand -->

# Installment

> Scheduled repayment slice collected from the borrower when it falls due.

An installment is one scheduled part of a credit agreement's repayment: at its due moment the maintenance loop collects the amount from the borrower's account back into the lending pool, gated on the agreement being advanced. A slice the sweep could not collect is marked overdue, and an overdue slice that will never collect writes off as the honest loss path.

- Instrument id: `installment`
- Initial state: `scheduled`
- Terminal states: `collected`, `written_off`
- Composed by: [b2b_trade_credit](../programs/b2b_trade_credit.md), [bnpl_checkout](../programs/bnpl_checkout.md), [dream_trip_packages](../programs/dream_trip_packages.md), [peer_lending_marketplace](../programs/peer_lending_marketplace.md), [tuition_installment_academy](../programs/tuition_installment_academy.md)

## Lifecycle

The diagram draws only the transitions a composing Product exposes as an action. A cell reading "no step any Product exposes" means no drawn edge enters or leaves that state, so the move belongs to a lifecycle step none of the composing Products exposes. Terminal states read "none" under Left by, because stopping there is the declared design rather than a missing step. Where an unexposed step carries a price it is listed under Steps no Product exposes, below.

```mermaid
stateDiagram-v2
    [*] --> scheduled: create
    collected --> [*]
    written_off --> [*]
```

| State | Kind | Entered by | Left by |
| --- | --- | --- | --- |
| `scheduled` | initial | create | no step any Product exposes |
| `overdue` | intermediate | no step any Product exposes | no step any Product exposes |
| `collected` | terminal | no step any Product exposes | none |
| `written_off` | terminal | no step any Product exposes | none |

## Fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `amount` | money<SAR> (minor_unit_integer_string) | yes | Positive minor-unit integer amount serialized as a string (at most 18 digits) |
| `currency` | text | yes | ISO 4217 currency code |
| `dueAt` | datetime | yes |  |
| `memo` | text | no |  |

## References

- `agreementId`: `credit_agreement`, required
- `borrowerAccountId`: `account`, required
- `fundingAccountId`: `account`, required

## Actions

### create

Opens one repayment slice against a credit agreement. The amount is admitted against the agreement's principal cap; no money moves until collection.

- From: none
- To: no state change
- Hints: readOnly=false, destructive=false, idempotent=true, moneyMovement=false

Exposed as:

| Program | Public action | Route |
| --- | --- | --- |
| [b2b_trade_credit](../programs/b2b_trade_credit.md) | `schedulePayment` | `POST /v1/operations/installment.schedulePayment`, scopes installment:write, idempotency required, receipt yes |
| [bnpl_checkout](../programs/bnpl_checkout.md) | `schedulePayment` | `POST /v1/operations/installment.schedulePayment`, scopes installment:write, idempotency required, receipt yes |
| [dream_trip_packages](../programs/dream_trip_packages.md) | `scheduleInstallment` | `POST /v1/operations/installment.scheduleInstallment`, scopes installment:write, idempotency required, receipt yes |
| [peer_lending_marketplace](../programs/peer_lending_marketplace.md) | `scheduleRepayment` | `POST /v1/operations/installment.scheduleRepayment`, scopes installment:write, idempotency required, receipt yes |
| [tuition_installment_academy](../programs/tuition_installment_academy.md) | `scheduleTuitionPayment` | `POST /v1/operations/installment.scheduleTuitionPayment`, scopes installment:write, idempotency required, receipt yes |

Input:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `agreementId` | ref<credit_agreement> | yes | Credit agreement this installment repays |
| `borrowerAccountId` | ref<account> | yes | Hyperscale account ID |
| `fundingAccountId` | ref<account> | yes | Hyperscale account ID |
| `amount` | money<SAR> (minor_unit_integer_string) | yes | Positive minor-unit integer amount serialized as a string (at most 18 digits) |
| `currency` | text | yes | ISO 4217 currency code |
| `dueAt` | datetime | yes |  |
| `memo` | text | no |  |

This action records no effect rows.

Errors: the 30 shared with every action on this instrument, listed once under Errors on every action.

## Steps no Product exposes

The cost table prices 3 lifecycle steps that no Product in this catalog exposes as an action, so none of them is callable through a Product API. They run inside the instrument. The IR carries only their price, so price is all this page can state about them.

### collect

| Action | Effect | Meter | Rate | Table version |
| --- | --- | --- | --- | --- |
| `collect` | `moves.transfer.internal` | `transfer.internal.volume_sar` | 75+11bps | 2026-09-02.1 |
| `collect` | `reads.requires_aggregate` | `instrument.event.count` | 0 | 2026-09-02.1 |
| `collect` | `reads.requires_refs` | `instrument.event.count` | 0 | 2026-09-02.1 |
| `collect` | `schedules.due` | `instrument.event.count` | 0 | 2026-09-02.1 |

### mark_overdue

| Action | Effect | Meter | Rate | Table version |
| --- | --- | --- | --- | --- |
| `mark_overdue` | `schedules.due` | `instrument.event.count` | 0 | 2026-09-02.1 |

### write_off

| Action | Effect | Meter | Rate | Table version |
| --- | --- | --- | --- | --- |
| `write_off` | `reads.requires_aggregate` | `instrument.event.count` | 0 | 2026-09-02.1 |

## Reads

- `installment.list` as `installment_list`: List installments
  - Route: `GET /v1/installments`, scopes installment:read, idempotency not_required, receipt yes
  - Output: items, nextCursor, statusCounts
- `installment.retrieve` as `installment_retrieve`: Retrieve an installment
  - Route: `GET /v1/installments/{installmentId}`, scopes installment:read, idempotency not_required, receipt yes
  - Output: installmentId, tenantId, productId, instrumentId, status, fields, refs, createdAt, updatedAt

## Cost

| Action | Effect | Meter | Rate | Table version |
| --- | --- | --- | --- | --- |
| `collect` | `moves.transfer.internal` | `transfer.internal.volume_sar` | 75+11bps | 2026-09-02.1 |
| `collect` | `reads.requires_aggregate` | `instrument.event.count` | 0 | 2026-09-02.1 |
| `collect` | `reads.requires_refs` | `instrument.event.count` | 0 | 2026-09-02.1 |
| `collect` | `schedules.due` | `instrument.event.count` | 0 | 2026-09-02.1 |
| `mark_overdue` | `schedules.due` | `instrument.event.count` | 0 | 2026-09-02.1 |
| `write_off` | `reads.requires_aggregate` | `instrument.event.count` | 0 | 2026-09-02.1 |

## Errors on every action

The 30 errors every action on this instrument declares. An action's own section lists only what it adds beyond these.

| Code | HTTP | Cause | Action | Remediation | Retryable | Idempotency safe |
| --- | --- | --- | --- | --- | --- | --- |
| `account_not_found` | 404 | No Account matches the `accountId` this request referenced (`account not found`). | List Accounts in the same tenant, product, and environment, then retry with the `accountId` that read returns. | Create or select the Account in the environment the call runs against; ids never carry across tenants, or across sandbox and live. | no | yes |
| `customer_access_compliance_forbidden` | 403 | The request contradicts the Customer access the platform has already durably recorded (`customer access compliance forbidden`). | Re-read the Customer access with `customer_access.retrieve` and compare the submitted ids, currency, product, tenant, and expected state against what comes back. | Rebuild the request from the Customer access as it now stands, then retry under a NEW idempotency key -- reuse the original key only when you are deliberately replaying the original mutation. | no | yes |
| `customer_access_required` | 403 | The operation cannot run against this Customer access until the `customer access required` prerequisite exists. | Supply the field, capability, evidence, or configuration the error details name for this Customer access, then call the operation again. | Complete the prerequisite through the surface that owns it -- the operation reference for this Customer access names it -- and the same call then succeeds unchanged. | no | yes |
| `customer_not_found` | 404 | No Customer matches the `customerId` this request referenced (`customer not found`). | List Customers in the same tenant, product, and environment, then retry with the `customerId` that read returns. | Create or select the Customer in the environment the call runs against; ids never carry across tenants, or across sandbox and live. | no | yes |
| `customer_status_forbidden` | 403 | The Customer's current lifecycle state does not allow this transition (`customer status forbidden`). | Read the Customer with `customer.retrieve` and pick the lifecycle operation its current status allows. | Apply a transition the Customer's current status permits. The reference itself is still valid, so no new Customer is needed. | no | yes |
| `entity_not_found` | 404 | No Entity matches the `entityId` this request referenced (`entity not found`). | List Entities in the same tenant, product, and environment, then retry with the `entityId` that read returns. | Create or select the Entity in the environment the call runs against; ids never carry across tenants, or across sandbox and live. | no | yes |
| `entity_status_forbidden` | 403 | The Entity's current lifecycle state does not allow this transition (`entity status forbidden`). | Read the Entity with `entity.retrieve` and pick the lifecycle operation its current status allows. | Apply a transition the Entity's current status permits. The reference itself is still valid, so no new Entity is needed. | no | yes |
| `instrument_aggregate_cap_exceeded` | 409 | The request contradicts the resource the platform has already durably recorded (`instrument aggregate cap exceeded`). | Re-read the resource this request referenced and compare the submitted ids, currency, product, tenant, and expected state against what comes back. | Rebuild the request from the resource as it now stands, then retry under a NEW idempotency key -- reuse the original key only when you are deliberately replaying the original mutation. | no | yes |
| `instrument_aggregate_currency_mismatch` | 409 | The request contradicts the resource the platform has already durably recorded (`instrument aggregate currency mismatch`). | Re-read the resource this request referenced and compare the submitted ids, currency, product, tenant, and expected state against what comes back. | Rebuild the request from the resource as it now stands, then retry under a NEW idempotency key -- reuse the original key only when you are deliberately replaying the original mutation. | no | yes |
| `instrument_reference_not_found` | 404 | No resource matches the `id` this request referenced (`instrument reference not found`). | List the collection this request referenced in the same tenant, product, and environment, then retry with an `id` that listing returns. | Create or select the resource in the environment the call runs against; ids never carry across tenants, or across sandbox and live. | no | yes |
| `instrument_retired` | 409 | The request contradicts the resource the platform has already durably recorded (`instrument retired`). | Re-read the resource this request referenced and compare the submitted ids, currency, product, tenant, and expected state against what comes back. | Rebuild the request from the resource as it now stands, then retry under a NEW idempotency key -- reuse the original key only when you are deliberately replaying the original mutation. | no | yes |
| `invalid_credentials` | 401 | Hyperscale could not accept the account or credential request. Signup, login, and bearer authentication use one 401 for every case they reject, so the response exposes no credential-specific reason or registered-email verdict. The single exception is a key that is real but homed in the other environment: because the presented secret already matched a stored key, the message names the mismatch instead of leaving a working credential looking unknown. | For signup or login, re-check the submitted account details. For API calls, re-check the `Authorization: Bearer <key>` API key and whether it matches the target environment (sandbox vs live). | For signup, verify the submitted details or use login or password reset for an existing account. For login, re-enter the credentials or reset the password. For API or MCP calls, use an active key for the target environment, set HYPERSCALE_API_KEY, and retry. When the message reports an environment mismatch, `details.keyEnvironment` names where the key does work: send that environment's key, or set `X-Hyperscale-Environment` to it. | no | yes |
| `kyc_tier_insufficient` | 403 | Opening an account of this role requires the owning customer's entity to have ATTAINED a KYC tier it has not reached: either required fields are missing or the profile is not verified. | Complete and verify the entity's KYC profile to the tier this account role requires, then retry. The error details name the required tier and the entity's attained tier. | Submit the missing fields (`entity.kyc.submit`), run verification (`entity.kyc.verification.start`), and once the outcome is verified at the required tier the account open succeeds unchanged. `entity.kyc.retrieve` shows exactly which fields stand between the entity and the required tier. | no | yes |
| `local_evidence_output_key_missing` | 500 | The platform could not durably complete the `local evidence output key missing` step, so the resource may or may not have been written. | Capture the request id and the receipt or operation id before retrying or escalating; do not re-send under a fresh key until you know whether the resource was written. | Treat the resource's side effects as unknown unless a terminal receipt says otherwise; retry only operations documented as retryable, and replay them with the SAME idempotency key. | no | no |
| `local_evidence_reference_missing` | 500 | The platform could not durably complete the `local evidence reference missing` step, so the resource may or may not have been written. | Capture the request id and the receipt or operation id before retrying or escalating; do not re-send under a fresh key until you know whether the resource was written. | Treat the resource's side effects as unknown unless a terminal receipt says otherwise; retry only operations documented as retryable, and replay them with the SAME idempotency key. | no | no |
| `local_evidence_resource_missing` | 500 | The platform could not durably complete the `local evidence resource missing` step, so the resource may or may not have been written. | Capture the request id and the receipt or operation id before retrying or escalating; do not re-send under a fresh key until you know whether the resource was written. | Treat the resource's side effects as unknown unless a terminal receipt says otherwise; retry only operations documented as retryable, and replay them with the SAME idempotency key. | no | no |
| `mfa_verification_required` | 403 | This tenant surface requires a session that completed TOTP at login. Tenant operations demand it while the organization's require-MFA policy (tenant.security.configure) is on; user.mfa.totp.disable demands it unconditionally, because removing a factor is the act that factor exists to guard. | Enroll and confirm a TOTP factor (user.mfa.totp.enrollment.begin, then user.mfa.totp.enrollment.confirm), then log in again and complete user.login.mfa.verify before retrying. | MFA verification is a mint-time session fact, so enrolling a factor mid-session never upgrades the current session. After the factor is active, a fresh login returns an MFA challenge; completing it mints the verified session this surface demands. Identity self-operations (logout, TOTP enrollment) stay open to a password-only session, so a factorless member is never locked out of enrolling. Disabling a factor is the exception: sign in again and complete the challenge first. | no | yes |
| `operation_insert_failed` | 500 | The platform could not durably complete the `operation insert failed` step, so the Operation may or may not have been written. | Capture the request id and the receipt or operation id before retrying or escalating; do not re-send under a fresh key until you know whether the Operation was written. | Treat the Operation's side effects as unknown unless a terminal receipt says otherwise; retry only operations documented as retryable, and replay them with the SAME idempotency key. | no | no |
| `operation_not_finalized` | 409 | The request contradicts the Operation the platform has already durably recorded (`operation not finalized`). | Re-read the Operation with `operation.retrieve` and compare the submitted ids, currency, product, tenant, and expected state against what comes back. | Rebuild the request from the Operation as it now stands, then retry under a NEW idempotency key -- reuse the original key only when you are deliberately replaying the original mutation. | no | yes |
| `product_not_found` | 404 | No Product matches the `productId` this request referenced (`product not found`). | List Products in the same tenant, product, and environment, then retry with the `productId` that read returns. | Create or select the Product in the environment the call runs against; ids never carry across tenants, or across sandbox and live. | no | yes |
| `session_expired` | 401 | The operation refused the request on its `session expired` contract condition. | Read the error details, then the operation reference for this resource, before retrying. | Correct the request against the generated SDK models and a fresh read of the resource; if the shape is already right, the product may be missing the capability this operation needs. | no | yes |
| `session_required` | 401 | The operation cannot run against this resource until the `session required` prerequisite exists. | Supply the field, capability, evidence, or configuration the error details name for this resource, then call the operation again. | Complete the prerequisite through the surface that owns it -- the operation reference for this resource names it -- and the same call then succeeds unchanged. | no | yes |
| `session_revoked` | 401 | The operation refused the request on its `session revoked` contract condition. | Read the error details, then the operation reference for this resource, before retrying. | Correct the request against the generated SDK models and a fresh read of the resource; if the shape is already right, the product may be missing the capability this operation needs. | no | yes |
| `tenant_code_missing` | 500 | The platform could not durably complete the `tenant code missing` step, so the Company may or may not have been written. | Capture the request id and the receipt or operation id before retrying or escalating; do not re-send under a fresh key until you know whether the Company was written. | Treat the Company's side effects as unknown unless a terminal receipt says otherwise; retry only operations documented as retryable, and replay them with the SAME idempotency key. | no | no |
| `usage_amount_missing` | 500 | The platform could not durably complete the `usage amount missing` step, so the resource may or may not have been written. | Capture the request id and the receipt or operation id before retrying or escalating; do not re-send under a fresh key until you know whether the resource was written. | Treat the resource's side effects as unknown unless a terminal receipt says otherwise; retry only operations documented as retryable, and replay them with the SAME idempotency key. | no | no |
| `usage_currency_mismatch` | 500 | The platform could not durably complete the `usage currency mismatch` step, so the resource may or may not have been written. | Capture the request id and the receipt or operation id before retrying or escalating; do not re-send under a fresh key until you know whether the resource was written. | Treat the resource's side effects as unknown unless a terminal receipt says otherwise; retry only operations documented as retryable, and replay them with the SAME idempotency key. | no | no |
| `usage_meter_missing` | 500 | The platform could not durably complete the `usage meter missing` step, so the resource may or may not have been written. | Capture the request id and the receipt or operation id before retrying or escalating; do not re-send under a fresh key until you know whether the resource was written. | Treat the resource's side effects as unknown unless a terminal receipt says otherwise; retry only operations documented as retryable, and replay them with the SAME idempotency key. | no | no |
| `usage_source_conflict` | 409 | The request contradicts the resource the platform has already durably recorded (`usage source conflict`). | Re-read the resource this request referenced and compare the submitted ids, currency, product, tenant, and expected state against what comes back. | Rebuild the request from the resource as it now stands, then retry under a NEW idempotency key -- reuse the original key only when you are deliberately replaying the original mutation. | no | yes |
| `usage_tenant_missing` | 500 | The platform could not durably complete the `usage tenant missing` step, so the resource may or may not have been written. | Capture the request id and the receipt or operation id before retrying or escalating; do not re-send under a fresh key until you know whether the resource was written. | Treat the resource's side effects as unknown unless a terminal receipt says otherwise; retry only operations documented as retryable, and replay them with the SAME idempotency key. | no | no |
| `usage_transfer_missing` | 500 | The platform could not durably complete the `usage transfer missing` step, so the resource may or may not have been written. | Capture the request id and the receipt or operation id before retrying or escalating; do not re-send under a fresh key until you know whether the resource was written. | Treat the resource's side effects as unknown unless a terminal receipt says otherwise; retry only operations documented as retryable, and replay them with the SAME idempotency key. | no | no |
