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

# Crear sesión de cobro

> Crea una sesión de cobro y retorna la URL de pago a la que debes redirigir al usuario. Si se envía un `external_id` que ya existe para tu API key, retorna la sesión original con HTTP 200 (idempotencia).

## ¿Para qué se usa?

Crea una sesión de pago y retorna la `payment_url` a la que debes redirigir al usuario para que complete el pago. Tupana se encarga de procesar el pago, emitir el DTE (si corresponde) y notificarte vía webhook.

## Qué hace

* Crea una sesión de cobro en estado `pending`.
* Retorna una URL de pago única con tiempo de expiración.
* Al confirmarse el pago:
  * **Modo emisión** (default): Tupana emite automáticamente el DTE si `auto_issue=true`.
  * **Modo cobro directo** (con `document_ids`): los documentos referenciados se marcan como pagados; no hay emisión de DTE.

## Modos de uso

| Modo              | Cuándo usarlo                                                             | Campos requeridos                                     |
| ----------------- | ------------------------------------------------------------------------- | ----------------------------------------------------- |
| **Emisión**       | Cobrás un servicio nuevo y necesitás emitir un DTE al confirmarse el pago | `amount`, `target_dte_type`, `dte_recipient`, `items` |
| **Cobro directo** | Querés cobrar uno o más DTEs ya emitidos                                  | `document_ids`                                        |

Los modos son mutuamente excluyentes: si envías `document_ids`, no puedes incluir `amount`, `target_dte_type`, `dte_recipient` ni `items` (Tupana retorna `CANNOT_MIX_MODES`).

### Ejemplo: modo cobro directo

```json theme={null}
{
  "recipient_id": 789,
  "redirect_url": "https://tuapp.com/confirmation",
  "document_ids": [9871, 9872],
  "expires_in_minutes": 30,
  "external_id": "collection_session_001",
  "metadata": {
    "campaign": "cobranza-abril"
  }
}
```

Reglas de validación en este modo:

* Todos los `document_ids` deben pertenecer al `recipient_id` (su `sender` debe coincidir).
* Todos deben compartir el mismo pagador (`receiver`) — un solo cobro = un solo pagador.
* Solo se aceptan tipos de DTE cobrables: `33`, `34`, `39`, `41`, `80`, `110`.
* Ninguno puede estar ya pagado.
* El monto del cobro se calcula automáticamente como la suma de los `amount_with_iva` de los documentos.
* **Si usas una API key de sandbox**, el `folio` de cada documento debe empezar con `SANDBOX`. Esto evita que cobres documentos reales con una key de prueba — si la regla no se cumple, el endpoint retorna `DOCUMENT_NOT_SANDBOX`.

En modo cobro directo no hay emisión posterior, por lo que `PATCH /v1/payment-requests/{id}/` con `status=issued` retorna `NOT_APPLICABLE`.

## Ejemplos de uso

* **Cobro de una sesión de salud mental**: Un paciente agenda una consulta y paga antes de la atención.
* **Pago de servicio**: Un cliente de una plataforma de servicios paga por una prestación.
* **Cobro recurrente manual**: Tu plataforma crea sesiones de cobro para cada período.

## Consideraciones importantes

### Idempotencia con `external_id`

Si envías el mismo `external_id` en dos requests distintos, Tupana retorna la sesión original sin crear una nueva. Usa esto para evitar sesiones duplicadas si tu request se reintenta.

### Checkout con o sin portal

El parámetro `skip_portal` controla la experiencia de pago:

| Valor             | Comportamiento                                                                                              |
| ----------------- | ----------------------------------------------------------------------------------------------------------- |
| `false` (default) | El usuario ve una pantalla de Tupana con el detalle del cobro antes de pagar.                               |
| `true`            | El usuario es redirigido directo a Transbank. Útil cuando el contexto del cobro ya es claro en tu interfaz. |

### Redirect post-pago

Si enviaste `redirect_url`, Tupana redirige al usuario a esa URL después del pago con parámetros de estado:

```
# Pago exitoso
https://tuapp.com/confirmation?payment_request_id=pr_abc123&status=paid

# Pago fallido
https://tuapp.com/confirmation?payment_request_id=pr_abc123&status=failed&error_code=CARD_DECLINED

# Sesión vencida
https://tuapp.com/confirmation?payment_request_id=pr_abc123&status=expired
```

