Saltar a contenido

Brand Partner API

Una REST API pequeña y predecible para crear pedidos de mintbot, consultar su estado y recibir eventos de ciclo de vida. JSON entra, JSON sale. Autenticación con bearer token. Escrituras idempotentes. Webhooks firmados.

Panel de acceso a la API Saltar a Webhooks


De un vistazo

URL base https://mint.mintbot.ai/api/v1
Autenticación Authorization: Bearer mo_live_…
Idempotencia Idempotency-Key: <uuid> en cada POST excepto POST /dns/records
Tipo de contenido application/json
Rate limit 120 req / 60 s por partner
Webhooks Firmados con HMAC-SHA256, reintentados hasta 7 veces

Autenticación

Cada solicitud envía una clave de API de partner en el header Authorization. Genera o rota la clave en el dashboard.

Authorization: Bearer mo_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json

La rotación no tiene ventana de gracia

Rotar la clave revoca la anterior de forma atómica. Planifica cambiar el valor en tu configuración antes de hacer clic en Rotate.

Idempotencia

Cada POST requiere un header Idempotency-Key, excepto POST /dns/records, que es idempotente de forma natural en la capa del proveedor y acepta reintentos sin él. Sirve cualquier UUID por solicitud distinta.

  • Guardamos la respuesta en caché durante 24 horas. Un reintento con la misma clave reproduce la respuesta original con Idempotent-Replay: true.
  • Reutilizar la misma clave con un body diferente devuelve 409 idempotency_key_mismatch.

Pedidos

Crear pedido

POST /orders

Crea un pedido y devuelve una URL de Stripe Checkout. Cuando se confirma el pago, mintbot provisiona el agente y el evento de ingresos correspondiente a la parte del partner se registra automáticamente.

Solicitud

{
  "tier": "mo_month",
  "duration_months": 1,
  "credit_usd": 10,
  "language": "en",
  "external_id": "your-side-id",
  "success_url": "https://your.app/thanks?id={ORDER_ID}",
  "cancel_url":  "https://your.app/cart",
  "webhook_url": "https://your.app/mintbot-webhook"
}
tier
Uno de mo_test (Test 24h), mo_month (Standard), mo_month_plus (Performance).
duration_months
Meses naturales de vida del servidor. Debe ser 1, 3 o 12. mo_test siempre dura 24 horas, envíes lo que envíes.
credit_usd
Opcional, por defecto 0. Uno de 0, 5, 10, 20, 50. Crédito de chat prefinanciado (USD) incluido con el pedido. Se suma al total del pedido y es para mintbot: no forma parte de tu margen.
language
Opcional. Afecta al locale de Stripe Checkout y al idioma del email de bienvenida.
external_id
Opcional. Se devuelve en cada webhook y respuesta de pedido: úsalo para vincular pedidos de mintbot con filas de tu propio sistema.
success_url · cancel_url
URLs de retorno de Stripe Checkout. {ORDER_ID} se sustituye en el servidor.
webhook_url
Opcional. Sustitución por pedido de la URL de webhook configurada a nivel de partner.
product_name
Opcional. El concepto que el cliente ve en Stripe Checkout (máx. 250 caracteres). Por defecto se genera una etiqueta TIER · N months.
price_usd
Opcional. Precio en USD que sustituye al de todo el pedido. No puede quedar por debajo de tu precio mayorista mínimo para ese nivel y duración; si no, 400 validation_error.

Respuesta — 201 Created

{
  "id": 42,
  "tier": "mo_month",
  "duration_months": 1,
  "credit_usd": 10,
  "amount_cents": 1200,
  "currency": "usd",
  "status": "pending",
  "checkout_url": "https://checkout.stripe.com/c/pay/cs_test_…",
  "panel_url": null,
  "expires_at": null,
  "language": "en",
  "external_id": "your-side-id",
  "created_at": "2026-05-16 09:58:00",
  "paid_at": null,
  "price_cents": 1200,
  "partner_cut_cents": 200
}

Si usas precios personalizados y no has fijado precio para este nivel, la respuesta incluye además "pricing_fallback": true.

Ejemplo

