# Referencia de la API de Makatea

> Generada del OpenAPI de producción (versión `2026-09-23`). URL base: `https://makatea.ai/api`. Las descripciones de rutas y campos vienen del spec, en inglés. Las guías en español están en https://makatea.ai/desarrolladores.

## Introducción

The Makatea API lets your system create members and billing orders, register payments, and
receive events. Every response carries `Makatea-API-Version: 2026-09-23` and an
`X-Request-Id` (send your own to correlate logs).

### Authentication

Create an API key in **Makatea → Settings → API**. Send it in either header:

    Authorization: Bearer mk_live_…
    X-API-Key: mk_live_…

Keys have scopes: `read` (GET) and `write` (everything else). A session JWT from the Makatea app
is also accepted in `Authorization` (legacy). Missing or invalid credentials → `401`; a key
without the needed scope → `403 insufficient_scope`.

### Responses

Success always has the same shape, for single objects and for lists:

    {"data": {…} }
    {"data": [ … ], "pagination": {"limit": 100, "has_more": true, "next_cursor": "eyJm…"}}

Errors always look like this:

    {"error": {"code": "validation_error", "message": "amount must be a positive number", "request_id": "…"}}

| Status | Meaning |
|---|---|
| 400 | Malformed request: invalid JSON, unknown query parameter (`unknown_parameter` lists the allowed ones), bad cursor |
| 401 | Missing/invalid credentials |
| 403 | Key lacks the scope |
| 404 | Not found (also unknown routes: `route_not_found`) |
| 405 | Route exists with another method |
| 409 | Conflict with the current state (already paid, duplicate `external_ref`, opted out, …) |
| 413 | Body or document too large |
| 422 | Validation error, or an `Idempotency-Key` reused with a different body |
| 429 | Rate limit exceeded (see `Retry-After`) |
| 5xx | Our fault; safe to retry with the same `Idempotency-Key` |

### Pagination and sync

Lists accept `limit` (default 100, max 500) and `cursor`. Pass the `next_cursor` you received to
get the next page, keeping the same filters; stop when `has_more` is `false`. `offset` still
works for compatibility but can skip rows while data changes. Add `count=true` to get
`pagination.total` (slower).

To mirror data into your system, call the list with `updated_since=<last sync instant>`: results come
ordered by `updated_at` ascending, so you can page with the cursor and store the `updated_at` of the
last row as your next checkpoint.

### Idempotency

Send `Idempotency-Key: <unique string>` on any POST/PUT/PATCH/DELETE. Retrying with the same key and
the same body returns the original response (with header `Idempotent-Replayed: true`) instead of
acting twice. The same key with a different body is `422 idempotency_key_reused`; while the first
request is still running, `409 idempotency_in_progress`. Keys are kept 24 hours.

### Webhooks

Register an https endpoint (`POST /v1/webhook-endpoints`) with the event types you want (or `*`).
The response includes the signing `secret` (`whsec_…`) **only once**. Each event is POSTed as:

    {"id": "<event id>", "type": "payment.received", "created_at": "…", "data": {…}}

with headers `Makatea-Event-Id`, `Makatea-Event-Type` and

    Makatea-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>

Verify by recomputing the HMAC over `t + "." + raw body` and comparing in constant time; reject
timestamps older than 5 minutes. Answer 2xx within 10 seconds. Otherwise we retry after 1 min, 5 min,
30 min, 2 h, 6 h and 24 h; after 8 failed attempts the delivery is `failed` (you can retry it with
`POST /v1/webhook-deliveries/{id}/retry`). Deliveries can arrive more than once or out of order: use
the event `id` to de-duplicate.

Event types: `member.created`, `member.updated`, `billing_order.created`, `billing_order.updated`, `billing_order.paid`, `billing_order.cancelled`, `payment.received`, `payment.voided`, `promise.created`, `promise.kept`, `promise.broken`, `message.sent`, `message.received`. `*.updated` events carry
`{"object": {…}, "previous_attributes": {…}}`. Records created by CSV imports do not emit events.

### Limits