<Warning>
  No uses los parámetros del redirect como fuente de verdad del estado del pago. Un usuario podría modificarlos en el browser. Siempre verifica el estado real vía el webhook recibido o consultando `GET /v1/payment-requests/{id}/`.
</Warning>

### Emisión automática de DTE (`auto_issue`)

Por defecto (`auto_issue=true`), Tupana emite el DTE automáticamente al confirmarse el pago. Si necesitas controlar el momento de la emisión, envía `auto_issue=false` y emite el DTE cuando lo necesites con [`PATCH /v1/payment-requests/{id}/`](/api-reference/payment-requests/issue) enviando `{"status": "issued"}`.

Esto es útil cuando quieres validar datos adicionales antes de emitir la boleta o factura.

### Tipos de DTE soportados

| `target_dte_type` | Documento emitido          |
| ----------------- | -------------------------- |
| `39`              | Boleta afecta electrónica  |
| `41`              | Boleta exenta electrónica  |
| `33`              | Factura afecta electrónica |

El destinatario debe estar habilitado para emitir ese tipo de documento en Tupana. Si no lo está, la sesión será rechazada al crearla.


## OpenAPI

````yaml api-reference/openapi-payment-requests.json POST /payment-requests/
openapi: 3.0.0
info:
  title: Tupana API - Recaudación
  description: >-
    API de Cobros — Crea sesiones de pago, procesa cobros y emite DTE
    automáticamente.
  version: 1.0.0
servers:
  - url: https://api.tupana.ai/v1
    description: Servidor de producción
security:
  - apiKeyAuth: []