curl -X POST https://mint.mintbot.ai/api/v1/orders \
  -H "Authorization: Bearer $MINTBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "tier": "mo_month",
    "duration_months": 1,
    "credit_usd": 10,
    "success_url": "https://your.app/thanks?id={ORDER_ID}",
    "cancel_url":  "https://your.app/cart"
  }'
import os, uuid, requests

r = requests.post(
    "https://mint.mintbot.ai/api/v1/orders",
    headers={
        "Authorization": f"Bearer {os.environ['MINTBOT_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "tier": "mo_month",
        "duration_months": 1,
        "credit_usd": 10,
        "success_url": "https://your.app/thanks?id={ORDER_ID}",
        "cancel_url":  "https://your.app/cart",
    },
    timeout=10,
)
r.raise_for_status()
order = r.json()
redirect_to = order["checkout_url"]

Obtener pedido

GET /orders/{id}

Obtiene un solo pedido por su id de mintbot. Devuelve la misma forma que POST /orders.

curl https://mint.mintbot.ai/api/v1/orders/42 \
  -H "Authorization: Bearer $MINTBOT_API_KEY"

Listar pedidos

GET /orders

Lista paginada con cursor, de más reciente a más antiguo.

Parámetros de consulta

status
Opcional. Filtra por pending, completed o expired. Un pedido pagado sigue en completed durante todo el despliegue; el progreso del despliegue lo sigues con los webhooks agent.*.
cursor
Opcional. Pasa el next_cursor de la página anterior para continuar.
limit
Opcional. Tamaño de página, 50 por defecto, máximo 200.

Respuesta

{
  "items": [ /* OrderResponse … */ ],
  "next_cursor": "37"
}

next_cursor es null cuando no hay más páginas.


Renovar pedido

POST /orders/{id}/renew

Extiende un agente existente durante otros duration_months. Solo infraestructura: no se incluye crédito de chat nuevo. Devuelve un nuevo id de pedido y una URL nueva de Stripe Checkout.

Solicitud

{
  "duration_months": 1,
  "external_id": "your-side-id",
  "success_url": "https://your.app/thanks?id={ORDER_ID}",
  "cancel_url":  "https://your.app/account"
}

Suscripciones

Crear suscripción

POST /subscriptions

Abre un Stripe Checkout en modo suscripción: el cliente paga cada mes un paquete mensual y la suscripción se renueva sola. Igual que POST /orders, necesita un Idempotency-Key y una cuenta de partner en estado live. El precio mensual sale de tus precios para ese nivel.

Solicitud

{
  "tier": "mo_month",
  "language": "en",
  "success_url": "https://your.app/thanks",
  "cancel_url":  "https://your.app/cart",
  "customer_email": "customer@example.com",
  "external_id": "your-side-id"
}

customer_email, agent_id (un agente tuyo existente al que vincular la suscripción), external_id y product_name son opcionales.

Respuesta — 201 Created: contiene id, tier, currency, amount_cents, partner_cut_cents, status, checkout_url, current_period_end, cancel_at_period_end, agent_id, language, customer_email, external_id y created_at.

Obtener suscripción

GET /subscriptions/{id}

Devuelve la misma estructura que POST /subscriptions.


Ingresos

Leer ingresos

GET /revenue

Totales más los 200 eventos de ledger más recientes.

Parámetros de consulta

include_paid
Opcional, por defecto true. Ponlo en false para ver solo eventos que aún no se han pagado.

Respuesta

{
  "currency": "usd",
  "gross_cents": 12000,
  "partner_cut_cents": 2000,
  "mintbot_cut_cents": 10000,
  "unpaid_cents": 800,
  "events": [
    {
      "id": 7,
      "order_id": 42,
      "kind": "order_paid",
      "gross_cents": 1200,
      "partner_cut_cents": 200,
      "mintbot_cut_cents": 1000,
      "currency": "usd",
      "created_at": "2026-05-16 09:58:00",
      "payout_id": null,
      "payout_at": null
    }
  ]
}

Perfil del partner

Obtener perfil

GET /partner

Devuelve tu perfil de partner más el saldo no pagado: útil para mostrar ingresos dentro de tu propio panel de administración sin almacenarlos tú.

