{
  "openapi": "3.0.3",
  "info": {
    "title": "ControlPOS · API de integración de tickets",
    "version": "2026-09-04",
    "description": "Contrato de integración del backend Java. La API no ejecuta pagos. EMV y autorización del voucher son simulados. Solicitar entorno y comercio de pruebas al administrador. El token de terminal solo autoriza POST /api/tickets."
  },
  "servers": [
    {
      "url": "https://ticket.control-pos.com",
      "description": "Servicio publicado. Usar comercio de pruebas autorizado."
    }
  ],
  "paths": {
    "/api/terminals/enroll": {
      "post": {
        "operationId": "enrollTerminal",
        "summary": "Canjear código de alta una sola vez",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EnrollRequest"
              },
              "example": {
                "enrollmentCode": "CPE-REEMPLAZAR",
                "serial": "MARCA-MODELO-0001",
                "tokenLabel": "Modelo · Sucursal de pruebas"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Terminal registrada y token (guardar una sola vez)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Enrollment"
                }
              }
            }
          },
          "400": {
            "description": "Error; revisar code, error y traceId. En 429 respetar Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "El código de alta es obligatorio",
                  "code": "datos_invalidos",
                  "traceId": "ejemplo-traza-005",
                  "details": [
                    {
                      "field": "enrollmentCode",
                      "message": "El código de alta es obligatorio"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Error; revisar code, error y traceId. En 429 respetar Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error; revisar code, error y traceId. En 429 respetar Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error; revisar code, error y traceId. En 429 respetar Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error; revisar code, error y traceId. En 429 respetar Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error; revisar code, error y traceId. En 429 respetar Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Demasiadas peticiones. Reintenta en 30 segundos.",
                  "code": "limite_excedido",
                  "traceId": "ejemplo-traza-004"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos antes del siguiente intento",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          },
          "500": {
            "description": "Error; revisar code, error y traceId. En 429 respetar Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error; revisar code, error y traceId. En 429 respetar Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/tickets": {
      "post": {
        "operationId": "createTicket",
        "summary": "Emitir comprobante después del pago",
        "security": [
          {
            "terminalToken": []
          },
          {
            "bearerToken": []
          }
        ],
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "description": "Persistir junto al cuerpo antes de enviar. Misma clave y cuerpo para reintentar la misma operación.",
            "schema": {
              "type": "string",
              "maxLength": 120
            }
          },
          {
            "in": "header",
            "name": "X-Request-Id",
            "description": "Identificador de correlación opcional",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateTicket"
              },
              "examples": {
                "efectivo": {
                  "summary": "100 de consumo + 10 de propina = 110 MXN",
                  "value": {
                    "terminalSerial": "MARCA-MODELO-0001",
                    "currency": "MXN",
                    "items": [
                      {
                        "name": "Consumo",
                        "qty": 1,
                        "price": 100
                      }
                    ],
                    "tip": 10,
                    "transactionType": "venta",
                    "capture": {
                      "orderReference": "ORDEN-0001",
                      "paymentMethod": "efectivo"
                    }
                  }
                },
                "tarjeta": {
                  "summary": "2 × 45 + 80 + 17 de propina = 187 MXN",
                  "value": {
                    "terminalSerial": "MARCA-MODELO-0001",
                    "currency": "MXN",
                    "items": [
                      {
                        "name": "Café americano",
                        "qty": 2,
                        "price": 45
                      },
                      {
                        "name": "Sándwich",
                        "qty": 1,
                        "price": 80
                      }
                    ],
                    "tip": 17,
                    "transactionType": "venta",
                    "card": {
                      "brand": "visa",
                      "type": "debito",
                      "last4": "1234",
                      "entryMode": "contactless"
                    },
                    "capture": {
                      "orderReference": "ORDEN-0002",
                      "cashier": "CAJA-01",
                      "operationNumber": "000123",
                      "batchNumber": "001",
                      "affiliation": "AFILIACION-DE-PRUEBA",
                      "paymentMethod": "tarjeta",
                      "capturedAt": "2026-09-04T18:00:00Z",
                      "transaccionId": "3f2a9c1e-7b4d-4e8f-9a6b-1c2d3e4f5a6b",
                      "rrn": "000012345678",
                      "location": { "latitude": 25.651234, "longitude": -100.289876, "label": "Av. Garza Sada, Monterrey" }
                    },
                    "receiptTemplate": "consumo-restaurante"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reintento: mismo ticket",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ticket"
                }
              }
            }
          },
          "201": {
            "description": "Ticket nuevo",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ticket"
                }
              }
            }
          },
          "400": {
            "description": "Error; revisar code, error y traceId. En 429 respetar Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "last4 deben ser exactamente 4 digitos",
                  "code": "datos_invalidos",
                  "traceId": "ejemplo-traza-002",
                  "details": [
                    {
                      "field": "card.last4",
                      "message": "last4 deben ser exactamente 4 digitos"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Error; revisar code, error y traceId. En 429 respetar Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error; revisar code, error y traceId. En 429 respetar Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error; revisar code, error y traceId. En 429 respetar Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error; revisar code, error y traceId. En 429 respetar Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "idempotencia": {
                    "summary": "Misma Idempotency-Key con otro cuerpo",
                    "value": {
                      "error": "Esa Idempotency-Key ya se uso con un cuerpo diferente. Usa una llave nueva.",
                      "code": "idempotencia_cuerpo_distinto",
                      "traceId": "ejemplo-traza-003"
                    }
                  },
                  "autorizacion": {
                    "summary": "Venta con tarjeta sin autorización del switch (reintentable)",
                    "value": {
                      "error": "El switch aun no ha publicado la autorizacion 3f2a9c1e-7b4d-4e8f-9a6b-1c2d3e4f5a6b; reintenta.",
                      "code": "AUTHORIZATION_NOT_FOUND",
                      "traceId": "ejemplo-traza-006"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Error; revisar code, error y traceId. En 429 respetar Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Demasiadas peticiones. Reintenta en 30 segundos.",
                  "code": "limite_excedido",
                  "traceId": "ejemplo-traza-004"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos antes del siguiente intento",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          },
          "500": {
            "description": "Error; revisar code, error y traceId. En 429 respetar Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error; revisar code, error y traceId. En 429 respetar Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/t/{id}": {
      "get": {
        "operationId": "viewTicket",
        "summary": "Vista pública del comprobante",
        "security": [],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Página del ticket",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Ticket no disponible"
          },
          "429": {
            "description": "Error; revisar code, error y traceId. En 429 respetar Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos antes del siguiente intento",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          }
        }
      }
    },
    "/t/{id}/pdf": {
      "get": {
        "operationId": "downloadTicket",
        "summary": "PDF público del comprobante",
        "security": [],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Comprobante PDF",
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "Ticket no disponible"
          },
          "429": {
            "description": "Error; revisar code, error y traceId. En 429 respetar Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Segundos antes del siguiente intento",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "terminalToken": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Terminal-Token"
      },
      "bearerToken": {
        "type": "http",
        "scheme": "bearer"
      }
    },
    "schemas": {
      "EnrollRequest": {
        "type": "object",
        "properties": {
          "enrollmentCode": {
            "type": "string",
            "maxLength": 40,
            "minLength": 1
          },
          "serial": {
            "type": "string",
            "maxLength": 64,
            "minLength": 1
          },
          "tokenLabel": {
            "type": "string",
            "maxLength": 120
          },
          "address": {
            "type": "string",
            "maxLength": 300
          },
          "rfc": {
            "type": "string",
            "maxLength": 20
          },
          "city": {
            "type": "string",
            "maxLength": 120
          }
        },
        "required": [
          "enrollmentCode",
          "serial"
        ]
      },
      "Enrollment": {
        "type": "object",
        "properties": {
          "terminal": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "serial": {
                "type": "string"
              },
              "merchant_name": {
                "type": "string"
              },
              "unassigned": {
                "type": "boolean"
              }
            }
          },
          "token_id": {
            "type": "string",
            "format": "uuid"
          },
          "token": {
            "type": "string"
          },
          "token_expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        },
        "required": [
          "terminal",
          "token_id",
          "token",
          "token_expires_at"
        ]
      },
      "Item": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 120,
            "minLength": 1
          },
          "qty": {
            "type": "number",
            "minimum": 0,
            "exclusiveMinimum": true,
            "maximum": 999999.999,
            "multipleOf": 0.001
          },
          "price": {
            "type": "number",
            "minimum": 0,
            "maximum": 9999999999.99,
            "multipleOf": 0.01
          }
        },
        "required": [
          "name",
          "qty",
          "price"
        ]
      },
      "Card": {
        "type": "object",
        "properties": {
          "brand": {
            "type": "string",
            "enum": [
              "visa",
              "mastercard",
              "amex",
              "card"
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "credito",
              "debito"
            ]
          },
          "last4": {
            "type": "string",
            "pattern": "^[0-9]{4}$"
          },
          "entryMode": {
            "type": "string",
            "enum": [
              "chip",
              "contactless",
              "swipe",
              "manual"
            ],
            "default": "chip"
          }
        },
        "required": [
          "brand",
          "type",
          "last4"
        ]
      },
      "Capture": {
        "type": "object",
        "properties": {
          "orderReference": {
            "type": "string",
            "maxLength": 64
          },
          "cashier": {
            "type": "string",
            "maxLength": 80
          },
          "operationNumber": {
            "type": "string",
            "maxLength": 32
          },
          "batchNumber": {
            "type": "string",
            "maxLength": 32
          },
          "affiliation": {
            "type": "string",
            "maxLength": 32
          },
          "installments": {
            "type": "integer",
            "minimum": 2,
            "maximum": 48
          },
          "paymentMethod": {
            "type": "string",
            "enum": [
              "efectivo",
              "tarjeta",
              "transferencia",
              "otro"
            ]
          },
          "capturedAt": {
            "type": "string",
            "format": "date-time"
          },
          "transaccionId": {
            "type": "string",
            "format": "uuid",
            "description": "Id de plataforma de la autorización del switch (transactionId del evento TransaccionAutorizada). Obligatorio en ventas con tarjeta: sin él el servidor responde 409 AUTHORIZATION_NOT_FOUND."
          },
          "rrn": {
            "type": "string",
            "maxLength": 12,
            "description": "RRN (DE-37) de esa autorización. Obligatorio en ventas con tarjeta."
          },
          "location": {
            "type": "object",
            "description": "Opcional. Dónde se cobró (GPS de la terminal, WGS84). Se imprime en el voucher con liga al mapa. Sin este bloque el ticket se emite igual.",
            "required": ["latitude", "longitude"],
            "properties": {
              "latitude": { "type": "number", "minimum": -90, "maximum": 90 },
              "longitude": { "type": "number", "minimum": -180, "maximum": 180 },
              "label": { "type": "string", "maxLength": 120, "description": "Dirección corta resuelta por la terminal." }
            }
          }
        }
      },
      "CreateTicket": {
        "type": "object",
        "properties": {
          "terminalSerial": {
            "type": "string",
            "maxLength": 64,
            "minLength": 1
          },
          "currency": {
            "type": "string",
            "pattern": "^[A-Z]{3}$",
            "default": "MXN"
          },
          "items": {
            "type": "array",
            "minItems": 1,
            "maxItems": 200,
            "items": {
              "$ref": "#/components/schemas/Item"
            }
          },
          "tip": {
            "type": "number",
            "minimum": 0,
            "maximum": 9999999999.99,
            "multipleOf": 0.01,
            "default": 0
          },
          "transactionType": {
            "type": "string",
            "enum": [
              "venta",
              "devolucion",
              "cancelacion",
              "check_in",
              "checkout"
            ],
            "default": "venta"
          },
          "card": {
            "$ref": "#/components/schemas/Card"
          },
          "capture": {
            "$ref": "#/components/schemas/Capture"
          },
          "receiptTemplate": {
            "type": "string",
            "maxLength": 64
          }
        },
        "required": [
          "terminalSerial",
          "items"
        ]
      },
      "Ticket": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "terminal_id": {
            "type": "string",
            "format": "uuid"
          },
          "folio": {
            "type": "integer",
            "format": "int64"
          },
          "amount": {
            "type": "string",
            "example": "110.00"
          },
          "base_amount": {
            "type": "string",
            "example": "100.00"
          },
          "tip_amount": {
            "type": "string",
            "example": "10.00"
          },
          "currency": {
            "type": "string"
          },
          "transaction_type": {
            "type": "string"
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "qty": {
                  "type": "string"
                },
                "price": {
                  "type": "string"
                }
              }
            }
          },
          "receipt_svg": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "card_brand": {
            "type": "string",
            "nullable": true
          },
          "card_type": {
            "type": "string",
            "nullable": true
          },
          "card_last4": {
            "type": "string",
            "nullable": true
          },
          "entry_mode": {
            "type": "string",
            "nullable": true
          },
          "emv_aid": {
            "type": "string",
            "nullable": true
          },
          "emv_app_label": {
            "type": "string",
            "nullable": true
          },
          "emv_arqc": {
            "type": "string",
            "nullable": true
          },
          "emv_tvr": {
            "type": "string",
            "nullable": true
          },
          "auth_code": {
            "type": "string",
            "nullable": true
          },
          "order_reference": {
            "type": "string",
            "nullable": true
          },
          "cashier": {
            "type": "string",
            "nullable": true
          },
          "operation_number": {
            "type": "string",
            "nullable": true
          },
          "batch_number": {
            "type": "string",
            "nullable": true
          },
          "affiliation": {
            "type": "string",
            "nullable": true
          },
          "payment_method": {
            "type": "string",
            "nullable": true
          },
          "receipt_template_code": {
            "type": "string",
            "nullable": true
          },
          "issued_source": {
            "type": "string",
            "nullable": true
          },
          "issued_by": {
            "type": "string",
            "nullable": true
          },
          "issued_trace_id": {
            "type": "string",
            "nullable": true
          },
          "terminal_serial": {
            "type": "string",
            "nullable": true
          },
          "terminal_merchant_name": {
            "type": "string",
            "nullable": true
          },
          "terminal_captured_at": {
            "type": "string",
            "nullable": true,
            "format": "date-time"
          },
          "last_viewed_at": {
            "type": "string",
            "nullable": true,
            "format": "date-time"
          },
          "installments": {
            "type": "integer",
            "nullable": true
          },
          "merchant_id": {
            "type": "string",
            "nullable": true,
            "format": "uuid"
          },
          "views_count": {
            "type": "integer",
            "format": "int64",
            "nullable": true
          },
          "view_dates": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string",
              "format": "date-time"
            }
          }
        },
        "required": [
          "id",
          "terminal_id",
          "folio",
          "amount",
          "base_amount",
          "tip_amount",
          "currency",
          "transaction_type",
          "items",
          "receipt_svg",
          "created_at",
          "expires_at",
          "card_brand",
          "card_type",
          "card_last4",
          "entry_mode",
          "emv_aid",
          "emv_app_label",
          "emv_arqc",
          "emv_tvr",
          "auth_code",
          "order_reference",
          "cashier",
          "operation_number",
          "batch_number",
          "affiliation",
          "payment_method",
          "receipt_template_code",
          "issued_source",
          "issued_by",
          "issued_trace_id",
          "terminal_serial",
          "terminal_merchant_name",
          "terminal_captured_at",
          "last_viewed_at",
          "installments",
          "merchant_id",
          "views_count",
          "view_dates"
        ],
        "description": "Respuesta de emisión. Incluye todos los campos del DTO; los campos de listado/panel son null al emitir. EMV y auth_code son simulados. Fechas y UUID del ejemplo son ilustrativos, receipt_svg está abreviado.",
        "additionalProperties": true,
        "example": {
          "id": "11111111-2222-4333-8444-555555555555",
          "terminal_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
          "folio": 123,
          "amount": "187.00",
          "base_amount": "170.00",
          "tip_amount": "17.00",
          "transaction_type": "venta",
          "currency": "MXN",
          "items": [
            {
              "name": "Café americano",
              "qty": "2",
              "price": "45.00"
            },
            {
              "name": "Sándwich",
              "qty": "1",
              "price": "80.00"
            }
          ],
          "card_brand": "visa",
          "card_type": "debito",
          "card_last4": "1234",
          "entry_mode": "contactless",
          "emv_aid": "A0000000031010",
          "emv_app_label": "VISA",
          "emv_arqc": "0123456789ABCDEF",
          "emv_tvr": "0000000000",
          "auth_code": "123456",
          "receipt_svg": "<svg xmlns=\"http://www.w3.org/2000/svg\"><!-- contenido abreviado --></svg>",
          "created_at": "2026-09-04T18:00:01Z",
          "expires_at": "2026-10-04T18:00:01Z",
          "order_reference": "ORDEN-0002",
          "cashier": "CAJA-01",
          "operation_number": "000123",
          "batch_number": "001",
          "affiliation": "AFILIACION-DE-PRUEBA",
          "installments": null,
          "payment_method": "tarjeta",
          "terminal_captured_at": "2026-09-04T18:00:00Z",
          "receipt_template_code": "consumo-restaurante",
          "issued_source": "tpv",
          "issued_by": "MARCA-MODELO-0001",
          "issued_trace_id": "ejemplo-traza-001",
          "merchant_id": null,
          "terminal_serial": null,
          "terminal_merchant_name": null,
          "views_count": null,
          "last_viewed_at": null,
          "view_dates": null
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "traceId": {
            "type": "string"
          },
          "details": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "field": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  }
}
