API de cupones
Un programa de cupones es la receta a partir de la cual un mensaje emite códigos personales ({{ coupons.<slug>.code }}). Joryio crea cada código en la tienda WooCommerce, Shopify, Magento o PrestaShop conectada (o lo genera y sigue por sí sola cuando el programa no tiene tienda) y reconoce el pedido que lo usó. El comportamiento de los programas se describe en Códigos de cupón.
Todos los endpoints de esta página son relativos a la URL base: https://api-eu1.joryio.com/api - consulta Resumen de la API.
Permisos
| Permiso | Permite |
|---|---|
campaigns:read | Listar y leer programas, el informe y las tiendas conectadas |
campaigns:write | Crear programas, cambiar programas que aún no han emitido códigos, borrarlos, emitir un código |
settings:write | Necesario además para cambiar un programa activo con códigos - uno que ya emitió códigos reales |
users:read | Listar códigos emitidos (los códigos pertenecen a personas) |
El objeto programa
{
"id": "6c0f3c1e-8a52-4a8e-9a39-2a1d7f0b9a11",
"name": "Welcome 10%",
"slug": "welcome10",
"description": "10% off the first order",
"integrationId": "0b8d6f2a-5f43-4e0a-b9a3-7a8b2d1c4e55",
"platform": "woocommerce",
"storeName": "My Store",
"source": "personal",
"status": "active",
"rules": {
"discountType": "percent",
"amount": 10,
"currency": null,
"minimumSubtotal": 50,
"minimumQuantity": null,
"scope": { "kind": "all" },
"excludeSaleItems": false,
"usageLimit": 1,
"oncePerCustomer": true,
"combinable": false,
"expiry": { "mode": "relative", "amount": 14, "unit": "days", "endOfDay": true }
},
"rulesVersion": 1,
"codeFormat": { "length": 10, "charset": "unambiguous", "prefix": "WELCOME-", "suffix": "", "separatorEvery": null },
"fallback": { "policy": "skip", "sharedCode": null },
"reuseOpenCode": true,
"lockToPerson": false,
"live": true,
"sampleCode": "WELCOME-PREVIEW",
"storeError": null,
"stats": { "issued": 1240, "redeemed": 311, "revenue": 18420.5, "currency": "EUR" },
"createdAt": "2026-10-04T08:00:00.000Z",
"updatedAt": "2026-10-04T08:00:00.000Z"
}
| Campo | Valores |
|---|---|
integrationId | Una tienda conectada, o null: Joryio genera y sigue los códigos sin tienda |
status | draft, active, paused, archived - solo active emite códigos |
rules.discountType | percent, fixed_cart, fixed_product (no en PrestaShop: 400), free_shipping |
rules.scope | { "kind": "all" }, o products (WooCommerce, Shopify, PrestaShop) / categories (WooCommerce, Magento, PrestaShop) / collections (Shopify) con ids - los ids de la propia tienda; products en Magento responde 400 |
rules.minimumQuantity | Solo Shopify |
rules.excludeSaleItems | Solo WooCommerce |
rules.usageLimit | Usos por código, por defecto 1; null = ilimitado |
rules.expiry | { "mode": "none" }, { "mode": "fixed", "date": "2026-12-31T23:59:59Z" }, o { "mode": "relative", "amount": 14, "unit": "days" | "hours", "endOfDay": true } |
codeFormat.charset | unambiguous (letras y dígitos sin caracteres parecidos, predeterminado), letters, digits; length 6-20 |
fallback.policy | skip (predeterminado), shared_code (necesita sharedCode), send_without |
lockToPerson | WooCommerce, Magento y PrestaShop (la cuenta de cliente de la persona) |
live | true en cuanto el programa ha emitido un código real (no de prueba) |
rulesVersion | Sube cuando cambian las reglas de un programa activo con códigos; los códigos ya emitidos conservan las reglas con las que se crearon |
Programas
Listar programas
GET /coupons/programs?status=active&includeArchived=false
Devuelve un array de objetos programa. Los programas archivados se omiten salvo con includeArchived=true.
Crear un programa
POST /coupons/programs
El cuerpo es el objeto programa sin los campos de solo lectura (id, platform, storeName, rulesVersion, stock, live, sampleCode, storeError, stats, fechas). slug es [a-z0-9_], único por espacio de trabajo. status puede ser draft (predeterminado) o active.
{
"name": "Welcome 10%",
"slug": "welcome10",
"integrationId": "0b8d6f2a-5f43-4e0a-b9a3-7a8b2d1c4e55",
"rules": { "discountType": "percent", "amount": 10, "scope": { "kind": "all" }, "usageLimit": 1, "oncePerCustomer": true, "combinable": false, "expiry": { "mode": "relative", "amount": 14, "unit": "days" } },
"codeFormat": { "length": 10, "charset": "unambiguous", "prefix": "WELCOME-" },
"fallback": { "policy": "skip" },
"reuseOpenCode": true,
"lockToPerson": false
}
Leer un programa
GET /coupons/programs/{id}
Actualizar un programa
PATCH /coupons/programs/{id}
Cualquier campo del cuerpo de creación, o status (draft, active, paused, archived). Cambiar un programa activo con códigos necesita settings:write además de campaigns:write.
Borrar un programa
DELETE /coupons/programs/{id}
204 para un programa que nunca emitió un código real. Un programa activo con códigos responde 409 - archívalo en su lugar (PATCH con "status": "archived").
Informe del programa
GET /coupons/programs/{id}/report
{
"programId": "6c0f3c1e-8a52-4a8e-9a39-2a1d7f0b9a11",
"totals": { "issued": 1240, "redeemed": 311, "redemptionRate": 0.2508, "revenue": 18420.5, "discount": 1842.05, "currency": "EUR", "expired": 402, "failed": 3, "redeemedBySomeoneElse": 12 },
"bySource": [
{ "kind": "journey", "id": "a1b2c3d4-0000-4000-8000-000000000001", "name": "Welcome journey", "issued": 980, "redeemed": 270, "revenue": 15900 }
],
"daily": [{ "date": "2026-10-04", "issued": 52, "redeemed": 14 }]
}
Los ingresos y el descuento están ambos en la moneda de informes del espacio de trabajo (currency): un pedido en otra moneda se convierte con el tipo de cambio de ese mismo pedido, el mismo con el que se convirtieron sus ingresos. Los códigos de prueba no cuentan.
Emitir un código
POST /coupons/programs/{id}/issue
Entrega un código de un programa activo a un contacto - para socios que muestran el código en su propio canal. Identifica al contacto con uno de:
| Campo | Descripción |
|---|---|
contactId | El id interno del contacto en Joryio |
externalId | Tu id de usuario |
email | El email del contacto |
issueKey | Clave de idempotencia opcional: el mismo issueKey devuelve siempre el mismo código |
{ "externalId": "customer-123", "issueKey": "order-confirmation-9876" }
Respuesta:
{
"issued": true,
"reused": false,
"applyUrl": "https://shop.example.com/?jry_coupon=WELCOME-7KQ4M2XH9P",
"issue": {
"id": "1f6a...", "programId": "6c0f...", "programSlug": "welcome10", "programName": "Welcome 10%",
"contactId": "66a1f2c3d4e5f6a7b8c9d0e1", "code": "WELCOME-7KQ4M2XH9P", "status": "issued", "isTest": false,
"sourceKind": "api", "campaignId": null, "canvasId": null, "nodeId": null, "channel": null,
"issuedAt": "2026-10-04T09:00:00.000Z", "expiresAt": "2026-10-18T23:59:59.000Z",
"redeemedAt": null, "redeemedOrderId": null, "redeemedRevenue": null, "failureReason": null
}
}
Con reuseOpenCode activado en el programa, un contacto que todavía tiene un código abierto del programa recibe ese código ("reused": true). Cuando no se puede emitir un código la respuesta es { "issued": false, "reason": "..." } - por ejemplo program_paused, store_error, store_permission_missing (la tienda Shopify aún no ha aprobado write_discounts), store_reconnect_required (WooCommerce se añadió al sitio WordPress después de conectarlo - desconecta y vuelve a conectar el plugin de Joryio).
Códigos emitidos
GET /coupons/issues?contactId=66a1f2c3d4e5f6a7b8c9d0e1&programId=&status=&limit=50&offset=0
Devuelve { "items": [issue, ...], "total": 3 }, del más reciente al más antiguo. status es pending, issued, failed, redeemed, expired o revoked. limit hasta 200.
Tiendas conectadas
GET /coupons/stores
[
{
"integrationId": "0b8d6f2a-5f43-4e0a-b9a3-7a8b2d1c4e55",
"platform": "shopify",
"storeName": "My Shop",
"storeUrl": "https://my-shop.myshopify.com",
"status": "connected",
"capabilities": { "lockToPerson": false, "perCodeExpiry": false, "percentCap": false, "scopes": ["all", "products", "collections"] },
"missingPermission": "write_discounts"
}
]
missingPermission indica lo que el comerciante debe conceder antes de poder crear códigos (Shopify: abre Integrations → Shopify → Approve new permissions).
Orígenes de los códigos
El campo source de un programa indica de dónde salen sus códigos. Se fija al crearlo (por defecto personal) y no puede cambiar una vez que el programa tiene códigos. Consulta Orígenes de los códigos.
source | Códigos | sourceConfig |
|---|---|---|
personal | Un código por persona, creado cuando un mensaje lo necesita | {} |
bank | Creados en la tienda por adelantado y repartidos uno por persona. Necesita integrationId. | { "bankTargetSize": 5000 } - cuántos códigos sin usar mantener listos (0 = solo a petición y antes de las campañas) |
uploaded | Subidos por ti (Subir códigos) y repartidos uno por persona; nunca se crean en una tienda. integrationId opcional; lockToPerson no permitido. | {} |
shared | Un código para todos, registrado por emisión | { "sharedCode": "SUMMER10", "createInStore": true } - createInStore necesita integrationId; false solo registra un código que ya existe |
Los programas también incluyen:
| Campo | Valores |
|---|---|
lowStock | { "threshold": 500, "percent": null } - para bank y uploaded: alerta cuando quedan menos de threshold códigos sin usar, o menos del percent (1-99) % de todos los códigos cargados. null desactiva una regla. |
stock | Solo lectura. { "available": 1830, "total": 5000, "level": "ok" } para bank y uploaded (level: ok, low, empty); null para los demás orígenes. |
En un programa uploaded, y en uno shared con createInStore: false, las rules solo describen el valor que muestra {{ coupons.<slug>.value }} - la tienda nunca las ve.
{
"name": "Partner coffee voucher",
"slug": "coffee",
"source": "uploaded",
"integrationId": null,
"lowStock": { "threshold": 200 },
"rules": { "discountType": "fixed_cart", "amount": 3, "currency": "EUR", "scope": { "kind": "all" }, "usageLimit": 1, "oncePerCustomer": true, "combinable": false, "expiry": { "mode": "none" } },
"fallback": { "policy": "send_without" }
}
Existencias
GET /coupons/programs/{id}/stock
{
"programId": "6c0f3c1e-8a52-4a8e-9a39-2a1d7f0b9a11",
"source": "bank",
"tracked": true,
"available": 1830,
"total": 5000,
"used": 3170,
"level": "ok",
"lowStock": { "threshold": 500, "percent": null },
"bank": { "targetSize": 5000, "filling": false, "lastRunAt": "2026-10-04T09:15:00.000Z", "lastAdded": 250, "lastError": null }
}
tracked es false (y los recuentos 0) en los programas personal y shared. bank solo aparece en los bancos de códigos; filling es true mientras se ejecuta una reposición.
Subir códigos
POST /coupons/programs/{id}/codes
POST /coupons/programs/{id}/codes?dryRun=true
Añade códigos a un programa uploaded. Envía o bien un archivo CSV como multipart/form-data (campo file) o bien JSON { "text": "...", "fileName": "optional.csv" } con los códigos pegados. Cada subida se suma; los códigos anteriores se quedan. dryRun=true valida y devuelve el mismo informe sin guardar nada - la vista previa.
{ "text": "code,expires_at,value\nCOFFEE-001,2026-12-31,Free coffee\nCOFFEE-002,,Free coffee" }
| Columna | Obligatoria | Formato |
|---|---|---|
code | Sí | 3-64 caracteres: letras, dígitos y - _ . # / +; se comparan sin distinguir mayúsculas y minúsculas |
expires_at | No | YYYY-MM-DD (el final de ese día, UTC) o una fecha y hora ISO; debe estar en el futuro |
value | No | Hasta 100 caracteres; se muestra como {{ coupons.<slug>.value }} para este código |
La fila de encabezado es opcional (sin ella las columnas se leen en este orden); los separadores coma, punto y coma y tabulador se detectan en la primera línea. Límites: 100.000 filas y 10 MB por subida, 2.000.000 de códigos por programa.
Respuesta (CouponUploadReport):
{
"uploadId": "9d2e4b10-1c3f-4b7a-8a55-0e6f2c9d1a77",
"dryRun": false,
"totalRows": 3,
"accepted": 2,
"rejected": 1,
"duplicates": 1,
"truncated": false,
"maxRows": 100000,
"hasHeader": true,
"preview": [{ "line": 2, "code": "COFFEE-001", "expiresAt": "2026-12-31T23:59:59.000Z", "value": "Free coffee" }],
"rejectedRows": [{ "line": 4, "code": "COFFEE-001", "reason": "duplicate_in_file", "message": "The same code appears earlier in this file." }]
}
uploadId es null en una prueba (dry run). preview contiene las 10 primeras filas aceptadas y rejectedRows las 500 primeras rechazadas. truncated es true cuando el archivo tenía más filas que maxRows (el resto no se leyó).
reason | Significado |
|---|---|
missing_code | La fila no tiene código |
invalid_code | No son 3-64 letras, dígitos y - _ . # / + |
invalid_expiry | La caducidad no es una fecha |
expired | La caducidad ya pasó |
value_too_long | El valor tiene más de 100 caracteres |
duplicate_in_file | El mismo código aparece antes en el archivo |
already_in_program | El programa ya tiene el código |
already_exists | Otro programa del espacio de trabajo tiene el código |
over_limit | El programa está lleno |
Un programa con otro origen responde 400.
Subidas anteriores
GET /coupons/programs/{id}/uploads
Las últimas 100 subidas, de la más reciente a la más antigua: { "id", "kind", "fileName", "totalRows", "accepted", "rejected", "duplicates", "rejectedRows", "createdBy", "createdAt" }.
Generar códigos del banco
POST /coupons/programs/{id}/bank/generate
{ "count": 5000 }
Crea count (1-100.000) códigos nuevos en la tienda para un programa bank. El trabajo se ejecuta en segundo plano: la respuesta es 202 Accepted con { "started": true, "requested": 5000 } al instante; síguelo con Existencias (bank.filling, bank.lastAdded, bank.lastError). Una segunda petición mientras se ejecuta una reposición responde 400. Shopify añade los códigos al descuento del programa, WooCommerce crea un cupón por código, Magento los genera bajo la regla de precio de carrito del programa con su formato de código y PrestaShop crea una regla de carrito por código.
Antes de que se envíe una campaña, cada banco que usa su contenido se repone automáticamente hasta cubrir la audiencia. Launch Review bloquea una campaña cuya audiencia es mayor que los códigos de un banco o una lista (coupon_stock_short) y avisa cuando el envío deja menos códigos que la alerta de pocas existencias (coupon_stock_low).
Errores
| Estado | Cuándo |
|---|---|
400 | Validación - por ejemplo lockToPerson en un programa de Shopify (Shopify decide quién puede usar un descuento por descuento, no por código), un slug ya usado o que no es [a-z0-9_], shared_code sin sharedCode, una fecha de caducidad pasada |
403 | Falta un permiso - incluido settings:write al cambiar un programa activo con códigos |
404 | El programa no está en este espacio de trabajo |
409 | Borrar un programa activo con códigos |