Respuesta

{
  "id": 12,
  "email": "you@kliendifirma.com",
  "pricing_currency": "usd",
  "balance_unpaid_cents": 800,
  "webhook_url": "https://your.app/mintbot-webhook",
  "api_key_prefix": "mo_live_a12b"
}

Configuración

Obtener configuración

GET /settings

Devuelve tu configuración completa de partner no secreta en un solo payload — modos elegidos, dominio apex, proveedor de DNS, proveedor de bot, repositorio de plantilla, patrón de subdominios (con una etiqueta de vista previa renderizada), precios por nivel, metadatos de la clave de API, metadatos del webhook, y el mismo estado de preparación / por sección que impulsa el banner del panel de MintOffice.

Diseñado para agentes de integración que necesitan descubrir cómo está configurado el partner sin scrapear el panel ni interrogar al operador. Un llamador típico es un agente de programación que instala tu configuración de marca blanca en un servidor nuevo — puede leer este endpoint, ramificar según readiness.status, y decirle al operador exactamente qué secciones aún necesitan atención.

Nunca se devuelve ningún secreto

Cualquier cosa que el partner haya proporcionado como credencial — token del bot de Telegram, clave de API de Zone.ee, secreto de firma de webhook, el propio texto plano de la clave de API — aparece solo como un booleano *_set. El secreto de webhook expone además el mismo prefijo de visualización de 8 caracteres que ya se muestra en el panel, así que un llamador puede confirmar cuál secreto está configurado sin ver nunca el valor.

Respuesta

{
  "id": 12,
  "email": "you@kliendifirma.com",
  "status": "live",
  "created_at": "2026-05-12 14:22:01",
  "onboarded_at": "2026-05-12 14:48:33",
  "activated_at": "2026-05-13 09:10:05",
  "last_login_at": "2026-05-20 00:51:18",
  "preferred_language": "en",
  "pricing_currency": "usd",
  "balance_unpaid_cents": 800,

  "domain": {
    "mode": "own",
    "client_apex_domain": "agent99.cc",
    "dns_provider": "zone_ee",
    "zone_ee_username": "you@kliendifirma.com",
    "zone_ee_api_key_set": true,
    "subdomain_prefix": "agent",
    "subdomain_numbering": "text",
    "subdomain_pad_width": 3,
    "subdomain_label_preview": "agent1"
  },

  "bot": {
    "mode": "own",
    "provider": "telegram",
    "telegram_bot_username": "agent99cc_bot",
    "telegram_bot_token_set": true
  },

  "template": {
    "mode": "default",
    "repo_url": null
  },

  "api": {
    "key_prefix": "mo_live_a12b",
    "key_created_at": "2026-05-13 09:11:42",
    "webhook_url": "https://your.app/mintbot-webhook",
    "webhook_secret_set": true,
    "webhook_secret_prefix": "ab12cd34"
  },

  "pricing": {
    "mode": "default",
    "currency": "usd",
    "tiers": {
      "mo_test":       { "price_cents": 100,  "partner_cut_cents": 0,   "updated_at": "2026-05-13 09:05:00" },
      "mo_month":      { "price_cents": 1200, "partner_cut_cents": 200, "updated_at": "2026-05-13 09:05:00" },
      "mo_month_plus": { "price_cents": 1800, "partner_cut_cents": 300, "updated_at": "2026-05-13 09:05:00" }
    },
    "allowed_credit_options": [10, 20, 50]
  },

  "readiness": {
    "ready": true,
    "missing_fields": [],
    "dns_blocker": null,
    "status": "live"
  },

  "section_status": {
    "domain":   { "status": "ok",      "mode": "own",     "issue": null, "dns_blocker": null },
    "bot":      { "status": "ok",      "mode": "own",     "issue": null, "dns_blocker": null },
    "template": { "status": "default", "mode": "default", "issue": null, "dns_blocker": null },
    "api":      { "status": "ok",      "mode": "active",  "issue": null, "dns_blocker": null },
    "all_ready": true
  }
}

Referencia de campos

