{"openapi":"3.0.3","info":{"title":"Vexorpay API","version":"1.0.0","description":"API pública de Vexorpay para cobrar en USD en Argentina y otros países. Los links de pago siempre operan en dólares; las comisiones se cobran en la cuenta de la plataforma y el saldo se acredita en la cuenta Stripe Express del creador. Autenticación por API key con el header `Authorization: Bearer <key>` (formato `vxp_u_<shard>_...`). El modo de prueba (sandbox) se elige request a request con `sandbox: true` en el body o query. Rate limit: 100 requests por minuto por API key (todas las respuestas incluyen los headers X-RateLimit-Limit/Remaining/Reset). Los POST de creación de links y checkout son idempotentes vía el header `Idempotency-Key`: misma clave + mismo body devuelve el recurso ya creado (200); misma clave + body distinto devuelve 409."},"servers":[{"url":"https://api.vexorpay.com/api/v1"}],"tags":[{"name":"Health","description":"Estado del servicio."},{"name":"Payment links","description":"Links de pago (one-time y recurrentes)."},{"name":"Charges","description":"Cobros y pagos del usuario."},{"name":"Disputes","description":"Disputas (chargebacks) y su evidencia."},{"name":"Subscriptions","description":"Suscripciones recurrentes."},{"name":"Balance","description":"Saldo de la cuenta Stripe Express."},{"name":"Payouts","description":"Retiros manuales (payouts `po_`)."},{"name":"Acreditaciones","description":"Transferencias recibidas (`tr_`) hacia la cuenta Express."},{"name":"Events","description":"Historial de eventos de webhook para reconciliación."}],"security":[{"bearerAuth":[]}],"paths":{"/health":{"get":{"tags":["Health"],"operationId":"getHealth","summary":"Estado del servicio","description":"Liveness: responde `{ status: 'ok' }` sin tocar la base de datos ni Stripe. No requiere autenticación.","security":[],"responses":{"200":{"description":"Servicio operativo.","content":{"application/json":{"schema":{"type":"object","required":["status","service"],"properties":{"status":{"type":"string","example":"ok"},"service":{"type":"string","example":"vexorpay-api"}}}}}}}}},"/payment-links":{"post":{"tags":["Payment links"],"operationId":"createPaymentLink","summary":"Crear un link de pago","description":"Crea un link de pago one-time (con `amount`) o recurrente (con `type: recurring` y `prices[]` de 1 a 5 planes). Los montos van en dólares (USD) y el mínimo por cobro o plan es USD 4. Idempotente con el header `Idempotency-Key`: un replay con el mismo body devuelve el link ya creado (200).","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentLinkCreate"}}}},"responses":{"200":{"description":"Replay de idempotencia: la misma `Idempotency-Key` con el mismo body devuelve el link previamente creado (mismo cuerpo que el 201).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentLink"}}}},"201":{"description":"Link de pago creado: misma forma que `GET /payment-links/{id}` (incluye `sandbox`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentLinkCreated"}}}},"400":{"description":"Error de validación: invalid_request, minimum_amount, invalid_amount, prices_required, too_many_prices, validation_error, invalid_interval, invalid_interval_count, invalid_trial, kyc_required, account_not_connected o sandbox_account_not_ready.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}}},"get":{"tags":["Payment links"],"operationId":"listPaymentLinks","summary":"Listar links de pago","description":"Lista los links del usuario con paginación por cursor. Excluye siempre los links borrados. Orden descendente por fecha de creación.","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["active","canceled","archived"]},"description":"Filtra por estado. Sin parámetro incluye todos."},{"name":"type","in":"query","required":false,"schema":{"type":"string","enum":["one_time","recurring"]},"description":"Filtra por tipo de link."},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/StartingAfter"},{"$ref":"#/components/parameters/EndingBefore"},{"$ref":"#/components/parameters/Sandbox"}],"responses":{"200":{"description":"Lista paginada de links.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentLinkList"}}}},"400":{"description":"validation_error: status o type inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/payment-links/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"ID del link de pago."}],"get":{"tags":["Payment links"],"operationId":"getPaymentLink","summary":"Ver un link de pago","description":"Devuelve el detalle de un link. Si hay una sesión de Checkout abierta se expone su URL en `checkoutUrl` (best-effort).","parameters":[{"$ref":"#/components/parameters/SandboxFlag"}],"responses":{"200":{"description":"Detalle del link de pago.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentLink"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"link_not_found: no existe o pertenece a otro usuario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"tags":["Payment links"],"operationId":"updatePaymentLink","summary":"Actualizar un link de pago","description":"Actualiza campos parciales de un link. El `type` es inmutable. Los planes (`prices`) solo se editan en links recurrentes: cada plan enviado con `id` se actualiza, sin `id` agrega uno nuevo, y los planes ausentes se eliminan (salvo que tengan suscripciones). La referencia y el estado se propagan a compras futuras, no a cargos ya creados. Cambiar contenido invalida la sesión de Checkout abierta.","parameters":[{"$ref":"#/components/parameters/SandboxFlag"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentLinkUpdate"}}}},"responses":{"200":{"description":"Link actualizado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentLink"}}}},"400":{"description":"Errores de validación: invalid_request (body vacío), invalid_amount, validation_error, prices_required, too_many_prices, payment_link_inactive, minimum_amount, invalid_interval, invalid_interval_count, invalid_trial o plan_in_use.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"link_not_found: no existe o pertenece a otro usuario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Payment links"],"operationId":"deletePaymentLink","summary":"Cancelar un link de pago","description":"Cancela un link (borrado blando: status `canceled`). No borra pagos ya realizados. Idempotente: cancelar un link ya cancelado devuelve 200 de nuevo con su estado actual.","parameters":[{"$ref":"#/components/parameters/SandboxFlag"}],"responses":{"200":{"description":"Link cancelado (devuelve el link con status `canceled`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentLink"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"link_not_found: no existe o pertenece a otro usuario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/payment-links/{id}/checkout":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"ID del link de pago activo que se va a pagar."}],"post":{"tags":["Payment links"],"operationId":"createPaymentLinkCheckout","summary":"Generar un checkout para un link","description":"Crea una compra (Charge) y una sesión de Checkout para un link one-time; en links recurrentes redirige directo al plan indicado (`price_id` o el default). Idempotente por `Idempotency-Key` (one-time); además, reusa una compra `pending` del mismo link con la misma referencia. Producción y sandbox nunca se mezclan: el link debe ser del mismo modo que la request.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutInput"}}}},"responses":{"200":{"description":"Sesión de checkout creada. Para one-time devuelve `{ charge, checkoutUrl, expiresAt }`; para recurrentes devuelve `{ price_id, price, seats, seatTotalCents, checkoutUrl, expiresAt }`.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/CheckoutResponse"},{"$ref":"#/components/schemas/RecurringCheckoutResponse"}]}}}},"400":{"description":"invalid_request, validation_error, prices_required, invalid_amount, minimum_amount, below_transfer_minimum, invalid_seats, payment_link_inactive o account_not_ready.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"link_not_found o price_not_found (plan inexistente en links recurrentes).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"idempotency_conflict: misma `Idempotency-Key` con body distinto. price_not_ready: el plan del link todavía no tiene Product/Price de Stripe asociado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/charges":{"get":{"tags":["Charges"],"operationId":"listCharges","summary":"Listar cobros","description":"Lista los cobros (pagos) del usuario con paginación por cursor, orden descendente por fecha de creación.","parameters":[{"name":"payment_link_id","in":"query","required":false,"schema":{"type":"string"},"description":"Filtra por link de pago."},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["pending","paid","failed","refunded","disputed"]},"description":"Filtra por estado del cobro."},{"name":"reference","in":"query","required":false,"schema":{"type":"string"},"description":"Filtra por referencia exacta del cobro."},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/StartingAfter"},{"$ref":"#/components/parameters/EndingBefore"},{"$ref":"#/components/parameters/Sandbox"}],"responses":{"200":{"description":"Lista paginada de cobros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChargeList"}}}},"400":{"description":"validation_error: status inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/charges/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"ID del cobro."}],"get":{"tags":["Charges"],"operationId":"getCharge","summary":"Ver un cobro","description":"Detalle de un cobro con embeds: link de pago resumido, desglose de fees y `receiptUrl` para descargar el comprobante.","parameters":[{"$ref":"#/components/parameters/SandboxFlag"}],"responses":{"200":{"description":"Detalle del cobro.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChargeDetail"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"charge_not_found: no existe o pertenece a otro usuario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/charges/{id}/receipt":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"ID del cobro."}],"get":{"tags":["Charges"],"operationId":"downloadChargeReceipt","summary":"Descargar comprobante en PDF","description":"Devuelve el comprobante de un pago `paid` en PDF. Para pagos pendientes o fallidos responde 400.","parameters":[{"$ref":"#/components/parameters/SandboxFlag"}],"responses":{"200":{"description":"Comprobante en PDF (adjunto, `receipt-<reference-or-id>.pdf`).","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"invalid_request: el comprobante solo está disponible para pagos completados.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"charge_not_found: no existe o pertenece a otro usuario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/disputes":{"get":{"tags":["Disputes"],"operationId":"listDisputes","summary":"Listar disputas","description":"Lista las disputas (chargebacks) del usuario con paginación por cursor, orden descendente por fecha de creación. El scope de modo de prueba se resuelve sobre el cobro asociado (la disputa no tiene flag propio).","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["warning_needs_response","warning_under_review","warning_closed","needs_response","under_review","won","lost","expired","closed"]},"description":"Filtra por estado de la disputa."},{"name":"charge_id","in":"query","required":false,"schema":{"type":"string"},"description":"Filtra por el cobro original asociado a la disputa."},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/StartingAfter"},{"$ref":"#/components/parameters/EndingBefore"},{"$ref":"#/components/parameters/Sandbox"}],"responses":{"200":{"description":"Lista paginada de disputas.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisputeList"}}}},"400":{"description":"validation_error: status inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/disputes/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"ID local de la disputa."}],"get":{"tags":["Disputes"],"operationId":"getDispute","summary":"Ver una disputa","description":"Detalle de una disputa con el estado EN VIVO de Stripe (el webhook puede demorar), la evidencia ya cargada, el costo estimado para el creador y el enlace al panel de Stripe.","parameters":[{"$ref":"#/components/parameters/SandboxFlag"}],"responses":{"200":{"description":"Detalle de la disputa.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisputeDetail"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"dispute_not_found: no existe o pertenece a otro usuario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/disputes/{id}/response":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"ID local de la disputa."}],"post":{"tags":["Disputes"],"operationId":"respondToDispute","summary":"Responder una disputa con evidencia","description":"Carga evidencia en una disputa abierta y la envía a Stripe (`submit: true`). La evidencia va en snake_case; al menos un campo debe tener contenido. Una vez enviada, la disputa pasa a `under_review` y el plazo deja de aplicar. Idempotente con el header `Idempotency-Key`: un replay con el mismo body devuelve el detalle actual de la disputa sin re-enviar evidencia (200); mismo clave con body distinto responde 409.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisputeEvidence"}}}},"responses":{"200":{"description":"Evidencia enviada: la disputa actualizada (con el estado vivo de Stripe).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisputeDetail"}}}},"400":{"description":"evidence_required: ningún campo de evidencia con contenido. dispute_not_respondable: la disputa ya está cerrada. dispute_deadline_passed: venció el plazo. dispute_evidence_rejected: Stripe rechazó la evidencia. validation_error: body inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"dispute_not_found: no existe o pertenece a otro usuario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/subscriptions":{"get":{"tags":["Subscriptions"],"operationId":"listSubscriptions","summary":"Listar suscripciones","description":"Lista las suscripciones del usuario con paginación por cursor, orden descendente por fecha de creación.","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["active","trialing","past_due","canceled","unpaid","pending","incomplete"]},"description":"Filtra por estado de la suscripción."},{"name":"payment_link_id","in":"query","required":false,"schema":{"type":"string"},"description":"Filtra por link de pago."},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/StartingAfter"},{"$ref":"#/components/parameters/EndingBefore"},{"$ref":"#/components/parameters/Sandbox"}],"responses":{"200":{"description":"Lista paginada de suscripciones.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionList"}}}},"400":{"description":"validation_error: status inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/subscriptions/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"ID de la suscripción."}],"get":{"tags":["Subscriptions"],"operationId":"getSubscription","summary":"Ver una suscripción","description":"Devuelve el detalle de una suscripción del usuario.","parameters":[{"$ref":"#/components/parameters/SandboxFlag"}],"responses":{"200":{"description":"Detalle de la suscripción.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Subscription"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"subscription_not_found: no existe o pertenece a otro usuario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}},"patch":{"tags":["Subscriptions"],"operationId":"updateSubscriptionSeats","summary":"Cambiar la cantidad de asientos","description":"Cambia la cantidad de asientos (`seats`) de una suscripción activa con plan per-seat. Es el único campo mutable: cambiar de plan implica cancelar y volver a suscribirse. Devuelve la suscripción con `seats` y `seatTotalCents` adicionales a nivel raíz.","parameters":[{"$ref":"#/components/parameters/SandboxFlag"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionSeatsUpdate"}}}},"responses":{"200":{"description":"Suscripción actualizada con los nuevos `seats` y `seatTotalCents`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionSeatsUpdateResponse"}}}},"400":{"description":"validation_error: campo desconocido o falta `seats`. invalid_seats: `seats` fuera de los límites del plan o en un plan de precio fijo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"subscription_not_found o price_not_found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"subscription_not_ready: la suscripción todavía no está activa en Stripe (reintentá). price_not_ready: el plan todavía no tiene Product/Price de Stripe asociado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/subscriptions/{id}/cancel":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"ID de la suscripción."}],"post":{"tags":["Subscriptions"],"operationId":"cancelSubscription","summary":"Cancelar una suscripción al final del período","description":"Programa la cancelación de la suscripción al final del período actual (`cancelAtPeriodEnd: true`). Idempotente: cancelar una suscripción ya cancelada o con cancelación programada devuelve 200 con su estado actual.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"sandbox":{"type":"boolean","description":"true para operar en modo de prueba."}}}}}},"responses":{"200":{"description":"Suscripción con la cancelación programada (o su estado actual si ya estaba cancelada).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Subscription"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"subscription_not_found: no existe o pertenece a otro usuario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/subscriptions/{id}/portal":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"ID de la suscripción."}],"post":{"tags":["Subscriptions"],"operationId":"openSubscriptionPortal","summary":"Abrir el Customer Portal de Stripe del suscriptor","description":"Crea una sesión del Customer Portal de Stripe para que el suscriptor administre su suscripción (métodos de pago, facturación). Devuelve la URL a la que hay que redirigir.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"sandbox":{"type":"boolean","description":"true para operar en modo de prueba."}}}}}},"responses":{"200":{"description":"URL del Customer Portal.","content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","description":"URL del Customer Portal de Stripe."}}}}}},"400":{"description":"invalid_request: la suscripción no tiene un customer de Stripe asociado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"subscription_not_found: no existe o pertenece a otro usuario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/balance":{"get":{"tags":["Balance"],"operationId":"getBalance","summary":"Ver el saldo de la cuenta Stripe Express","description":"Devuelve el saldo disponible y pendiente de la cuenta Express, desglosado por moneda, junto con el estado de reembolsos y disputas a nivel Vexorpay. Si el usuario aún no conectó una cuenta Express responde 200 con `connected: false` (no es un error).","parameters":[{"$ref":"#/components/parameters/SandboxFlag"}],"responses":{"200":{"description":"Saldo de la cuenta Express (o `connected: false` sin cuenta conectada).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Balance"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/payouts":{"get":{"tags":["Payouts"],"operationId":"listPayouts","summary":"Listar retiros manuales","description":"Lista los retiros manuales (payouts `po_` de Stripe) que el usuario hizo desde el dashboard. Las acreditaciones por cobros (transfers `tr_`) se listan en /acreditaciones.","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/StartingAfter"},{"$ref":"#/components/parameters/EndingBefore"},{"$ref":"#/components/parameters/Sandbox"}],"responses":{"200":{"description":"Lista paginada de retiros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayoutList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/acreditaciones":{"get":{"tags":["Acreditaciones"],"operationId":"listAcreditaciones","summary":"Listar acreditaciones","description":"Lista las acreditaciones (transfers `tr_` hacia la cuenta Express) en orden cronológico descendente, con el monto en la moneda original del cobro (USD), el neto en la moneda de liquidación de la cuenta y la tasa de cambio aplicada. El estado es `acreditada` / `pendiente` según `available_on`, o `reversed` si el transfer fue revertido.","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/StartingAfter"},{"$ref":"#/components/parameters/EndingBefore"},{"$ref":"#/components/parameters/SandboxFlag"}],"responses":{"200":{"description":"Lista paginada de acreditaciones.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcreditacionList"}}}},"400":{"description":"account_not_connected: no hay una cuenta Stripe Express conectada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/events":{"get":{"tags":["Events"],"operationId":"listEvents","summary":"Listar eventos de webhook","description":"Lista los eventos de negocio del usuario (payment.completed/failed/refunded/disputed, payout.paid) en orden cronológico descendente para reconciliación estilo Stripe. Cada evento usa el mismo id determinístico (`evt_...`) que se firma en los webhooks salientes, así el integrador puede cruzar lo recibido por POST con el histórico y deduplicar. El objeto embebido en `data.object` tiene exactamente la misma forma que el payload del webhook.","parameters":[{"name":"type","in":"query","required":false,"schema":{"type":"string","enum":["payment.completed","payment.failed","payment.refunded","payment.disputed","dispute.resolved","payout.paid","subscription.created","subscription.payment.completed","subscription.payment.failed","subscription.canceled","subscription.trial_will_end"]},"description":"Filtra por tipo de evento."},{"name":"created[gte]","in":"query","required":false,"schema":{"type":"string"},"description":"Incluye solo eventos creados en esta fecha ISO 8601 o después (comparación lexicográfica)."},{"name":"created[lte]","in":"query","required":false,"schema":{"type":"string"},"description":"Incluye solo eventos creados en esta fecha ISO 8601 o antes (comparación lexicográfica)."},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/StartingAfter"},{"$ref":"#/components/parameters/EndingBefore"},{"$ref":"#/components/parameters/SandboxFlag"}],"responses":{"200":{"description":"Lista paginada de eventos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventList"}}}},"400":{"description":"validation_error: tipo de evento inválido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"API key con formato `vxp_u_<shard>_...` (legacy `vxp_live_...`). Mandar como `Authorization: Bearer <api_key>`."}},"headers":{"RateLimitLimit":{"description":"Cantidad máxima de requests por minuto para la API key (siempre 100).","schema":{"type":"integer"}},"RateLimitRemaining":{"description":"Requests restantes en la ventana de un minuto.","schema":{"type":"integer"}},"RateLimitReset":{"description":"Timestamp Unix (segundos) en el que se reinicia la ventana del rate limit.","schema":{"type":"integer"}},"RetryAfter":{"description":"Segundos a esperar antes de reintentar tras un 429.","schema":{"type":"integer"}}},"parameters":{"Limit":{"name":"limit","in":"query","required":false,"description":"Cantidad máxima de elementos por página (1 a 100, default 10).","schema":{"type":"integer","minimum":1,"maximum":100,"default":10}},"StartingAfter":{"name":"starting_after","in":"query","required":false,"description":"ID del cursor: devuelve los elementos posteriores a este id (paginación hacia adelante).","schema":{"type":"string"}},"EndingBefore":{"name":"ending_before","in":"query","required":false,"description":"ID del cursor: devuelve los elementos anteriores a este id (paginación hacia atrás).","schema":{"type":"string"}},"Sandbox":{"name":"sandbox","in":"query","required":false,"description":"Filtra por modo de prueba: `true` solo sandbox, `false` solo producción. **Sin el parámetro no filtra: la respuesta mezcla ambos entornos** (los detalles de cada fila traen su `sandbox`).","schema":{"type":"boolean"}},"SandboxFlag":{"name":"sandbox","in":"query","required":false,"description":"Modo de prueba de ESTA operación: `true` para leer/escribir sobre el entorno sandbox. **Sin el parámetro (o con `false`) la operación aplica al entorno de producción**.","schema":{"type":"boolean"}},"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"description":"Clave de idempotencia (máx. 255 caracteres). Misma clave + mismo body devuelve el recurso ya creado (200); misma clave + body distinto responde 409. Se conserva 24 h: después la clave se descarta y la operación corre de nuevo.","schema":{"type":"string"}}},"responses":{"Unauthorized":{"description":"No autenticado. `unauthorized`: falta el header Authorization. `invalid_key`: API key inválida o revocada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Conflict":{"description":"idempotency_conflict: la `Idempotency-Key` ya se usó con un body distinto al actual.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"rate_limited: superaste el límite de 100 requests por minuto.","headers":{"Retry-After":{"$ref":"#/components/headers/RetryAfter"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"ServerError":{"description":"unexpected_error, stripe_error (Stripe rechazó la operación) u otros fallos sin código estable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Error":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"Código de error estable (ej. validation_error, link_not_found, idempotency_conflict).","example":"validation_error"},"message":{"type":"string","description":"Descripción legible del error en español."},"detail":{"oneOf":[{"type":"string"},{"type":"object","additionalProperties":true}],"description":"Detalle opcional: string en errores 500 con el mensaje subyacente, objeto en 429 (`resetInSeconds`)."}}},"PaymentLink":{"type":"object","description":"Link de pago serializado por `paymentLinkToJson` (GET/lists).","required":["id","amountCents","currency","title","description","image","reference","metadata","status","slug","sandbox","url","createdAt","updatedAt","type"],"properties":{"id":{"type":"string"},"amountCents":{"type":"integer","description":"Monto en centavos de USD."},"currency":{"type":"string","example":"usd"},"title":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"image":{"type":"string","nullable":true,"description":"URL absoluta de la imagen del producto."},"reference":{"type":"string","nullable":true,"description":"Referencia interna del creador."},"metadata":{"type":"object","nullable":true,"additionalProperties":true,"description":"Metadatos libres enviados al crear el link."},"status":{"type":"string","enum":["active","canceled","archived","deleted"]},"slug":{"type":"string"},"sandbox":{"type":"boolean"},"url":{"type":"string","description":"URL pública `/p/{slug}` del link."},"checkoutUrl":{"type":"string","nullable":true,"description":"URL de una sesión de Checkout abierta (solo si existe)."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"type":{"type":"string","enum":["one_time","recurring"],"default":"one_time"},"prices":{"type":"array","description":"Planes del link recurrente (solo si tiene precios).","items":{"$ref":"#/components/schemas/SubscriptionPrice"}}}},"PaymentLinkCreated":{"type":"object","description":"Cuerpo de 201 de POST /payment-links: misma forma que `PaymentLink` (incluye `sandbox`).","required":["id","amountCents","currency","title","description","reference","metadata","status","slug","sandbox","url","image","createdAt","updatedAt","type"],"properties":{"id":{"type":"string"},"amountCents":{"type":"integer"},"currency":{"type":"string","example":"usd"},"title":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"reference":{"type":"string","nullable":true},"metadata":{"type":"object","nullable":true,"additionalProperties":true},"status":{"type":"string","enum":["active","canceled","archived"]},"slug":{"type":"string"},"sandbox":{"type":"boolean","description":"Link de prueba del modo sandbox (Stripe test mode)."},"url":{"type":"string"},"image":{"type":"string","nullable":true},"checkoutUrl":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"type":{"type":"string","enum":["one_time","recurring"]},"prices":{"type":"array","items":{"$ref":"#/components/schemas/SubscriptionPrice"}}}},"SubscriptionPrice":{"type":"object","description":"Plan de un link recurrente.","required":["id","stripePriceId","amountCents","currency","interval","intervalCount","trialPeriodDays","description","isDefault","pricingType","perSeatAmountCents","minSeats","maxSeats","seatTiers","tiersMode"],"properties":{"id":{"type":"string"},"stripePriceId":{"type":"string","nullable":true},"amountCents":{"type":"integer","description":"Precio del ciclo en centavos de USD. En per-seat uniforme es el precio por asiento; en base+extra es la base que cubre el primer asiento."},"currency":{"type":"string","example":"usd"},"interval":{"type":"string","enum":["day","week","month","year"]},"intervalCount":{"type":"integer","description":"Cada cuántos intervalos se cobra (1-12)."},"trialPeriodDays":{"type":"integer","nullable":true},"description":{"type":"string","nullable":true},"isDefault":{"type":"boolean"},"pricingType":{"type":"string","enum":["flat","per_seat"]},"perSeatAmountCents":{"type":"integer","nullable":true,"description":"Precio de cada asiento extra en centavos (null si todos cuestan lo mismo)."},"minSeats":{"type":"integer"},"maxSeats":{"type":"integer","nullable":true},"seatTiers":{"type":"array","nullable":true,"items":{"$ref":"#/components/schemas/SeatTier"}},"tiersMode":{"type":"string","enum":["graduated","volume"],"nullable":true}}},"SeatTier":{"type":"object","required":["upTo"],"properties":{"upTo":{"type":"integer","nullable":true,"description":"Cantidad máxima de asientos del tramo; null = último tramo (hasta el infinito)."},"unitAmountCents":{"type":"integer","description":"Precio por unidad del tramo en centavos."},"flatAmountCents":{"type":"integer","description":"Cargo fijo del tramo en centavos."}}},"PaymentLinkCreate":{"type":"object","required":["amount"],"properties":{"amount":{"type":"number","description":"Monto en dólares (USD) para links one-time. Mínimo 4. Ignorado cuando `type` es recurring.","minimum":4,"example":25},"title":{"type":"string","description":"Título del link."},"description":{"type":"string"},"image_file_id":{"type":"string","description":"ID de un archivo GridFS completado del usuario (foto del producto)."},"reference":{"type":"string","description":"Referencia interna del creador."},"metadata":{"type":"object","additionalProperties":true,"description":"Metadatos libres de la compra."},"sandbox":{"type":"boolean","description":"true crea el link en modo de prueba (requiere una cuenta Express sandbox completada)."},"type":{"type":"string","enum":["one_time","recurring"],"default":"one_time"},"prices":{"type":"array","maxItems":5,"description":"Planes del link recurrente (requerido cuando `type` es recurring).","items":{"$ref":"#/components/schemas/PricePlanInput"}}}},"PricePlanInput":{"type":"object","description":"Definición de un plan de link recurrente. Keys en snake_case; montos en dólares (se convierten a centavos). El monto mínimo por plan es USD 4.","required":["amount","interval"],"properties":{"id":{"type":"string","description":"ID del plan existente (solo al editar con PATCH: actualiza ese mismo plan)."},"amount":{"type":"number","description":"Precio del ciclo en dólares, mínimo 4.","minimum":4},"currency":{"type":"string","enum":["usd"],"default":"usd"},"interval":{"type":"string","enum":["day","week","month","year"]},"interval_count":{"type":"integer","default":1,"description":"Cada cuántos intervalos (1-12; anual solo cada 1)."},"trial_period_days":{"type":"integer","description":"Días de prueba gratis (1-90)."},"description":{"type":"string"},"pricing_type":{"type":"string","enum":["flat","per_seat"]},"per_seat_amount":{"type":"number","description":"Precio en dólares de cada asiento extra (per-seat base+extra)."},"min_seats":{"type":"integer","default":1},"max_seats":{"type":"integer","nullable":true},"seat_tiers":{"type":"array","description":"Matriz de tramos de precios. Usa CENTAVOS por asiento (convención de Stripe), a diferencia de `amount`/`per_seat_amount` que van en dólares.","items":{"$ref":"#/components/schemas/SeatTier"}},"tiers_mode":{"type":"string","enum":["graduated","volume"],"nullable":true}}},"PaymentLinkUpdate":{"type":"object","description":"Cuerpo de PATCH /payment-links/{id}. Solo los campos enviados se actualizan.","properties":{"amount":{"type":"number","description":"Nuevo monto en dólares (solo one-time), mínimo 4.","minimum":4},"title":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"image_file_id":{"type":"string","nullable":true,"description":"ID de una imagen del usuario; null quita la foto actual."},"reference":{"type":"string","nullable":true},"status":{"type":"string","enum":["active","canceled","archived"]},"prices":{"type":"array","maxItems":5,"description":"Nueva lista de planes (solo recurring): plan con `id` se actualiza, sin `id` agrega uno, y los ausentes se eliminan (salvo plan_in_use).","items":{"$ref":"#/components/schemas/PricePlanInput"}}}},"CheckoutInput":{"type":"object","properties":{"reference":{"type":"string","description":"Referencia de la compra. Reusa una compra pending del mismo link con la misma referencia."},"metadata":{"type":"object","additionalProperties":true,"description":"Metadatos libres del cobro."},"customer_email":{"type":"string","format":"email","description":"Email del comprador (debe incluir '@')."},"sandbox":{"type":"boolean","description":"true realiza el cobro en modo de prueba (el link tiene que ser del mismo modo)."},"price_id":{"type":"string","description":"ID del plan al que llevar al comprador (solo recurrente). Ausente = plan default."},"seats":{"type":"integer","description":"Cantidad de asientos a cotizar (solo planes per_seat). Ausente = min_seats."}}},"CheckoutResponse":{"type":"object","description":"Respuesta de checkout para links one-time.","required":["charge","checkoutUrl","expiresAt"],"properties":{"charge":{"$ref":"#/components/schemas/Charge"},"checkoutUrl":{"type":"string","nullable":true,"description":"URL de la sesión de Checkout de Stripe."},"expiresAt":{"type":"string","format":"date-time","nullable":true,"description":"Fecha de expiración de la sesión (null si la sesión no está abierta)."}}},"RecurringCheckoutResponse":{"type":"object","description":"Respuesta de checkout para links recurrentes.","required":["price_id","price","seats","seatTotalCents","checkoutUrl","expiresAt"],"properties":{"price_id":{"type":"string"},"price":{"$ref":"#/components/schemas/SubscriptionPrice"},"seats":{"type":"integer","description":"Cantidad de asientos cotizada."},"seatTotalCents":{"type":"integer","description":"Total por ciclo con esos asientos, en centavos."},"checkoutUrl":{"type":"string"},"expiresAt":{"type":"string","format":"date-time","nullable":true}}},"Charge":{"type":"object","description":"Cobro/pago serializado por `chargeToJson`.","required":["id","paymentLinkId","reference","amountCents","currency","status","sandbox","platformFeeCents","stripeFeeCents","netCents","customerEmail","metadata","paidAt","createdAt","refundedCents","recoveredCents","disputeStatus","disputeOutcome"],"properties":{"id":{"type":"string"},"paymentLinkId":{"type":"string"},"reference":{"type":"string","nullable":true},"amountCents":{"type":"integer","description":"Monto cobrado en centavos de USD."},"currency":{"type":"string","example":"usd"},"status":{"type":"string","enum":["pending","paid","failed","refunded","disputed"]},"sandbox":{"type":"boolean"},"platformFeeCents":{"type":"integer","description":"Comisión de Vexorpay en centavos (USD 1 por cobro)."},"stripeFeeCents":{"type":"integer","nullable":true},"netCents":{"type":"integer","nullable":true,"description":"Neto transferido al creador."},"customerEmail":{"type":"string","nullable":true},"metadata":{"type":"object","nullable":true,"additionalProperties":true},"paidAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"},"refundedCents":{"type":"integer","nullable":true},"recoveredCents":{"type":"integer","nullable":true,"description":"Total reversado/recuperado del creador por reembolsos o disputas perdidas."},"disputeStatus":{"type":"string","nullable":true},"disputeOutcome":{"type":"string","nullable":true}}},"ChargeDetail":{"type":"object","description":"Detalle de un cobro (GET /charges/{id}) con embeds.","allOf":[{"$ref":"#/components/schemas/Charge"},{"type":"object","required":["paymentLink","fees","receiptUrl"],"properties":{"paymentLink":{"$ref":"#/components/schemas/PaymentLinkSummary"},"fees":{"$ref":"#/components/schemas/FeesBreakdown"},"receiptUrl":{"type":"string","description":"Ruta relativa al comprobante PDF."}}}]},"PaymentLinkSummary":{"type":"object","nullable":true,"required":["id","title","slug","url"],"properties":{"id":{"type":"string"},"title":{"type":"string","nullable":true},"slug":{"type":"string"},"url":{"type":"string"}}},"Dispute":{"type":"object","description":"Disputa (chargeback) serializada por `disputeToJson`.","required":["id","chargeId","paymentLinkId","reference","customerEmail","stripeDisputeId","status","statusLabel","reason","reasonLabel","amountCents","currency","feeCents","evidenceDeadline","daysLeft","isOpen","canRespond","canAccept","outcome","recoveredCents","compensatedCents","sandbox","paymentLinkUrl","createdAt","resolvedAt"],"properties":{"id":{"type":"string","description":"ID local de la disputa (usar en GET/POST /disputes/{id})."},"chargeId":{"type":"string","description":"Cobro original asociado a la disputa."},"paymentLinkId":{"type":"string"},"reference":{"type":"string","nullable":true},"customerEmail":{"type":"string","nullable":true},"stripeDisputeId":{"type":"string","description":"ID de la disputa en Stripe (dp_...)."},"status":{"type":"string","enum":["warning_needs_response","warning_under_review","warning_closed","needs_response","under_review","won","lost","expired","closed"]},"statusLabel":{"type":"string","description":"Estado traducido al español."},"reason":{"type":"string","nullable":true,"enum":["fraudulent","product_not_received","product_unacceptable","unrecognized","subscription_canceled","credit_not_processed","duplicate","incorrect_amount","merchandise_not_as_described","merchandise_undelivered","other","inquiry"]},"reasonLabel":{"type":"string","description":"Motivo traducido al español."},"amountCents":{"type":"integer","description":"Monto disputado en centavos de USD."},"currency":{"type":"string","example":"usd"},"feeCents":{"type":"integer","description":"Fee de Stripe por la disputa en centavos (se cobra al creador, USD 15)."},"evidenceDeadline":{"type":"string","format":"date-time","nullable":true,"description":"Límite para enviar evidencia (null si no aplica o ya venció)."},"daysLeft":{"type":"integer","nullable":true,"description":"Días restantes para responder (null si venció o no hay plazo)."},"isOpen":{"type":"boolean","description":"true si la disputa sigue abierta."},"canRespond":{"type":"boolean","description":"true si todavía se puede enviar evidencia."},"canAccept":{"type":"boolean","description":"true si se puede aceptar (perder) la disputa. No aplica a consultas tempranas (warning_*)."},"outcome":{"type":"string","nullable":true,"enum":["won","lost",null],"description":"Desenlace: `won`, `lost` o null mientras la disputa siga abierta."},"recoveredCents":{"type":"integer","nullable":true,"description":"Monto recuperado del creador si la disputa se perdió."},"compensatedCents":{"type":"integer","nullable":true},"sandbox":{"type":"boolean","description":"Modo de prueba del cobro asociado."},"paymentLinkUrl":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"resolvedAt":{"type":"string","format":"date-time","nullable":true}}},"DisputeDetail":{"type":"object","description":"Detalle de una disputa (GET /disputes/{id}) con estado vivo de Stripe.","allOf":[{"$ref":"#/components/schemas/Dispute"},{"type":"object","required":["stripe","evidence","cost","paymentLinkUrl","stripeDashboardUrl"],"properties":{"stripe":{"type":"object","nullable":true,"description":"Estado en vivo de Stripe (null si no se pudo consultar).","properties":{"status":{"type":"string"},"evidenceDueBy":{"type":"string","format":"date-time","nullable":true},"hasEvidence":{"type":"boolean"},"submissionCount":{"type":"integer"},"pastDue":{"type":"boolean"},"cardBrand":{"type":"string","nullable":true},"cardCaseType":{"type":"string","nullable":true},"networkReasonCode":{"type":"string","nullable":true},"reason":{"type":"string","nullable":true}}},"evidence":{"type":"object","description":"Evidencia cargada: los valores son los IDs de los recursos de Stripe (file_... o texto).","properties":{"productDescription":{"type":"string","nullable":true},"customerCommunication":{"type":"string","nullable":true},"receipt":{"type":"string","nullable":true},"refundPolicy":{"type":"string","nullable":true},"cancellationPolicyDisclosure":{"type":"string","nullable":true},"cancellationRebuttal":{"type":"string","nullable":true},"serviceDate":{"type":"string","nullable":true},"shippingCarrier":{"type":"string","nullable":true},"shippingTrackingNumber":{"type":"string","nullable":true},"uncategorizedText":{"type":"string","nullable":true},"duplicateChargeId":{"type":"string","nullable":true},"duplicateChargeExplanation":{"type":"string","nullable":true}}},"cost":{"type":"object","required":["amountInDisputeCents","feeCents","feeChargedToCreator"],"properties":{"amountInDisputeCents":{"type":"integer"},"feeCents":{"type":"integer"},"feeChargedToCreator":{"type":"boolean"},"recoveredCents":{"type":"integer","nullable":true},"compensatedCents":{"type":"integer","nullable":true},"reversedCents":{"type":"integer","nullable":true},"debtAppliedCents":{"type":"integer","nullable":true},"transferReversalId":{"type":"string","nullable":true}}},"paymentLinkUrl":{"type":"string","nullable":true},"stripeDashboardUrl":{"type":"string","description":"Enlace al panel de Stripe de la disputa (path /test en mundo sandbox)."}}}]},"DisputeEvidence":{"type":"object","description":"Evidencia a enviar en POST /disputes/{id}/response. Al menos un campo debe tener contenido. El texto libre (notas y conversación con el cliente) va a `uncategorized_text` en Stripe.","properties":{"product_description":{"type":"string","description":"Qué se entregó/servició y por qué el cobro es legítimo (campo principal)."},"customer_communication":{"type":"string","description":"Conversación con el cliente (se concatena con `notes` en `uncategorized_text`)."},"notes":{"type":"string","description":"Notas libres."},"service_date":{"type":"string","description":"Fecha de entrega/servicio (YYYY-MM-DD)."},"shipping_carrier":{"type":"string"},"shipping_tracking_number":{"type":"string"},"refund_policy_url":{"type":"string","description":"URL pública de la política de reembolso."},"cancellation_policy_url":{"type":"string","description":"URL pública de la política de cancelación."},"cancellation_rebuttal":{"type":"string"},"duplicate_charge_id":{"type":"string","description":"ID del cobro duplicado (reason `duplicate`)."},"duplicate_charge_explanation":{"type":"string"},"attach_receipt":{"type":"boolean","description":"Adjunta el comprobante PDF del pago como evidencia (default false)."},"sandbox":{"type":"boolean","description":"Modo de prueba: true lee/escribe disputas del modo sandbox, false (default) del modo live."}}},"DisputeList":{"type":"object","required":["data","has_more","total_count","url"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Dispute"}},"has_more":{"type":"boolean"},"total_count":{"type":"integer"},"url":{"type":"string"}}},"FeesBreakdown":{"type":"object","required":["vexor","stripe","net"],"properties":{"vexor":{"$ref":"#/components/schemas/MoneyAmount"},"stripe":{"$ref":"#/components/schemas/MoneyAmount"},"net":{"$ref":"#/components/schemas/MoneyAmount"}}},"MoneyAmount":{"type":"object","required":["amountCents","currency"],"properties":{"amountCents":{"type":"integer","nullable":true},"currency":{"type":"string"}}},"Subscription":{"type":"object","description":"Suscripción serializada por `subscriptionToJson`.","required":["id","stripeSubscriptionId","paymentLinkId","paymentLinkPriceId","price","status","customerEmail","reference","currentPeriodStart","currentPeriodEnd","canceledAt","cancelAtPeriodEnd","trialStart","trialEnd","sandbox","createdAt"],"properties":{"id":{"type":"string"},"stripeSubscriptionId":{"type":"string","nullable":true},"paymentLinkId":{"type":"string"},"paymentLinkPriceId":{"type":"string"},"price":{"$ref":"#/components/schemas/SubscriptionPriceSummary"},"status":{"type":"string","enum":["active","trialing","past_due","canceled","unpaid","pending","incomplete"]},"customerEmail":{"type":"string","nullable":true},"reference":{"type":"string","nullable":true},"currentPeriodStart":{"type":"string","format":"date-time","nullable":true},"currentPeriodEnd":{"type":"string","format":"date-time","nullable":true},"canceledAt":{"type":"string","format":"date-time","nullable":true},"cancelAtPeriodEnd":{"type":"boolean"},"trialStart":{"type":"string","format":"date-time","nullable":true},"trialEnd":{"type":"string","format":"date-time","nullable":true},"sandbox":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"}}},"SubscriptionPriceSummary":{"type":"object","required":["amountCents","currency","interval","intervalCount","trialPeriodDays","seats","seatTotalCents","perSeatAmountCents"],"properties":{"amountCents":{"type":"integer"},"currency":{"type":"string","example":"usd"},"interval":{"type":"string","enum":["day","week","month","year"]},"intervalCount":{"type":"integer"},"trialPeriodDays":{"type":"integer","nullable":true},"seats":{"type":"integer","description":"Asientos comprados (1 en todo lo que no es per-seat)."},"seatTotalCents":{"type":"integer","description":"Total por ciclo con esos asientos, en centavos."},"perSeatAmountCents":{"type":"integer","nullable":true}}},"SubscriptionSeatsUpdate":{"type":"object","required":["seats"],"properties":{"seats":{"type":"integer","description":"Nueva cantidad de asientos (1-1000)."}}},"SubscriptionSeatsUpdateResponse":{"type":"object","description":"Respuesta de PATCH /subscriptions/{id}: la suscripción más `seats` y `seatTotalCents` a nivel raíz.","allOf":[{"$ref":"#/components/schemas/Subscription"},{"type":"object","required":["seats","seatTotalCents"],"properties":{"seats":{"type":"integer"},"seatTotalCents":{"type":"integer"}}}]},"Balance":{"type":"object","description":"Saldo de la cuenta Express. Sin cuenta conectada responde solo `{ connected: false, refunds, sandbox }`.","required":["connected","refunds","sandbox"],"properties":{"connected":{"type":"boolean","description":"false si el usuario no tiene una cuenta Stripe Express conectada."},"currency":{"type":"string","description":"Moneda principal del saldo disponible."},"available":{"type":"array","description":"Entradas de saldo disponible CRUDAS de Stripe (una por source_type).","items":{"$ref":"#/components/schemas/BalanceEntry"}},"pending":{"type":"array","description":"Entradas de saldo pendiente CRUDAS de Stripe.","items":{"$ref":"#/components/schemas/BalanceEntry"}},"byCurrency":{"type":"array","description":"Saldo sumado por moneda, con los tres buckets resueltos.","items":{"$ref":"#/components/schemas/ByCurrencyEntry"}},"usd":{"type":"object","nullable":true,"description":"Saldo en USD comparable con el ledger de Vexorpay (null si no hay disponibilidad en USD).","properties":{"availableCents":{"type":"integer"},"pendingCents":{"type":"integer"}}},"negativeCurrencies":{"type":"array","items":{"type":"string"},"description":"Monedas con saldo disponible negativo."},"refunds":{"type":"object","required":["debtCents","creditCents"],"properties":{"debtCents":{"type":"integer","description":"Saldo deudor del creador por reembolsos/disputas perdidas."},"creditCents":{"type":"integer","description":"Crédito por disputas ganadas."}}},"sandbox":{"type":"boolean"},"updatedAt":{"type":"string","format":"date-time"}}},"BalanceEntry":{"type":"object","required":["currency","amountCents"],"properties":{"currency":{"type":"string"},"amountCents":{"type":"integer"}}},"ByCurrencyEntry":{"type":"object","required":["currency","available","pending"],"properties":{"currency":{"type":"string"},"available":{"$ref":"#/components/schemas/CurrencyBalance"},"pending":{"$ref":"#/components/schemas/CurrencyBalance"}}},"CurrencyBalance":{"type":"object","required":["currency","amountCents","bySourceType"],"properties":{"currency":{"type":"string"},"amountCents":{"type":"integer"},"bySourceType":{"type":"object","nullable":true,"additionalProperties":{"type":"integer"},"description":"Reparto por source_type (card, bank_account, fpx) si Stripe lo desglosó."}}},"Payout":{"type":"object","description":"Retiro manual (payout `po_` de Stripe).","required":["id","stripeTransferId","stripePayoutId","amountCents","currency","status","createdAt"],"properties":{"id":{"type":"string"},"stripeTransferId":{"type":"string","nullable":true,"description":"Transferencia de un cobro al creador (`tr_`)."},"stripePayoutId":{"type":"string","nullable":true,"description":"Retiro manual de la cuenta Express (`po_`)."},"amountCents":{"type":"integer"},"currency":{"type":"string","example":"usd"},"status":{"type":"string","enum":["pending","in_transit","paid","failed","canceled"],"description":"Estado del payout en Stripe: `pending` (a enviar), `in_transit` (en camino), `paid`, `failed` o `canceled`."},"createdAt":{"type":"string","format":"date-time"}}},"Acreditacion":{"type":"object","description":"Transferencia recibida (`tr_`) hacia la cuenta Express.","required":["id","status","amountCents","currency","receivedCurrency","receivedGrossCents","receivedNetCents","exchangeRate","availableOn","description","createdAt"],"properties":{"id":{"type":"string"},"status":{"type":"string","enum":["acreditada","pendiente","reversed"]},"amountCents":{"type":"integer","description":"Monto del transfer en la moneda original del cobro (USD)."},"currency":{"type":"string","example":"usd"},"receivedCurrency":{"type":"string","description":"Moneda efectivamente acreditada en la cuenta Express."},"receivedGrossCents":{"type":"integer","description":"Bruto acreditado, neto del fee de Stripe no aplicado."},"receivedNetCents":{"type":"integer","description":"Neto acreditado en la cuenta del creador."},"exchangeRate":{"type":"number","nullable":true,"description":"Tasa de conversión aplicada por Stripe (null sin conversión)."},"availableOn":{"type":"string","format":"date-time","nullable":true},"description":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}},"Event":{"type":"object","description":"Evento de negocio con id determinístico (`evt_...`) idéntico al que se firma en los webhooks salientes.","required":["id","type","created","sandbox","data"],"properties":{"id":{"type":"string","description":"ID determinístico del evento (mismo que el webhook saliente)."},"type":{"type":"string","enum":["payment.completed","payment.failed","payment.refunded","payment.disputed","payout.paid"]},"created":{"type":"string","format":"date-time"},"sandbox":{"type":"boolean"},"data":{"type":"object","required":["object"],"properties":{"object":{"$ref":"#/components/schemas/EventObject"}}}}},"EventObject":{"type":"object","description":"Payload igual en forma al del webhook saliente.","properties":{"charge":{"$ref":"#/components/schemas/EventCharge"},"payment_link":{"type":"object","required":["id","slug","title","reference"],"properties":{"id":{"type":"string"},"slug":{"type":"string"},"title":{"type":"string","nullable":true},"reference":{"type":"string","nullable":true}}},"payout":{"type":"object","required":["id","amountCents","currency","status"],"properties":{"id":{"type":"string"},"amountCents":{"type":"integer"},"currency":{"type":"string"},"status":{"type":"string"}}}}},"EventCharge":{"type":"object","required":["id","reference","amountCents","currency","status","sandbox","paidAt"],"properties":{"id":{"type":"string"},"reference":{"type":"string","nullable":true},"amountCents":{"type":"integer"},"currency":{"type":"string"},"status":{"type":"string"},"sandbox":{"type":"boolean"},"paidAt":{"type":"string","format":"date-time","nullable":true},"refundedCents":{"type":"integer","nullable":true,"description":"Presente en events `payment.refunded`."},"recoveredCents":{"type":"integer","nullable":true,"description":"Presente en events `payment.refunded`."},"dispute":{"type":"object","nullable":true,"description":"Presente en events `payment.disputed`.","required":["status","outcome","amountCents"],"properties":{"status":{"type":"string"},"outcome":{"type":"string","nullable":true},"amountCents":{"type":"integer"}}}}},"PaymentLinkList":{"type":"object","required":["data","has_more","total_count","url"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PaymentLink"}},"has_more":{"type":"boolean"},"total_count":{"type":"integer"},"url":{"type":"string","description":"URL del request (utilizable para paginar con limit)."}}},"ChargeList":{"type":"object","required":["data","has_more","total_count","url"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Charge"}},"has_more":{"type":"boolean"},"total_count":{"type":"integer"},"url":{"type":"string"}}},"SubscriptionList":{"type":"object","required":["data","has_more","total_count","url"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Subscription"}},"has_more":{"type":"boolean"},"total_count":{"type":"integer"},"url":{"type":"string"}}},"PayoutList":{"type":"object","required":["data","has_more","total_count","url"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Payout"}},"has_more":{"type":"boolean"},"total_count":{"type":"integer"},"url":{"type":"string"}}},"AcreditacionList":{"type":"object","required":["data","has_more","total_count","url"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Acreditacion"}},"has_more":{"type":"boolean"},"total_count":{"type":"integer"},"url":{"type":"string"}}},"EventList":{"type":"object","required":["data","has_more","total_count","url"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Event"}},"has_more":{"type":"boolean"},"total_count":{"type":"integer"},"url":{"type":"string"}}}}}}