600 requests per minute per API key (per organization for session JWTs). Every response includes
`X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (unix seconds). Over the limit →
`429` with `Retry-After`. Request bodies up to 21 MB; documents up to 15 MB.

### Status

`GET /v1/status` (no key) says whether the service and its database answer.

## Índice de rutas

- **Status**
  - [`GET /v1/status`](#get-v1-status)
- **Members**
  - [`GET /v1/members`](#get-v1-members)
  - [`POST /v1/members`](#post-v1-members)
  - [`GET /v1/members/{id}`](#get-v1-members-id)
  - [`PUT /v1/members/{id}`](#put-v1-members-id)
  - [`PATCH /v1/members/{id}`](#patch-v1-members-id)
  - [`DELETE /v1/members/{id}`](#delete-v1-members-id)
- **Promises**
  - [`GET /v1/members/{id}/promises`](#get-v1-members-id-promises)
  - [`POST /v1/members/{id}/promises`](#post-v1-members-id-promises)
  - [`DELETE /v1/members/{id}/promises/current`](#delete-v1-members-id-promises-current)
- **Credit**
  - [`GET /v1/members/{id}/credit`](#get-v1-members-id-credit)
  - [`POST /v1/members/{id}/credit/apply`](#post-v1-members-id-credit-apply)
- **Messages**
  - [`GET /v1/members/{id}/conversation`](#get-v1-members-id-conversation)
  - [`POST /v1/messages`](#post-v1-messages)
  - [`POST /v1/conversations/import`](#post-v1-conversations-import)
- **Documents**
  - [`GET /v1/members/{id}/documents`](#get-v1-members-id-documents)
  - [`POST /v1/members/{id}/documents`](#post-v1-members-id-documents)
  - [`DELETE /v1/members/{id}/documents/{document_id}`](#delete-v1-members-id-documents-document-id)
- **Billing orders**
  - [`GET /v1/billing-orders`](#get-v1-billing-orders)
  - [`POST /v1/billing-orders`](#post-v1-billing-orders)
  - [`GET /v1/billing-orders/{id}`](#get-v1-billing-orders-id)
  - [`PATCH /v1/billing-orders/{id}`](#patch-v1-billing-orders-id)
  - [`DELETE /v1/billing-orders/{id}`](#delete-v1-billing-orders-id)
  - [`POST /v1/billing-orders/{id}/forgive`](#post-v1-billing-orders-id-forgive)
  - [`POST /v1/billing-orders/{id}/late-fees`](#post-v1-billing-orders-id-late-fees)
  - [`POST /v1/billing-orders/{id}/payment-link`](#post-v1-billing-orders-id-payment-link)
- **Payments**
  - [`POST /v1/billing-orders/{id}/pay`](#post-v1-billing-orders-id-pay)
  - [`POST /v1/billing-orders/{id}/payments`](#post-v1-billing-orders-id-payments)
  - [`GET /v1/payments`](#get-v1-payments)
  - [`GET /v1/payments/{id}`](#get-v1-payments-id)
  - [`POST /v1/payments/{id}/void`](#post-v1-payments-id-void)
- **Credits (loans)**
  - [`GET /v1/credits`](#get-v1-credits)
  - [`POST /v1/credits`](#post-v1-credits)
  - [`GET /v1/credits/{id}`](#get-v1-credits-id)
  - [`PATCH /v1/credits/{id}`](#patch-v1-credits-id)
  - [`GET /v1/settings/payment-allocation`](#get-v1-settings-payment-allocation)
  - [`PUT /v1/settings/payment-allocation`](#put-v1-settings-payment-allocation)
- **Webhooks**
  - [`GET /v1/webhook-endpoints`](#get-v1-webhook-endpoints)
  - [`POST /v1/webhook-endpoints`](#post-v1-webhook-endpoints)
  - [`DELETE /v1/webhook-endpoints/{id}`](#delete-v1-webhook-endpoints-id)
  - [`GET /v1/webhook-events`](#get-v1-webhook-events)
  - [`GET /v1/webhook-deliveries`](#get-v1-webhook-deliveries)
  - [`POST /v1/webhook-deliveries/{id}/retry`](#post-v1-webhook-deliveries-id-retry)
- **Operations**
  - [`GET /v1/sequence-runs`](#get-v1-sequence-runs)
  - [`GET /v1/flows`](#get-v1-flows)
  - [`GET /v1/templates`](#get-v1-templates)
  - [`GET /v1/stats`](#get-v1-stats)

## Status

### GET /v1/status

Service status (no authentication).

Checks that the database answers. Returns 503 when it does not.

**Respuestas**

- `200` OK · `data`: [Status](#status)

## Members

### GET /v1/members

List members.

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `state` | query | string: `contact`, `active`, `at_risk`, `delinquent`, `dormant`, `churned` |  |  |
| `q` | query | string |  | Search in name or phone. |
| `external_ref` | query | string |  | Exact match on your own identifier. |
| `updated_since` | query | string (date-time) |  | Only records changed at or after this instant (ISO-8601). Switches ordering to `updated_at` ascending, so you can resume a sync with the last `next_cursor`. |
| `limit` | query | integer |  |  |
| `cursor` | query | string |  | `next_cursor` from the previous page. |
| `offset` | query | integer |  | Legacy; prefer `cursor`. |
| `count` | query | boolean |  | Include `pagination.total`. |

**Respuestas**

- `200` OK · `data`: array de [Member](#member)
- `401` Missing or invalid credentials
- `429` Rate limit exceeded

### POST /v1/members

Create a member.

Two members may share a phone number. `external_ref` is unique per organization (409 if reused).

Acepta `Idempotency-Key`.

**Cuerpo (JSON)**

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `full_name` | string | sí |  |
| `phone_e164` | string |  | Ejemplo: `"+5215512345678"`. |
| `email` | string |  |  |
| `state` | string: `contact`, `active`, `at_risk`, `delinquent`, `dormant`, `churned` |  | Por omisión: `"active"`. |
| `external_ref` | string |  | Your identifier for this member. |
| `identity` | object |  | Free-form attributes (used as template variables). |
| `tags` | array de string |  |  |
| `notes` | string |  |  |
| `customer_since` | string (date) |  | When the member became your customer (not when it entered Makatea). Not in the future. |
| `do_not_contact` | boolean |  | Never send this member any message. Requires `do_not_contact_reason`. Por omisión: `false`. |
| `do_not_contact_reason` | string |  |  |

**Respuestas**

- `201` OK · `data`: [Member](#member)
- `401` Missing or invalid credentials
- `422` Validation error
- `429` Rate limit exceeded

### GET /v1/members/{id}

Get a member (with billing orders and recent interactions).

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |

**Respuestas**

- `200` OK · `data`: [Member](#member)
- `401` Missing or invalid credentials
- `404` Not found
- `429` Rate limit exceeded

### PUT /v1/members/{id}

Update a member.

Only the fields you send are changed.

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |

Acepta `Idempotency-Key`.

**Cuerpo (JSON)**

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `full_name` | string |  |  |
| `phone_e164` | string |  |  |
| `email` | string, nullable |  |  |
| `state` | string: `contact`, `active`, `at_risk`, `delinquent`, `dormant`, `churned` |  |  |
| `external_ref` | string, nullable |  |  |
| `identity` | object |  |  |
| `tags` | array de string |  |  |
| `notes` | string, nullable |  |  |
| `customer_since` | string (date), nullable |  | Not in the future. `null` clears it. |
| `do_not_contact` | boolean |  | `true` requires `do_not_contact_reason` (sent now or already on file); `false` clears the reason. Pending messages are withdrawn when it turns on. |
| `do_not_contact_reason` | string |  | Can be sent alone to change the reason of a member already marked. |

**Respuestas**

- `200` OK · `data`: [Member](#member)
- `401` Missing or invalid credentials
- `404` Not found
- `422` Validation error
- `429` Rate limit exceeded

### PATCH /v1/members/{id}

Update a member (partial).

Only the fields you send are changed.

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |

Acepta `Idempotency-Key`.

**Cuerpo (JSON)**

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `full_name` | string |  |  |
| `phone_e164` | string |  |  |
| `email` | string, nullable |  |  |
| `state` | string: `contact`, `active`, `at_risk`, `delinquent`, `dormant`, `churned` |  |  |
| `external_ref` | string, nullable |  |  |
| `identity` | object |  |  |
| `tags` | array de string |  |  |
| `notes` | string, nullable |  |  |
| `customer_since` | string (date), nullable |  | Not in the future. `null` clears it. |
| `do_not_contact` | boolean |  | `true` requires `do_not_contact_reason` (sent now or already on file); `false` clears the reason. Pending messages are withdrawn when it turns on. |
| `do_not_contact_reason` | string |  | Can be sent alone to change the reason of a member already marked. |

**Respuestas**

- `200` OK · `data`: [Member](#member)
- `401` Missing or invalid credentials
- `404` Not found
- `422` Validation error
- `429` Rate limit exceeded

### DELETE /v1/members/{id}

Soft-delete a member (state becomes `churned`).

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |

Acepta `Idempotency-Key`.

**Respuestas**

- `200` OK · `data`: [Member](#member)
- `401` Missing or invalid credentials
- `404` Not found
- `429` Rate limit exceeded

## Promises

### GET /v1/members/{id}/promises

Current promise and history.

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |

**Respuestas**

- `200` OK · `data`: [PromiseState](#promisestate)
- `401` Missing or invalid credentials
- `404` Not found
- `429` Rate limit exceeded

### POST /v1/members/{id}/promises

Record a payment promise.

Pauses collection messages until the promised date (one reminder survives). Replaces any active promise. When the date passes the promise becomes `kept` or `broken` (webhooks `promise.kept` / `promise.broken`).

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |

Acepta `Idempotency-Key`.

**Cuerpo (JSON)**

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `promised_date` | string (date) | sí |  |
| `amount` | number |  |  |

**Respuestas**

- `201` OK · `data`: [PromiseState](#promisestate)
- `401` Missing or invalid credentials
- `404` Not found
- `422` Validation error
- `429` Rate limit exceeded

### DELETE /v1/members/{id}/promises/current

Cancel the active promise.

Marks it `cancelled` (kept in history) and re-schedules collection for the member's open orders. 409 if there is no active promise.

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |

Acepta `Idempotency-Key`.

**Respuestas**

- `200` OK · `data`: [PromiseState](#promisestate)
- `401` Missing or invalid credentials
- `404` Not found
- `429` Rate limit exceeded

## Credit

### GET /v1/members/{id}/credit

Credit balance and its ledger.

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |

**Respuestas**

- `200` OK · `data`: [Credit](#credit)
- `401` Missing or invalid credentials
- `404` Not found
- `429` Rate limit exceeded

### POST /v1/members/{id}/credit/apply

Apply the credit balance to open orders (oldest first).

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |

Acepta `Idempotency-Key`.

**Respuestas**

- `200` OK · `data`: [CreditApplied](#creditapplied)
- `401` Missing or invalid credentials
- `404` Not found
- `429` Rate limit exceeded

## Messages

### GET /v1/members/{id}/conversation

Messages exchanged with a member (newest first).

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |
| `channel` | query | string: `whatsapp`, `sms`, `email` |  |  |
| `limit` | query | integer |  |  |
| `cursor` | query | string |  | `next_cursor` from the previous page. |
| `offset` | query | integer |  | Legacy; prefer `cursor`. |
| `count` | query | boolean |  | Include `pagination.total`. |

**Respuestas**

- `200` OK · `data`: array de [Interaction](#interaction)
- `401` Missing or invalid credentials
- `404` Not found
- `429` Rate limit exceeded

### POST /v1/messages

Send a message to a member.

With `text`: sent right away through the organization's line (WhatsApp or SMS) — `status: sent`.

With `template_id`: queued for the sending engine, which renders the template variables — `status: queued` (any channel, including email).

Respects opt-outs (409 `opted_out`), the organization's kill switch (409 `comms_disabled`) and its send window (409 `outside_send_window`, text only; queued messages wait for the window).

Organizations in simulation mode record the message but nothing leaves (`status: simulated`).

Acepta `Idempotency-Key`.

**Cuerpo (JSON)**

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `member_id` | string (uuid) | sí |  |
| `channel` | string: `whatsapp`, `sms`, `email` |  | Por omisión: `"whatsapp"`. |
| `text` | string |  |  |
| `template_id` | string (uuid) |  |  |

**Respuestas**

- `202` OK · `data`: [MessageResult](#messageresult)
- `401` Missing or invalid credentials
- `422` Validation error
- `429` Rate limit exceeded

### POST /v1/conversations/import

Import conversation history (messages that happened outside Makatea).

Up to 20,000 rows per call. Idempotent by `external_id` (or by member + time + direction + text). Imported messages trigger nothing: no auto-replies, no notices, no webhooks. `dry_run` validates without writing. Members are matched by `member_external_ref`, or by `phone` when exactly one member has it.

Acepta `Idempotency-Key`.

**Cuerpo (JSON)**

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `dry_run` | boolean |  |  |
| `rows` | array de object |  |  |

Cada elemento de `rows`:

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `member_external_ref` | string |  |  |
| `phone` | string |  |  |
| `occurred_at` | string (date-time) | sí |  |
| `direction` | string: `inbound`, `outbound` | sí |  |
| `channel` | string: `whatsapp`, `email`, `sms` | sí |  |
| `body` | string | sí |  |
| `subject` | string |  |  |
| `external_id` | string |  |  |

**Respuestas**

- `200` OK · `data`: [ConversationImportResult](#conversationimportresult)
- `401` Missing or invalid credentials
- `422` Validation error
- `429` Rate limit exceeded

## Documents

### GET /v1/members/{id}/documents

List a member's documents (with temporary download URLs, valid 1 hour).

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |
| `external_ref` | query | string |  | Exact match on your own identifier. |

**Respuestas**

- `200` OK · `data`: array de [Document](#document)
- `401` Missing or invalid credentials
- `404` Not found
- `429` Rate limit exceeded

### POST /v1/members/{id}/documents

Attach a document to a member.

Send the file inline (`content_base64`) or a public https `url` the server downloads. Max 15 MB. With `external_ref`, repeating the call returns the existing document (200) instead of uploading twice.

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |

Acepta `Idempotency-Key`.

**Cuerpo (JSON)**

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `name` | string | sí |  |
| `mime_type` | string |  |  |
| `content_base64` | string (byte) |  |  |
| `url` | string (uri) |  |  |
| `external_ref` | string |  |  |
| `type` | string: `ine_frontal`, `ine_reverso`, `comprobante`, `otro` |  | Por omisión: `"otro"`. |
| `description` | string |  |  |

**Respuestas**

- `201` OK · `data`: [Document](#document)
- `401` Missing or invalid credentials
- `404` Not found
- `422` Validation error
- `429` Rate limit exceeded

### DELETE /v1/members/{id}/documents/{document_id}

Delete a document (file and record).

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |
| `document_id` | path | string (uuid) | sí |  |

Acepta `Idempotency-Key`.

**Respuestas**

- `200` OK · `data`: [Deleted](#deleted)
- `401` Missing or invalid credentials
- `404` Not found
- `429` Rate limit exceeded

## Billing orders

### GET /v1/billing-orders

List billing orders.

Default order: `due_date` ascending. With `updated_since`: `updated_at` ascending.

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `status` | query | string: `pending`, `overdue`, `paid`, `cancelled` |  |  |
| `member_id` | query | string (uuid) |  |  |
| `external_ref` | query | string |  | Exact match on your own identifier. |
| `updated_since` | query | string (date-time) |  | Only records changed at or after this instant (ISO-8601). Switches ordering to `updated_at` ascending, so you can resume a sync with the last `next_cursor`. |
| `credit_id` | query | string (uuid) |  | Only the installments of this credit. |
| `due_from` | query | string (date) |  |  |
| `due_to` | query | string (date) |  |  |
| `limit` | query | integer |  |  |
| `cursor` | query | string |  | `next_cursor` from the previous page. |
| `offset` | query | integer |  | Legacy; prefer `cursor`. |
| `count` | query | boolean |  | Include `pagination.total`. |

**Respuestas**

- `200` OK · `data`: array de [BillingOrder](#billingorder)
- `401` Missing or invalid credentials
- `429` Rate limit exceeded

### POST /v1/billing-orders

Create a billing order.

Acepta `Idempotency-Key`.

**Cuerpo (JSON)**

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `member_id` | string (uuid) | sí |  |
| `amount` | number | sí |  |
| `currency` | string |  | Por omisión: `"MXN"`. |
| `due_date` | string (date) | sí |  |
| `period_label` | string |  |  |
| `external_ref` | string |  | Unique per organization. |
| `notes` | string |  |  |
| `capital_portion` | number, nullable |  | Principal part of this installment. Send together with `interest_portion` (both `null` removes the breakdown). capital + interest ≤ amount; the rest of `amount` is the collectible late fee and, beyond it, «other charges». |
| `interest_portion` | number, nullable |  | Interest part of this installment (include commissions you consider interest). |
| `credit_id` | string (uuid), nullable |  | Credit (loan) this installment belongs to; must be of the same member. `null` detaches it. |

**Respuestas**

- `201` OK · `data`: [BillingOrder](#billingorder)
- `401` Missing or invalid credentials
- `422` Validation error
- `429` Rate limit exceeded

### GET /v1/billing-orders/{id}

Get a billing order.

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |

**Respuestas**

- `200` OK · `data`: [BillingOrder](#billingorder)
- `401` Missing or invalid credentials
- `404` Not found
- `429` Rate limit exceeded

### PATCH /v1/billing-orders/{id}

Update a billing order (move the date, change the amount, …).

Moving `due_date` re-schedules its collection messages. Raising `amount` above what was paid re-opens a paid order; lowering it to what is covered marks it paid. `late_fee` sets the accumulated late fee (absolute). `capital_portion`/`interest_portion` set the breakdown (also on paid orders: it is history) and re-apply the order's payments. Cancelled orders cannot be edited (409).

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |

Acepta `Idempotency-Key`.

**Cuerpo (JSON)**

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `due_date` | string (date) |  |  |
| `amount` | number |  |  |
| `period_label` | string, nullable |  |  |
| `late_fee` | number |  |  |
| `external_ref` | string, nullable |  |  |
| `notes` | string, nullable |  |  |
| `member_id` | string (uuid) |  | Move the order (with its payments) to another member of the organization. |
| `capital_portion` | number, nullable |  | Principal part of this installment. Send together with `interest_portion` (both `null` removes the breakdown). capital + interest ≤ amount; the rest of `amount` is the collectible late fee and, beyond it, «other charges». |
| `interest_portion` | number, nullable |  | Interest part of this installment (include commissions you consider interest). |
| `credit_id` | string (uuid), nullable |  | Credit (loan) this installment belongs to; must be of the same member. `null` detaches it. |

**Respuestas**

- `200` OK · `data`: [BillingOrder](#billingorder)
- `401` Missing or invalid credentials
- `404` Not found
- `422` Validation error
- `429` Rate limit exceeded

### DELETE /v1/billing-orders/{id}

Cancel a billing order (409 if already paid).

Optional `reason` (query `?reason=` or JSON body `{"reason": …}`, max 500 chars) is stored as `cancel_reason` — e.g. «consolidated into another account». Cancelling an already cancelled order with a reason updates the reason.

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |
| `reason` | query | string |  | Why the order is cancelled (stored as `cancel_reason`). |

Acepta `Idempotency-Key`.

**Cuerpo (JSON)**

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `reason` | string |  |  |

**Respuestas**

- `200` OK · `data`: [BillingOrder](#billingorder)
- `401` Missing or invalid credentials
- `404` Not found
- `422` Validation error
- `429` Rate limit exceeded

### POST /v1/billing-orders/{id}/forgive

Forgive (condone) part or all of the balance.

Forgiven money is not a payment: it does not appear in `/v1/payments`. Without `amount`, forgives the whole balance and the order becomes `paid`. On orders with a breakdown the forgiveness takes, of what is still owed, late fee first, then interest, then other charges and principal last (`forgiven_now_breakdown`; forgiven principal is a loss).

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |

Acepta `Idempotency-Key`.

**Cuerpo (JSON)**

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `amount` | number |  |  |
| `reason` | string |  |  |

**Respuestas**

- `200` OK · `data`: [BillingOrder](#billingorder)
- `401` Missing or invalid credentials
- `404` Not found
- `422` Validation error
- `429` Rate limit exceeded

### POST /v1/billing-orders/{id}/late-fees

Add a late fee to an order.

Adds to `late_fee` and fires the `late_fee_applied` notice if the organization configured one. With `collectible: true` the fee is also added to `amount` (it becomes owed; a paid order re-opens). When omitted, the organization's `moratorio_cobrable` setting decides (default: informational only).

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |

Acepta `Idempotency-Key`.

**Cuerpo (JSON)**

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `amount` | number | sí |  |
| `collectible` | boolean |  |  |

**Respuestas**

- `201` OK · `data`: [BillingOrder](#billingorder)
- `401` Missing or invalid credentials
- `404` Not found
- `422` Validation error
- `429` Rate limit exceeded

### POST /v1/billing-orders/{id}/payment-link

Create (or reuse) a payment link with the organization's payment provider.

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |

Acepta `Idempotency-Key`.

**Respuestas**

- `200` OK · `data`: [PaymentLink](#paymentlink)
- `401` Missing or invalid credentials
- `404` Not found
- `429` Rate limit exceeded

## Payments

### POST /v1/billing-orders/{id}/pay

Settle the whole remaining balance.

Registers one payment for exactly the remaining balance. For partial or historical payments use `POST /v1/billing-orders/{id}/payments`.

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |

Acepta `Idempotency-Key`.

**Cuerpo (JSON)**

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `method` | string |  | Por omisión: `"external"`. |
| `type` | string |  | Old name of `method`. |
| `reference` | string |  |  |
| `notes` | string |  |  |
| `paid_at` | string |  | When the money was received. Defaults to now. A date without time (`2026-09-22`) means that day in the organization's timezone (`organizations.timezone`): today → now, any other day → 12:00 local that day, so it never shifts to the previous day. A date and time without offset (`2026-09-22T23:30`) is that wall-clock time in the organization's timezone. A full ISO-8601 timestamp with `Z` or an offset is taken as is. Cannot be in the future. Ejemplo: `"2026-09-22"`. |

**Respuestas**

- `200` OK · `data`: [PaymentResult](#paymentresult)
- `401` Missing or invalid credentials
- `404` Not found
- `422` Validation error
- `429` Rate limit exceeded

### POST /v1/billing-orders/{id}/payments

Register a payment (partial, historical date, overpayment policy).

`excess` decides what happens when `amount` is larger than the balance: `reject` (default) answers 422 `amount_exceeds_balance`; `credit` applies the excess to the member's next open orders (oldest first) and keeps the rest as credit balance.

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |

Acepta `Idempotency-Key`.

**Cuerpo (JSON)**

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `amount` | number | sí |  |
| `paid_at` | string |  | When the money was received. Defaults to now. A date without time (`2026-09-22`) means that day in the organization's timezone (`organizations.timezone`): today → now, any other day → 12:00 local that day, so it never shifts to the previous day. A date and time without offset (`2026-09-22T23:30`) is that wall-clock time in the organization's timezone. A full ISO-8601 timestamp with `Z` or an offset is taken as is. Cannot be in the future. Ejemplo: `"2026-09-22"`. |
| `method` | string |  | Ejemplo: `"transfer"`. |
| `reference` | string |  |  |
| `notes` | string |  |  |
| `excess` | string: `reject`, `credit` |  | Por omisión: `"reject"`. |

**Respuestas**

- `201` OK · `data`: [PaymentResult](#paymentresult)
- `401` Missing or invalid credentials
- `404` Not found
- `422` Validation error
- `429` Rate limit exceeded

### GET /v1/payments

List payments (the ledger).

Every money movement: payments (`kind: payment`) and reversals of voided payments (`kind: reversal`, negative amount). `allocation` says how each entry was applied to its installment (principal, interest, late fee, other charges, unassigned; `null` when the installment has no breakdown); a reversal carries exactly the same figures, negative. `paid_at` is when the money was received (it can be in the past); `recorded_at` is when the entry reached Makatea — `created_since`/`updated_since` and the ordering use `recorded_at`, so a back-dated payment registered today still shows up in your next sync. The ledger is append-only, so `updated_since` is an alias of `created_since`. Default order: newest first; with a `*_since` filter: oldest first.

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `member_id` | query | string (uuid) |  |  |
| `billing_order_id` | query | string (uuid) |  |  |
| `created_since` | query | string (date-time) |  | Only records created at or after this instant (ISO-8601). Ordering becomes `created_at` ascending. |
| `updated_since` | query | string (date-time) |  | Only records changed at or after this instant (ISO-8601). Switches ordering to `updated_at` ascending, so you can resume a sync with the last `next_cursor`. |
| `limit` | query | integer |  |  |
| `cursor` | query | string |  | `next_cursor` from the previous page. |
| `offset` | query | integer |  | Legacy; prefer `cursor`. |
| `count` | query | boolean |  | Include `pagination.total`. |

**Respuestas**

- `200` OK · `data`: array de [Payment](#payment)
- `401` Missing or invalid credentials
- `429` Rate limit exceeded

### GET /v1/payments/{id}

Get a payment.

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |

**Respuestas**

- `200` OK · `data`: [Payment](#payment)
- `401` Missing or invalid credentials
- `404` Not found
- `429` Rate limit exceeded

### POST /v1/payments/{id}/void

Void a payment.

Writes a reversal (negative) entry, lowers the order's `amount_paid` and re-opens it if needed. 409 `already_voided` the second time.

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |

Acepta `Idempotency-Key`.

**Cuerpo (JSON)**

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `reason` | string |  |  |

**Respuestas**

- `200` OK · `data`: [Payment](#payment)
- `401` Missing or invalid credentials
- `404` Not found
- `422` Validation error
- `429` Rate limit exceeded

## Credits (loans)

### GET /v1/credits

List credits (loans) with their principal, what came back and what is owed.

Each credit groups the installments of one loan (`external_ref` = your folio) and carries its principal. `totals` are to date: `capital_recovered`/`interest_collected`/`late_fee_collected` = what the payment ledger applied to its installments (voided payments net out); `capital_outstanding`/`interest_outstanding` = of its open installments with breakdown; `balance` = amount − paid − forgiven of its open installments; `paid_without_ledger` = paid amounts imported as history, with no ledger entry. Default order: `start_date` descending; with `updated_since`: `updated_at` ascending.

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `member_id` | query | string (uuid) |  |  |
| `external_ref` | query | string |  | Exact match on your own identifier. |
| `updated_since` | query | string (date-time) |  | Only records changed at or after this instant (ISO-8601). Switches ordering to `updated_at` ascending, so you can resume a sync with the last `next_cursor`. |
| `status` | query | string: `active`, `liquidated`, `cancelled` |  |  |
| `limit` | query | integer |  |  |
| `cursor` | query | string |  | `next_cursor` from the previous page. |
| `offset` | query | integer |  | Legacy; prefer `cursor`. |
| `count` | query | boolean |  | Include `pagination.total`. |

**Respuestas**

- `200` OK · `data`: array de [Loan](#loan)
- `401` Missing or invalid credentials
- `429` Rate limit exceeded

### POST /v1/credits

Create a credit (loan) and attach its installments.

`external_ref` (your folio) is unique per organization: reusing it is 409. Installments are attached by id or by their `external_ref`; they must belong to the same member (422 `credit_member_mismatch`). `start_date` is when the money was lent: it decides in which period the principal counts as «placed».

Acepta `Idempotency-Key`.

**Cuerpo (JSON)**

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `member_id` | string (uuid) | sí |  |
| `external_ref` | string |  | Your folio / contract number. Unique per organization. |
| `principal` | number | sí | Money lent (capital prestado). |
| `start_date` | string (date) | sí | Date the money was lent. |
| `total_amount` | number |  | Total the member will pay (principal + interest), if you have it. |
| `periods` | integer |  |  |
| `frequency` | string: `weekly`, `biweekly`, `monthly`, `daily`, `other` |  |  |
| `notes` | string |  |  |
| `billing_order_ids` | array de string (uuid) |  |  |
| `billing_order_external_refs` | array de string |  |  |

**Respuestas**

- `201` OK · `data`: [LoanDetail](#loandetail)
- `401` Missing or invalid credentials
- `422` Validation error
- `429` Rate limit exceeded

### GET /v1/credits/{id}

Get a credit with its installments and their breakdown.

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |

**Respuestas**

- `200` OK · `data`: [LoanDetail](#loandetail)
- `401` Missing or invalid credentials
- `404` Not found
- `429` Rate limit exceeded

### PATCH /v1/credits/{id}

Update a credit, attach or detach installments.

Only the fields you send change. `status` can be set to `cancelled` (the principal stops counting as placed) or back to `active`; `liquidated` is computed from its installments.

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |

Acepta `Idempotency-Key`.

**Cuerpo (JSON)**

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `external_ref` | string, nullable |  |  |
| `principal` | number |  |  |
| `start_date` | string (date) |  |  |
| `total_amount` | number, nullable |  |  |
| `periods` | integer, nullable |  |  |
| `frequency` | string: `weekly`, `biweekly`, `monthly`, `daily`, `other`, nullable |  |  |
| `notes` | string, nullable |  |  |
| `status` | string: `active`, `cancelled` |  |  |
| `add_billing_order_ids` | array de string (uuid) |  |  |
| `add_billing_order_external_refs` | array de string |  |  |
| `remove_billing_order_ids` | array de string (uuid) |  |  |

**Respuestas**

- `200` OK · `data`: [LoanDetail](#loandetail)
- `401` Missing or invalid credentials
- `404` Not found
- `422` Validation error
- `429` Rate limit exceeded

### GET /v1/settings/payment-allocation

How payments are split between late fee, interest and principal.

**Respuestas**

- `200` OK · `data`: [AllocationRule](#allocationrule)
- `401` Missing or invalid credentials
- `429` Rate limit exceeded

### PUT /v1/settings/payment-allocation

Change the payment allocation rule.

`moratorio_interes_capital` (default: late fee, then interest, then other charges, then principal), `proporcional` (each payment split in proportion to what is owed of each part) or `al_liquidar` (a partial payment stays «unassigned» until the installment is settled; the settling payment recognizes all of its principal and interest on its date). The rule is fixed on each installment when its breakdown is loaded; with `apply_existing: true` every installment with breakdown is re-applied with the new rule (past figures change).

Acepta `Idempotency-Key`.

**Cuerpo (JSON)**

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `rule` | string: `moratorio_interes_capital`, `proporcional`, `al_liquidar` | sí |  |
| `apply_existing` | boolean |  | Por omisión: `false`. |

**Respuestas**

- `200` OK · `data`: [AllocationRule](#allocationrule)
- `401` Missing or invalid credentials
- `422` Validation error
- `429` Rate limit exceeded

## Webhooks

### GET /v1/webhook-endpoints

List webhook endpoints.

**Respuestas**

- `200` OK · `data`: array de [WebhookEndpoint](#webhookendpoint)
- `401` Missing or invalid credentials
- `429` Rate limit exceeded

### POST /v1/webhook-endpoints

Register a webhook endpoint (the signing secret is returned only here).

Acepta `Idempotency-Key`.

**Cuerpo (JSON)**

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `url` | string (uri) | sí | https only; private/internal hosts are rejected. |
| `events` | array de string: `*`, `member.created`, `member.updated`, `billing_order.created`, `billing_order.updated`, `billing_order.paid`, `billing_order.cancelled`, `payment.received`, `payment.voided`, `promise.created`, `promise.kept`, `promise.broken`, `message.sent`, `message.received` | sí |  |
| `description` | string |  |  |

**Respuestas**

- `201` OK · `data`: [WebhookEndpointCreated](#webhookendpointcreated)
- `401` Missing or invalid credentials
- `422` Validation error
- `429` Rate limit exceeded

### DELETE /v1/webhook-endpoints/{id}

Delete a webhook endpoint (its pending deliveries are dropped).

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |

Acepta `Idempotency-Key`.

**Respuestas**

- `200` OK · `data`: [Deleted](#deleted)
- `401` Missing or invalid credentials
- `404` Not found
- `429` Rate limit exceeded

### GET /v1/webhook-events

List emitted events (newest first).

Events are recorded only while the organization has at least one active endpoint subscribed to that type.

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `type` | query | string |  |  |
| `created_since` | query | string (date-time) |  | Only records created at or after this instant (ISO-8601). Ordering becomes `created_at` ascending. |
| `limit` | query | integer |  |  |
| `cursor` | query | string |  | `next_cursor` from the previous page. |
| `offset` | query | integer |  | Legacy; prefer `cursor`. |
| `count` | query | boolean |  | Include `pagination.total`. |

**Respuestas**

- `200` OK · `data`: array de [WebhookEvent](#webhookevent)
- `401` Missing or invalid credentials
- `429` Rate limit exceeded

### GET /v1/webhook-deliveries

List delivery attempts (newest first).

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `status` | query | string: `pending`, `delivered`, `failed` |  |  |
| `event_id` | query | string (uuid) |  |  |
| `endpoint_id` | query | string (uuid) |  |  |
| `limit` | query | integer |  |  |
| `cursor` | query | string |  | `next_cursor` from the previous page. |
| `offset` | query | integer |  | Legacy; prefer `cursor`. |
| `count` | query | boolean |  | Include `pagination.total`. |

**Respuestas**

- `200` OK · `data`: array de [WebhookDelivery](#webhookdelivery)
- `401` Missing or invalid credentials
- `429` Rate limit exceeded

### POST /v1/webhook-deliveries/{id}/retry

Retry a delivery now (pending or failed).

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `id` | path | string (uuid) | sí |  |

Acepta `Idempotency-Key`.

**Respuestas**

- `200` OK · `data`: [WebhookDelivery](#webhookdelivery)
- `401` Missing or invalid credentials
- `404` Not found
- `429` Rate limit exceeded

## Operations

### GET /v1/sequence-runs

List scheduled/sent collection messages (newest scheduled first).

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `state` | query | string |  |  |
| `member_id` | query | string (uuid) |  |  |
| `limit` | query | integer |  |  |
| `cursor` | query | string |  | `next_cursor` from the previous page. |
| `offset` | query | integer |  | Legacy; prefer `cursor`. |
| `count` | query | boolean |  | Include `pagination.total`. |

**Respuestas**

- `200` OK · `data`: array de [SequenceRun](#sequencerun)
- `401` Missing or invalid credentials
- `429` Rate limit exceeded

### GET /v1/flows

List the organization's flows.

**Respuestas**

- `200` OK · `data`: array de [Flow](#flow)
- `401` Missing or invalid credentials
- `429` Rate limit exceeded

### GET /v1/templates

List the organization's active templates.

**Respuestas**

- `200` OK · `data`: array de [Template](#template)
- `401` Missing or invalid credentials
- `429` Rate limit exceeded

### GET /v1/stats

Dashboard numbers — the same figures the app's Panel shows.

Every money figure comes from the same database function the app uses (`org_metricas_cobranza`), so the API and the Panel never disagree. `metrics` has the full set for the period (`from`/`to`, default: 1st of the current month to today, in the org's timezone): `collected` = money that came in by payment date (voided payments, credit-balance applications and future-dated rows excluded), `overdue`/`upcoming` = real balance (amount − paid − forgiven) of open installments due before / on-or-after today, `aging` by days late (1-9, 10-30, 31-60, 60+), `collection_rate` = of what came due in the period up to today, the share already paid. `metrics.capital_profit` splits the same period into principal and profit: `placed` (principal lent, by credit `start_date`), `collected.capital|interest|late_fee|other|unassigned|no_breakdown|to_credit_balance` (they add up exactly to `collected.total`, the same «collected» as above), `profit` = interest + late fee collected with its `margin` over what was collected with a breakdown, `outstanding` principal and interest (current / overdue) of open installments, and `forgiven` split into principal (a loss), interest, late fee and other. `coverage.status` is `none` when no installment has a breakdown: then every figure is 0 and `no_breakdown` carries the money. `portfolio` and `members` keep their old shape.

**Parámetros**

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `from` | query | string (date) |  | Period start (YYYY-MM-DD). Default: 1st of the current month. |
| `to` | query | string (date) |  | Period end (YYYY-MM-DD). Default: today (org timezone). |

**Respuestas**

- `200` OK · `data`: [Stats](#stats)
- `401` Missing or invalid credentials
- `429` Rate limit exceeded

## Objetos

### Error

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `error` | object |  |  |

### Pagination

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `limit` | integer |  |  |
| `has_more` | boolean |  |  |
| `next_cursor` | string, nullable |  |  |
| `offset` | integer |  | Only when paging with offset. |
| `total` | integer |  | Only with count=true. |

### Status

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `status` | string: `ok`, `degraded` |  |  |
| `database` | string |  |  |
| `database_latency_ms` | integer |  |  |
| `api_version` | string |  |  |
| `time` | string (date-time) |  |  |

### Member

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `id` | string (uuid) |  |  |
| `external_ref` | string, nullable |  |  |
| `full_name` | string |  |  |
| `phone_e164` | string, nullable |  |  |
| `email` | string, nullable |  |  |
| `state` | string |  |  |
| `identity` | object |  |  |
| `tags` | array de string |  |  |
| `notes` | string, nullable |  |  |
| `payment_reference` | string, nullable |  |  |
| `segment_id` | string, nullable |  |  |
| `promised_date` | string (date), nullable |  |  |
| `promise_status` | string |  |  |
| `promised_amount` | number, nullable |  |  |
| `promise_registered_at` | string (date-time), nullable |  |  |
| `credit_balance` | number |  |  |
| `do_not_contact` | boolean |  | Never message this member (withdraws anything queued). |
| `do_not_contact_reason` | string, nullable |  |  |
| `customer_since` | string (date), nullable |  | When it became your customer; `created_at` is when it entered Makatea. |
| `import_batch_id` | string (uuid), nullable |  |  |
| `created_at` | string (date-time) |  |  |
| `updated_at` | string (date-time) |  |  |

### BillingOrder

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `id` | string (uuid) |  |  |
| `member_id` | string (uuid) |  |  |
| `external_ref` | string, nullable |  |  |
| `amount` | number |  |  |
| `amount_paid` | number |  |  |
| `amount_forgiven` | number |  |  |
| `late_fee` | number |  |  |
| `balance` | number |  | amount − amount_paid − amount_forgiven (late fees are reported apart). |
| `currency` | string |  |  |
| `due_date` | string (date) |  |  |
| `period_label` | string, nullable |  |  |
| `status` | string: `pending`, `overdue`, `paid`, `cancelled` |  |  |
| `paid_at` | string (date-time), nullable |  |  |
| `paid_via` | string, nullable |  |  |
| `notes` | string, nullable |  |  |
| `forgiven_at` | string (date-time), nullable |  |  |
| `forgiven_reason` | string, nullable |  |  |
| `cancel_reason` | string, nullable |  | Why it was cancelled (`DELETE …?reason=`). |
| `created_at` | string (date-time) |  |  |
| `updated_at` | string (date-time) |  |  |
| `credit_id` | string (uuid), nullable |  | Credit (loan) it belongs to. |
| `capital_portion` | number, nullable |  | Principal part of the installment (null = no breakdown). |
| `interest_portion` | number, nullable |  | Interest part of the installment. |
| `breakdown` | object, nullable |  | null when the installment has no breakdown (figures are never invented). |

### Payment

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `id` | string (uuid) |  |  |
| `billing_order_id` | string (uuid) |  |  |
| `member_id` | string (uuid) |  |  |
| `amount` | number |  |  |
| `kind` | string: `payment`, `reversal` |  |  |
| `method` | string, nullable |  |  |
| `reference` | string, nullable |  |  |
| `notes` | string, nullable |  |  |
| `voids_payment_id` | string (uuid), nullable |  |  |
| `voided` | boolean |  |  |
| `paid_at` | string (date-time) |  | When the money was received. |
| `created_at` | string (date-time) |  |  |
| `recorded_at` | string (date-time) |  | When the entry was recorded in Makatea. |
| `allocation` | object, nullable |  | How this entry was applied to its installment. Adds up to `amount`. `unassigned` = beyond the installment's parts or, with rule `al_liquidar`, a partial payment not yet recognized (the settling payment then carries it negative). null when the installment has no breakdown. |

### PaymentResult

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `order_id` | string (uuid) |  |  |
| `amount_paid` | number |  |  |
| `status` | string |  |  |
| `fully_paid` | boolean |  |  |
| `excess_applied_to_other_orders` | number |  |  |
| `credit_added` | number |  |  |
| `already_applied` | boolean |  |  |
| `payment` | [Payment](#payment) |  |  |
| `order` | [BillingOrder](#billingorder) |  |  |

### PromiseState

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `member_id` | string (uuid) |  |  |
| `status` | string: `none`, `active`, `kept`, `broken`, `cancelled` |  |  |
| `current` | object, nullable |  |  |
| `history` | array de object |  |  |

### Credit

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `member_id` | string (uuid) |  |  |
| `credit_balance` | number |  |  |
| `ledger` | array de object |  |  |

### CreditApplied

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `member_id` | string (uuid) |  |  |
| `applied` | number |  |  |
| `credit_balance` | number |  |  |

### Interaction

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `id` | string (uuid) |  |  |
| `channel` | string |  |  |
| `direction` | string: `inbound`, `outbound` |  |  |
| `text` | string, nullable |  |  |
| `attachment` | string, nullable |  |  |
| `classification` | string, nullable |  |  |
| `sequence_run_id` | string (uuid), nullable |  |  |
| `simulated` | boolean |  |  |
| `occurred_at` | string (date-time) |  |  |

### MessageResult

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `id` | string (uuid) |  |  |
| `object` | string: `interaction`, `sequence_run` |  |  |
| `status` | string: `sent`, `simulated`, `queued`, `queued_simulated` |  |  |
| `channel` | string |  |  |
| `member_id` | string (uuid) |  |  |

### ConversationImportResult

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `inserted` | integer |  |  |
| `skipped_duplicates` | integer |  |  |
| `unmatched` | array de object |  |  |
| `errors` | array de object |  |  |

### Document

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `id` | string (uuid) |  |  |
| `member_id` | string (uuid) |  |  |
| `name` | string |  |  |
| `type` | string |  |  |
| `mime_type` | string |  |  |
| `size` | integer |  |  |
| `external_ref` | string, nullable |  |  |
| `description` | string, nullable |  |  |
| `storage_path` | string |  |  |
| `download_url` | string, nullable |  | Signed URL, valid 1 hour. |
| `download_url_expires_at` | string (date-time), nullable |  |  |
| `created_at` | string (date-time) |  |  |

### Deleted

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `id` | string (uuid) |  |  |
| `deleted` | boolean |  |  |

### PaymentLink

`object`

### WebhookEndpoint

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `id` | string (uuid) |  |  |
| `url` | string |  |  |
| `events` | array de string |  |  |
| `active` | boolean |  |  |
| `description` | string, nullable |  |  |
| `created_at` | string (date-time) |  |  |
| `updated_at` | string (date-time) |  |  |

### WebhookEndpointCreated

`object`

### WebhookEvent

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `id` | string (uuid) |  |  |
| `type` | string: `member.created`, `member.updated`, `billing_order.created`, `billing_order.updated`, `billing_order.paid`, `billing_order.cancelled`, `payment.received`, `payment.voided`, `promise.created`, `promise.kept`, `promise.broken`, `message.sent`, `message.received` |  |  |
| `created_at` | string (date-time) |  |  |
| `data` | object |  |  |

### WebhookDelivery

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `id` | string (uuid) |  |  |
| `event_id` | string (uuid) |  |  |
| `event_type` | string |  |  |
| `endpoint_id` | string (uuid) |  |  |
| `status` | string: `pending`, `delivered`, `failed` |  |  |
| `attempts` | integer |  |  |
| `next_attempt_at` | string (date-time), nullable |  |  |
| `last_status` | integer, nullable |  |  |
| `last_error` | string, nullable |  |  |
| `created_at` | string (date-time) |  |  |
| `delivered_at` | string (date-time), nullable |  |  |

### SequenceRun

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `id` | string (uuid) |  |  |
| `member_id` | string (uuid) |  |  |
| `billing_order_id` | string (uuid), nullable |  |  |
| `template_id` | string (uuid) |  |  |
| `state` | string |  |  |
| `channel` | string |  |  |
| `scheduled_at` | string (date-time) |  |  |
| `outcome` | string, nullable |  |  |
| `attempts` | integer |  |  |

### Flow

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `id` | string (uuid) |  |  |
| `name` | string |  |  |
| `status` | string |  |  |
| `steps` | integer |  |  |

### Template

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `id` | string (uuid) |  |  |
| `name` | string |  |  |
| `channel` | string |  |  |
| `trigger_type` | string |  |  |
| `version` | integer |  |  |
| `subject` | string, nullable |  |  |

### Stats

`object`

### Loan

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `id` | string (uuid) |  |  |
| `member_id` | string (uuid) |  |  |
| `member_name` | string, nullable |  |  |
| `external_ref` | string, nullable |  | Your folio. |
| `principal` | number |  | Money lent. |
| `total_amount` | number, nullable |  |  |
| `periods` | integer, nullable |  |  |
| `frequency` | string, nullable |  |  |
| `start_date` | string (date) |  | Date the money was lent. |
| `status` | string: `active`, `liquidated`, `cancelled` |  |  |
| `notes` | string, nullable |  |  |
| `created_at` | string (date-time) |  |  |
| `updated_at` | string (date-time) |  |  |
| `totals` | object |  |  |

### LoanDetail

`object`

### AllocationRule

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `rule` | string |  |  |
| `rules` | array de string |  |  |
| `orders_reapplied` | integer |  |  |