status
Ciclo de vida del partner. Uno de onboarding, live, suspended. La Brand Partner API rechaza las escrituras mientras el estado no sea live; solo el equipo de mintbot te pasa a live.
domain.mode
own (aportas tu propio dominio raíz) o mintbot (mintbot te da un subdominio *.mintbot.ai). Con mintbot, los campos del proveedor DNS van vacíos a propósito, sin importar lo que el partner hubiera introducido antes.
domain.subdomain_label_preview
La etiqueta renderizada que recibiría el primer agente de cliente (agent1, 1, 001, …) dados los subdomain_prefix, subdomain_numbering y subdomain_pad_width actuales. Úsala para una vista previa de "esto es lo que verán tus clientes" sin reimplementar las reglas de numeración.
bot.mode
own (bot de Telegram propio del partner), mintbot (el bot de mintbot, para pruebas) o none (solo el panel, sin Telegram).
template.mode
default (sin agent customization repo — el agente corre sobre la base sencilla y sin marca) o own (el agent customization repo del partner en repo_url). Cuando es own, el propio VPS del agente clona ese repositorio después del despliegue estándar y ejecuta su install.sh / update.sh para aplicar tu persona, el tema del panel y cualquier herramienta extra encima de la base — central nunca ejecuta el repositorio en sí. Apunta repo_url a tu agent customization repo HTTPS de GitHub cuando quieras una marca personalizada.
api.webhook_secret_prefix
Primeros 8 caracteres del secreto de firma de webhook, presente solo cuando hay un secreto configurado. Permite a un llamador distinguir cuál secreto está en uso sin ver el valor completo. El panel muestra el mismo prefijo.
pricing.allowed_credit_options
Los paquetes de credit_usd que ofreces a tus clientes (se eligen en Settings → Pricing).
readiness.missing_fields
Lista de nombres de campos del panel aún requeridos antes de Go Live. Vacía cuando ready es true. Lo bastante estable como para impulsar una UI de "qué falta aún" en tu propio panel de administración.
readiness.dns_blocker
Un motivo legible cuando falla la comprobación del proveedor DNS (p. ej. partner has no zone.ee username configured). null en caso contrario.
section_status.*
Refleja los indicadores de estado de cada sección del panel. status es ok, warn o default (se usa un valor por defecto de mintbot); issue indica el campo que bloquea esa sección (p. ej. client_apex_domain, telegram_bot_token, dns_blocker), o null.

Ejemplo

curl https://mint.mintbot.ai/api/v1/settings \
  -H "Authorization: Bearer $MINTBOT_API_KEY"
import os, requests

r = requests.get(
    "https://mint.mintbot.ai/api/v1/settings",
    headers={"Authorization": f"Bearer {os.environ['MINTBOT_API_KEY']}"},
    timeout=10,
)
r.raise_for_status()
settings = r.json()

if not settings["readiness"]["ready"]:
    print("Still missing:", settings["readiness"]["missing_fields"])

La lista de paquetes vendibles para tu tienda — los paquetes de Brand Partner (mo_test / mo_month / mo_month_plus) unidos con tu precio resuelto, en tu moneda de precios. Renderiza tus tarjetas de plan a partir de esto en lugar de fijar a mano los slugs de nivel y los precios: los paquetes retirados desaparecen por sí solos, y un cambio de precio o moneda que hagas en el panel llega a tu tienda dentro de su ventana de caché — sin redesplegar.

Este es el endpoint que evita que una tienda anuncie los paquetes de ayer. El portal de referencia lo llama en cada render de landing / /buy / /extend (cacheado, con un fallback persistente).

Los textos de visualización siguen siendo tuyos

display_name / description / featured son los metadatos canónicos de los paquetes de mintbot. Si quieres renombrar un paquete o reescribir su descripción para tu marca, sobrescríbelo en tu propia tienda (el portal de referencia incluye un mapa PLAN_OVERRIDES exactamente para esto) — el catálogo es la fuente de verdad de qué paquetes existen y cuánto cuestan, no de tu copy de marketing.

GET /api/v1/catalog

Sin parámetros. Solo autenticación con bearer — no hay puerta de activación, así que una tienda puede renderizar el catálogo antes de que tu cuenta de partner pase a live.

Respuesta