paths:
  /payment-requests/:
    post:
      tags:
        - Cobros
      summary: Crear sesión de cobro
      description: >-
        Crea una sesión de cobro y retorna la URL de pago a la que debes
        redirigir al usuario. Si se envía un `external_id` que ya existe para tu
        API key, retorna la sesión original con HTTP 200 (idempotencia).
      operationId: createPaymentRequest
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PaymentRequestCreate'
            examples:
              emission_mode:
                summary: Modo emisión (cobrar + emitir un DTE nuevo)
                value:
                  amount: 50000
                  currency: CLP
                  recipient_id: 789
                  redirect_url: https://tuapp.com/confirmation
                  target_dte_type: 39
                  dte_recipient:
                    rut: 11111111-1
                    name: Juan Soto
                    email: juan@ejemplo.com
                  items:
                    - description: Sesión de psicoterapia
                      quantity: 1
                      unit_price: 50000
                  expires_in_minutes: 30
                  skip_portal: false
                  auto_issue: true
                  external_id: booking_abc123
                  metadata:
                    patient_name: Juan Soto
                    appointment_start_time: '2026-04-15T10:00:00-04:00'
              direct_charge_mode:
                summary: Modo cobro directo (cobrar documentos existentes)
                value:
                  recipient_id: 789
                  redirect_url: https://tuapp.com/confirmation
                  document_ids:
                    - 9871
                    - 9872
                  expires_in_minutes: 30
                  external_id: collection_session_001
                  metadata:
                    campaign: cobranza-abril
      responses:
        '200':
          description: Sesión existente retornada (idempotencia por `external_id`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentRequestResponse'
        '201':
          description: Sesión de cobro creada exitosamente
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentRequestResponse'
              example:
                payment_request_id: 1042
                status: pending
                payment_url: https://www.tupana.ai/pagos/tok_xyz
                expires_at: '2026-04-06T12:30:00Z'
                created_at: '2026-04-06T12:00:00Z'
                amount: 50000
                target_dte_type: '39'
                auto_issue: true
                external_id: booking_abc123
                metadata: {}
                issued_document_id: null
                document_ids: []
        '400':
          description: Parámetros inválidos
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                recipient_not_found:
                  value:
                    error_code: RECIPIENT_NOT_FOUND
                    message: Recipient with id 789 not found.
                missing_banking_info:
                  value:
                    error_code: RECIPIENT_MISSING_BANKING_INFO
                    message: Recipient does not have a registered bank account.
                invalid_dte_type:
                  value:
                    error_code: INVALID_TARGET_DTE_TYPE
                    message: target_dte_type must be one of [33, 39, 41].
                cannot_mix_modes:
                  value:
                    error_code: CANNOT_MIX_MODES
                    message: document_ids cannot be combined with amount, items.
                empty_document_ids:
                  value:
                    error_code: EMPTY_DOCUMENT_IDS
                    message: document_ids must contain at least one document.
                document_not_found:
                  value:
                    error_code: DOCUMENT_NOT_FOUND
                    message: 'Documents not found: [9999].'
                document_not_owned_by_recipient:
                  value:
                    error_code: DOCUMENT_NOT_OWNED_BY_RECIPIENT
                    message: Document 9871 is not owned by recipient 789.
                mixed_payers:
                  value:
                    error_code: MIXED_PAYERS
                    message: >-
                      All documents in a payment request must share the same
                      receiver.
                document_already_paid:
                  value:
                    error_code: DOCUMENT_ALREADY_PAID
                    message: Document 9871 is already paid.
                document_not_payable:
                  value:
                    error_code: DOCUMENT_NOT_PAYABLE
                    message: Document 9871 has a non-payable DTE type 61.
                document_not_sandbox:
                  value:
                    error_code: DOCUMENT_NOT_SANDBOX
                    message: >-
                      Document 9871 is not a sandbox document (folio must start
                      with 'SANDBOX' when using a sandbox API key).
        '401':
          description: API key inválida o no proporcionada
      security:
        - apiKeyAuth: []
components:
  schemas:
    PaymentRequestCreate:
      type: object
      required:
        - recipient_id
      description: >-
        La sesión de cobro soporta dos modos:


        - **Modo emisión** (default): Tupana cobra y emite un DTE nuevo.
        Requiere `amount`, `target_dte_type`, `dte_recipient` y `items`.

        - **Modo cobro directo**: cobra documentos ya existentes. Requiere
        `document_ids` y rechaza `amount`, `target_dte_type`, `dte_recipient` e
        `items`.
      properties:
        amount:
          type: integer
          description: >-
            Monto en CLP, sin decimales. Requerido en modo emisión. **No
            permitido si envías `document_ids`** — el monto se calcula sumando
            los `amount_with_iva` de los documentos.
          example: 50000
        currency:
          type: string
          description: Moneda. Siempre `CLP`. Si se omite, se usa `CLP` por defecto.
          enum:
            - CLP
          default: CLP
        recipient_id:
          type: integer
          description: >-
            ID numérico del destinatario en Tupana. Debe tener cuenta bancaria
            registrada. En modo cobro directo, todos los documentos
            referenciados deben pertenecer a este destinatario (es decir, su
            `sender` debe coincidir).
          example: 789
        redirect_url:
          type: string
          format: uri
          description: >-
            URL a la que Tupana redirige al usuario después del pago. Si se
            omite, el usuario permanece en el portal de pago.
          example: https://tuapp.com/confirmation
        target_dte_type:
          type: integer
          description: >-
            Tipo de DTE a emitir al confirmarse el pago. `39` = boleta afecta,
            `41` = boleta exenta, `33` = factura afecta. Requerido en modo
            emisión. **No permitido si envías `document_ids`**.
          enum:
            - 33
            - 39
            - 41
          example: 39
        dte_recipient:
          $ref: '#/components/schemas/DteRecipient'
        items:
          type: array
          description: >-
            Ítems del DTE. La suma de `quantity × unit_price` debe coincidir con
            `amount`. Requerido en modo emisión. **No permitido si envías
            `document_ids`**.
          items:
            $ref: '#/components/schemas/PaymentRequestItem'
        document_ids:
          type: array
          description: >-
            IDs de documentos existentes a cobrar (modo cobro directo). Todos
            deben pertenecer al `recipient_id`, compartir el mismo receptor
            (pagador), ser de tipo cobrable (33, 34, 39, 41, 80, 110) y no estar
            pagados. Si envías este campo, omite `amount`, `target_dte_type`,
            `dte_recipient` e `items`.


            Cuando uses una API key de sandbox, cada documento debe tener un
            folio que comience con `SANDBOX` (de lo contrario el endpoint
            retorna `DOCUMENT_NOT_SANDBOX`).
          items:
            type: integer
            minimum: 1
          minItems: 1
          example:
            - 9871
            - 9872
        expires_in_minutes:
          type: integer
          description: 'Minutos hasta que vence la sesión. Por defecto: `30`.'
          default: 30
          example: 30
        skip_portal:
          type: boolean
          description: >-
            Si `true`, redirige directo a Transbank sin mostrar la pantalla de
            Tupana. Por defecto: `false`.
          default: false
        auto_issue:
          type: boolean
          description: >-
            Si `true` (default), Tupana emite el DTE automáticamente al
            confirmarse el pago. Si `false`, debes emitirlo manualmente con
            `PATCH /v1/payment-requests/{id}/` enviando `{"status": "issued"}`.
            Solo aplica en modo emisión; ignorado en modo cobro directo (no hay
            DTE que emitir).
          default: true
        external_id:
          type: string
          description: >-
            ID propio para idempotencia. Si ya existe una sesión con ese
            `external_id` para tu API key, se retorna la sesión original con
            HTTP 200.
          example: booking_abc123
        metadata:
          type: object
          description: >-
            Datos adicionales de libre formato. Tupana los guarda y devuelve en
            webhooks y en el GET.
          additionalProperties: true
    PaymentRequestResponse:
      type: object
      properties:
        payment_request_id:
          type: integer
          description: ID único de la sesión de cobro.
          example: 1042
        status:
          type: string
          enum:
            - pending
            - paid
            - issued
            - failed
            - expired
            - refunded
          example: pending
        payment_url:
          type: string
          format: uri
          description: URL a la que debes redirigir al usuario para que complete el pago.
          example: https://www.tupana.ai/pagos/tok_xyz
        expires_at:
          type: string
          format: date-time
          description: Fecha y hora en que vence la sesión (UTC).
          example: '2026-04-06T12:30:00Z'
        created_at:
          type: string
          format: date-time
          description: Fecha y hora de creación de la sesión (UTC).
          example: '2026-04-06T12:00:00Z'
        amount:
          type: number
          description: Monto del cobro en CLP.
          example: 50000
        target_dte_type:
          type: string
          nullable: true
          description: >-
            Tipo de DTE que se emitirá al pagarse. `null` en modo cobro directo
            (no hay DTE a emitir).
          example: '39'
        auto_issue:
          type: boolean
          nullable: true
          description: >-
            Si el DTE se emite automáticamente al confirmarse el pago. `null` en
            modo cobro directo.
          example: true
        external_id:
          type: string
          nullable: true
          description: Tu ID propio para esta sesión.
          example: booking_abc123
        metadata:
          type: object
          additionalProperties: true
          description: Datos adicionales que enviaste al crear la sesión.
        issued_document_id:
          type: integer
          nullable: true
          description: >-
            ID del DTE emitido. `null` mientras la sesión no haya emitido el
            DTE. Siempre `null` en modo cobro directo.
          example: 9871
        document_ids:
          type: array
          items:
            type: integer
          description: >-
            IDs de los documentos que esta sesión cobra. Lista vacía en modo
            emisión (donde el DTE se emite al pagarse).
          example:
            - 9871
            - 9872
    ErrorResponse:
      type: object
      properties:
        error_code:
          type: string
          example: RECIPIENT_NOT_FOUND
        message:
          type: string
          example: Recipient with id 789 not found.
    DteRecipient:
      type: object
      required:
        - rut
        - name
      properties:
        rut:
          type: string
          description: RUT del receptor del DTE.
          example: 11111111-1
        name:
          type: string
          description: Nombre o razón social del receptor.
          example: Juan Soto
        email:
          type: string
          format: email
          description: Email al que se enviará el DTE. Opcional.
          example: juan@ejemplo.com
    PaymentRequestItem:
      type: object
      required:
        - description
        - quantity
        - unit_price
      properties:
        description:
          type: string
          description: Descripción del ítem.
          example: Sesión de psicoterapia
        quantity:
          type: number
          description: Cantidad.
          example: 1
        unit_price:
          type: integer
          description: Precio unitario en CLP.
          example: 50000
  securitySchemes:
    apiKeyAuth:
      type: apiKey
      in: header
      name: Authorization
      description: 'API Key para autenticación. Formato: `Api-Key YOUR-API-KEY`'

````