{
  "currency": "usd",
  "packages": [
    {
      "tier": "mo_test",
      "display_name": "Test 24h",
      "description": "2 vCPU · 4 GB RAM · 40 GB SSD · 24-hour evaluation. EU.",
      "featured": false,
      "default_credit_usd": 5,
      "durations": [
        { "months": 1, "label": "24 hours", "price_cents": 0 }
      ],
      "subscription": { "available": false, "price_cents": null }
    },
    {
      "tier": "mo_month",
      "display_name": "Standard",
      "description": "2 vCPU · 4 GB RAM · 40 GB SSD, monthly. EU.",
      "featured": false,
      "default_credit_usd": 10,
      "durations": [
        { "months": 1,  "label": "1 month",   "price_cents": 1500 },
        { "months": 3,  "label": "3 months",  "price_cents": 4500 },
        { "months": 12, "label": "12 months", "price_cents": 18000 }
      ],
      "subscription": { "available": true, "price_cents": 1500 }
    }
  ],
  "credit_options": [
    { "usd": 5,  "price_cents": 500 },
    { "usd": 10, "price_cents": 1000 }
  ]
}
Campo Significado
currency Tu moneda de precios. Cada price_cents de abajo está denominado en ella.
packages[] Una entrada por paquete público, en orden de oferta/visualización.
tier El slug que pasas a POST /orders / POST /subscriptions.
display_name / description / featured Copy de visualización canónico (sobrescríbelo en tu tienda si quieres).
default_credit_usd La pista de crédito LLM incluido del paquete (informativo).
durations[] Las duraciones de configuración para las que se puede comprar este paquete. months es el valor que envías como duration_months; price_cents es lo que el pedido facturará. Los paquetes mensuales anuncian 1 / 3 / 12 meses; mo_test anuncia una única duración de 24 horas.
subscription available es true para los paquetes que pueden comprarse como auto-renovación mensual vía POST /subscriptions; price_cents es el importe mensual. mo_test nunca admite suscripción.
credit_options[] Cada paquete de credit_usd con su precio en tu moneda (usd = importe del crédito, price_cents = lo que suma al total del pedido). Los paquetes que ofreces están en pricing.allowed_credit_options de GET /settings.

Los precios coinciden con lo que se te facturará

Cada price_cents se calcula con el mismo resolutor de precios que usa el endpoint de pedidos, así que el precio de una tarjeta siempre equivale al total final de Stripe Checkout para ese (tier, duration_months).

Ejemplo

curl -s https://mint.mintbot.ai/api/v1/catalog \
  -H "Authorization: Bearer $MINTBOT_API_KEY" | jq
import os, requests

r = requests.get(
    "https://mint.mintbot.ai/api/v1/catalog",
    headers={"Authorization": f"Bearer {os.environ['MINTBOT_API_KEY']}"},
    timeout=10,
)
r.raise_for_status()
catalog = r.json()

for pkg in catalog["packages"]:
    month1 = next(d for d in pkg["durations"] if d["months"] == 1)
    print(pkg["display_name"], month1["price_cents"], catalog["currency"])

Registros DNS

Las integraciones de brand partner a menudo necesitan provisionar registros DNS en el dominio apex del partner — apuntando @, www, o subdominios por cliente al servidor correcto. MintOffice expone un proxy ciego al proveedor para que puedas hacer esto sin manejar nunca la clave de API del proveedor upstream en tu propia infraestructura. Las credenciales que guardaste durante el onboarding (hoy: zone.ee; cloudflare y route53 están en la hoja de ruta detrás de la misma forma) permanecen cifradas en MintOffice; tu llamada llega con Bearer mo_live_… y hacemos la llamada upstream en tu nombre.

El alcance es el propio apex configurado del partner (domain.client_apex_domain de GET /settings). No puedes tocar un registro en una zona que no te pertenece.

Si tu proveedor DNS es Manual, el proxy no cambia nada: las escrituras no hacen nada y los listados vuelven vacíos.

Hoy el proxy admite las operaciones que usa el propio pipeline de despliegue — registros A (upsert / list / delete por id). El soporte de CNAME / TXT / MX seguirá la misma forma cuando la abstracción del proveedor subyacente los incorpore.

Aquí no se requiere Idempotency-Key

A diferencia de los endpoints de creación de pedidos, los upserts de DNS son naturalmente idempotentes a nivel de proveedor (reejecutar con el mismo (subdomain, ip) es un no-op y los duplicados obsoletos se barren en cada llamada). No necesitas enviar el header Idempotency-Key — los reintentos siempre convergen en un único registro A por FQDN.

Insertar o actualizar un registro A

POST /dns/records

Apunta de forma idempotente <subdomain>.<your-apex> a una dirección IPv4. Pasa "" (o "@") para el propio apex.

Solicitud

{
  "subdomain": "www",
  "ip": "203.0.113.10"
}

subdomain acepta:

  • "" o "@" — el propio apex (example.com).
  • Una sola etiqueta DNS — "www", "api" — para <label>.<apex>.
  • Sub-etiquetas con puntos — "api.eu" — para subdominios anidados (api.eu.example.com).

ip debe ser una dirección IPv4 con puntos. Cada etiqueta es [a-z0-9_-], 1–63 caracteres, sin - al principio ni al final.

Respuesta — 200

{
  "apex": "example.com",
  "provider": "zone_ee",
  "fqdn": "www.example.com",
  "records": [
    { "id": "9182734", "name": "www.example.com", "destination": "203.0.113.10" }
  ]
}

La lista records posterior a la escritura siempre refleja la vista actual del proveedor upstream del FQDN tras un barrido de duplicados — así que una respuesta exitosa con exactamente un registro es la señal de camino feliz de que no queda nada obsoleto.

Listar registros

GET /dns/records

Devuelve cada registro A del apex configurado. Añade ?name=<label-or-fqdn> para filtrar a un solo FQDN — el valor puede ser una etiqueta desnuda ("www"), un subdominio con puntos ("api.eu"), o el FQDN completo ("www.example.com").

Respuesta — 200

{
  "apex": "example.com",
  "provider": "zone_ee",
  "items": [
    { "id": "9182734", "name": "example.com",     "destination": "203.0.113.10" },
    { "id": "9182735", "name": "www.example.com", "destination": "203.0.113.10" }
  ]
}

Eliminar un registro

DELETE /dns/records/{record_id}

record_id es el identificador del proveedor devuelto por GET /dns/records (zone.ee emite ids enteros, convertidos a string en la frontera de la API). Eliminar un registro que ya no existe se trata como un éxito — el endpoint es idempotente del lado del cliente.

Respuesta — 200

{ "deleted": true, "id": "9182734" }

Errores

HTTP error.code Causa
401 unauthenticated / invalid_api_key Token Bearer ausente / incorrecto.
422 validation_error Subdominio incorrecto o destino no IPv4.
422 dns_not_configured El partner no tiene client_apex_domain o faltan las credenciales del proveedor configurado. Corrígelo en Settings → Domain.
422 dns_provider_unsupported El proveedor de DNS del partner aún no está conectado al proxy de MintOffice (p. ej. el placeholder de cloudflare). Cámbiate a zone_ee por ahora.
502 dns_provider_error El proveedor upstream devolvió un 4xx/5xx, o el transporte falló. El message del error lleva el estado upstream y un extracto corto, con secretos redactados, del cuerpo upstream. Seguro de reintentar.

Ejemplo

# Point apex + www at your server's IP.
curl https://mint.mintbot.ai/api/v1/dns/records \
  -H "Authorization: Bearer $MINTBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"subdomain": "",    "ip": "203.0.113.10"}'

curl https://mint.mintbot.ai/api/v1/dns/records \
  -H "Authorization: Bearer $MINTBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"subdomain": "www", "ip": "203.0.113.10"}'

# Verify.
curl "https://mint.mintbot.ai/api/v1/dns/records" \
  -H "Authorization: Bearer $MINTBOT_API_KEY"
import os, requests

URL = "https://mint.mintbot.ai/api/v1/dns/records"
H = {"Authorization": f"Bearer {os.environ['MINTBOT_API_KEY']}"}

for sub in ("", "www"):
    r = requests.post(URL, headers=H, json={"subdomain": sub, "ip": "203.0.113.10"})
    r.raise_for_status()
    print(sub or "@", r.json()["records"])

Webhooks

Cuando le ocurre algo a un pedido, hacemos POST de un evento JSON firmado a tu URL de webhook configurada.

Los webhooks son opcionales

La Webhook URL y el Webhook signing secret en Settings → API access son totalmente opcionales: déjalos vacíos durante el onboarding si aún no tienes un receptor. Las entregas solo se envían cuando ambos están configurados; con una URL pero sin secreto de firma no se envía ningún evento. El dashboard y el resto de la API funcionan perfectamente sin webhooks; en su lugar puedes consultar GET /api/v1/orders/{id} para seguir el ciclo de vida. Añade la URL (y genera el secreto) cuando estés listo; no hace falta reactivar nada.

Tipos de evento

Evento Cuándo se dispara
order.created Sesión de Stripe Checkout creada, esperando pago.
order.paid Stripe confirmó el pago. Evento de ingresos registrado.
order.cancelled El pedido agotó el tiempo o fue cancelado explícitamente.
agent.provisioning_started El pipeline de deploy empezó para este pedido.
agent.ready El deploy tuvo éxito. El payload contiene panel_url y expires_at.
agent.failed Falló el pipeline de despliegue. error_step indica el paso que falló y error_message es un resumen breve.
agent.expired El TTL del agente terminó. El partner puede renovar vía POST /orders/{id}/renew.
agent.resumed Un agente que había expirado se renovó dentro de su periodo de gracia y vuelve a estar activo. Contrapartida de recuperación de agent.expired.
subscription.created · subscription.activated · subscription.updated · subscription.cancelled Ciclo de vida de una suscripción creada con POST /subscriptions.
partner.activated · partner.deactivated El equipo de mintbot activó tu cuenta o la sacó de live (suspended en el payload indica si se suspendió o volvió a onboarding).

Entrega y reintentos

  • Hasta 7 intentos con un calendario exponencial: 0s, 30s, 2m, 10m, 1h, 6h, 24h.
  • Tras el último intento, la entrega se marca como exhausted y deja de reintentarse.
  • Responde con un estado 2xx en un plazo de 10 segundos para confirmar la recepción.

Headers de solicitud

Content-Type: application/json
User-Agent: mintbot-webhook/1.0
X-Mintbot-Signature: t=<unix_ts>,v1=<hex_hmac_sha256>
X-Mintbot-Event-Id: evt_42_order.paid_1747371234567
X-Mintbot-Event-Type: order.paid

Payload de ejemplo — order.paid

{
  "id": 42,
  "tier": "mo_month",
  "duration_months": 1,
  "currency": "usd",
  "amount_cents": 1200,
  "status": "completed",
  "external_id": "your-side-id",
  "created_at": "2026-05-16 09:58:00",
  "paid_at": "2026-05-16 10:02:14",
  "expires_at": null
}

Verificar la firma

La firma es HMAC-SHA256(secret, "{timestamp}.{raw_body}"). Verifica siempre el cuerpo raw de la solicitud: volver a serializar el JSON parseado romperá la comparación.

import hmac, hashlib, time

def verify(secret: str, body: bytes, header: str, tolerance: int = 300) -> bool:
    try:
        ts_part, v1_part = header.split(",", 1)
        ts  = int(ts_part.split("=", 1)[1])
        sig = v1_part.split("=", 1)[1]
    except Exception:
        return False
    if abs(int(time.time()) - ts) > tolerance:
        return False
    expected = hmac.new(
        secret.encode(),
        f"{ts}.".encode() + body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, sig)
const crypto = require("crypto");

function verify(secret, rawBody, header, toleranceSec = 300) {
  const [tsPart, v1Part] = header.split(",");
  const ts  = Number(tsPart.split("=")[1]);
  const sig = v1Part.split("=")[1];
  if (Math.abs(Date.now() / 1000 - ts) > toleranceSec) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${ts}.`)
    .update(rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(sig, "hex"),
  );
}

Receptores idempotentes

Usa X-Mintbot-Event-Id como clave de deduplicación: los reintentos reutilizan el mismo id, así que un INSERT … ON CONFLICT DO NOTHING a nivel de fila sobre esa columna mantiene seguro tu handler.

Portal de referencia

Hay una referencia completa y ejecutable en mintbot-ai/partner-portal-example: FastAPI + SQLite, en Docker, con una marca de ejemplo llamada ExampleAI a la que haces fork y sustituyes por la tuya. La visión de arquitectura —cómo encajan la tienda, esta API, el worker de despliegue y el repo de personalización— está en MintOffice — Técnico. El portal implementa:

  • las páginas de inicio, selección de plan, agradecimiento y cancelación,
  • POST /buy → POST /api/v1/orders → redirección a Stripe,
  • POST /webhooks/mintoffice con el verificador HMAC descrito arriba,
  • un navegador de eventos en /admin protegido con HTTP Basic para revisar las entregas de un vistazo.

Haz fork, define PARTNER_BRAND, MINTOFFICE_API_KEY y MINTOFFICE_WEBHOOK_SECRET en .env, ejecuta docker compose up y apunta la URL de webhook de tu cuenta de partner a https://<your-host>/webhooks/mintoffice. La guía completa está en MintOffice → Portal de referencia.


Errores

Cada respuesta de error lleva un code estable sobre el que puedes ramificar programáticamente. El campo message es una pista legible para humanos y puede cambiar entre versiones. El request_id replica el header de respuesta X-Request-Id; inclúyelo en tickets de soporte.

{
  "error": {
    "code": "invalid_api_key",
    "message": "API key is unknown or revoked.",
    "request_id": "f0c2d6c4-…"
  }
}
Código Significado
unauthenticated Header Authorization ausente o mal formado.
invalid_api_key La API key es desconocida o fue rotada.
rate_limited Se superó el rate limit por partner o por IP; consulta Retry-After.
missing_idempotency_key Solicitud POST sin Idempotency-Key.
idempotency_key_mismatch Misma clave reutilizada con un body de solicitud diferente.
validation_error El body de la solicitud no pasó la validación del esquema.
not_found El recurso no existe o no pertenece a tu partner.
partner_not_live HTTP 403: una escritura (POST /orders, /orders/{id}/renew, /subscriptions) con tu cuenta fuera de live. El cuerpo incluye status y, si estás en onboarding, missing_fields + dns_blocker.
conflict HTTP 409: una solicitud con el mismo Idempotency-Key aún se está procesando. Reintenta tras Retry-After.
unpriced_tier HTTP 409: el nivel no tiene precio que cobrar.
internal_error Error inesperado del servidor. Incluye el request_id al contactar con soporte.
payment_gateway_error Stripe rechazó la creación de la sesión de Checkout.
dns_not_configured POST /dns/records contra un partner sin apex o sin credenciales DNS. Corrígelo en Settings → Domain.
dns_provider_unsupported El valor dns_provider del partner aún no está conectado al proxy de MintOffice.
dns_provider_error El proveedor DNS upstream devolvió un error; el mensaje incluye un extracto sin secretos.
quota_exceeded HTTP 429, solo en el entorno DEV (mint.mintbot.dev); LIVE no tiene límite por partner. El partner ha agotado todos sus slots de despliegue (10 por defecto, 20 tras la tarifa de alta de Brand Partner). Contacta con el soporte de mintbot para ampliar el límite. Lo devuelven POST /orders y POST /orders/{id}/renew. El cuerpo incluye deploys_used + deploy_quota.

Rate limits

  • 120 solicitudes / ventana móvil de 60 s, por partner. Ráfagas y estado estable comparten el mismo bucket.
  • 60 solicitudes / 60 s, bucket separado por IP para solicitudes no autenticadas y con autenticación fallida: evita que un ataque de fuerza bruta al bearer consuma la cuota del partner.
  • Las solicitudes excedentes devuelven 429 rate_limited con un header Retry-After (segundos que debes esperar).

¿Necesitas ayuda?

Esta documentación está escrita para los partners que realmente usan la API. Si algo falta, resulta confuso o está desactualizado, coméntaselo a tu agente de mintbot: nos reenviará el feedback y actualizaremos la página.