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

# API de Reval: consultá tus suscripciones desde tus propias herramientas

> API GraphQL sobre suscripciones, pedidos, clientes, planes y cajas: consultas y acciones (pausar, cancelar, saltear, cambiar productos, dirección, link al portal, links de checkout). API keys por tienda, idempotencia, paginación, filtros, límites y ejemplos.

La API de Reval expone los datos de suscripciones de tu tienda para que los lleves a tu BI, tu data warehouse o tus automatizaciones, y te deja **operarlas**: pausar, cancelar, saltear, cambiar productos y dirección. Es **GraphQL**, y cada clave ve y opera únicamente la tienda que la generó.

> [!NOTE]
>
> Las consultas están en las secciones siguientes; las acciones, en [Acciones](#acciones). La lista completa de tipos y campos está en la [referencia del esquema](https://docs.appreval.com/desarrolladores/api-referencia.md), y hay [recetas](https://docs.appreval.com/desarrolladores/recetas.md) para los casos más comunes. Para enterarte de lo que pasa sin consultar, están los [webhooks](https://docs.appreval.com/desarrolladores/webhooks.md).

***

## Autenticación

Las claves se generan en **Reval → Integraciones** en tu admin de Shopify. Ponele el nombre de la herramienta que la va a usar («Triple Whale», «Looker») y guardá la clave en el momento: **se muestra completa una sola vez**. Después, la lista solo muestra el prefijo.

Mandá la clave en cada request como Bearer token:

```http theme={null}
POST https://api.appreval.com/api/graphql
Authorization: Bearer reval_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Content-Type: application/json
```

Si tu cliente no te deja usar `Authorization`, el header `X-Reval-Api-Key` es equivalente.

* Una clave revocada deja de funcionar al instante. Las consultas reciben `401` con el código `UNAUTHENTICATED`.
* Reval guarda solo un hash de la clave. Si la perdés, revocala y generá otra.
* Podés tener hasta **25 claves activas** por tienda, cada una con un nombre de hasta 60 caracteres. La lista muestra el último uso de cada una, así sabés cuáles revocar.
* Al generar la clave elegís **lectura y escritura** o **solo lectura**. Una clave de solo lectura consulta todo pero no puede ejecutar acciones (ver [Acciones](#acciones)). El tipo no se cambia después: revocá y generá otra.

> [!WARNING]
>
> Las respuestas incluyen datos personales de tus clientes (email, dirección, teléfono, documento fiscal). Tratá las claves como una contraseña: no las pegues en código que se publique ni las compartas por chat.

***

## Primera consulta

```bash theme={null}
curl https://api.appreval.com/api/graphql \
  -H "Authorization: Bearer reval_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" \
  -d '{"query":"{ shop { myshopifyDomain } subscriptions(first: 5, filter: { status: [ACTIVE] }) { totalCount nodes { id customerEmail amount currency nextPaymentDate } } }"}'
```

```json theme={null}
{
  "data": {
    "shop": { "myshopifyDomain": "tu-tienda.myshopify.com" },
    "subscriptions": {
      "totalCount": 312,
      "nodes": [
        { "id": "cmgabc123…", "customerEmail": "ana@example.com", "amount": "18500.00", "currency": "ARS", "nextPaymentDate": "2026-11-02T13:00:00.000Z" }
      ]
    }
  }
}
```

El endpoint acepta **introspección**, así que cualquier cliente GraphQL (GraphiQL, Insomnia, Postman, Altair) te muestra el esquema completo con la descripción de cada campo. Esta página es el resumen; la [referencia del esquema](https://docs.appreval.com/desarrolladores/api-referencia.md) tiene todos los tipos y campos.

* Solo se acepta `POST` con un body JSON `{ "query", "variables", "operationName" }`: una operación por request, sin batching ni `GET`.
* El endpoint responde a CORS, así que una herramienta en el navegador puede llamarlo. No lo hagas desde el front de tu tienda: la clave quedaría expuesta a cualquiera que abra el código fuente.

***

## Qué podés consultar

| Consulta | Devuelve |
| - | - |
| `shop` | El dominio myshopify y el idioma de la tienda |
| `subscription(id)` / `subscriptions` | Suscripciones con productos, plan, frecuencia, estado, próximo cobro, precio, envío, impuestos, tarjeta enmascarada e historial de estados y precios |
| `order(id)` / `orders` | Pedidos generados por las suscripciones: un pedido por cobro, con su número de ciclo, monto y el ID del pedido en Shopify |
| `customer(email)` / `customers` | Clientes (por email) con sus suscripciones y pedidos |
| `planGroup(id)` / `planGroups` | Los planes como los ves en **Planes**: cadencia, tipo de precio, descuentos, ciclos mínimos y máximos |
| `plan(id)` / `plans` | Cada producto (variante) atado a un plan |
| `box(id)` / `boxes` | Las cajas con sus planes, tamaños, productos elegibles, add-ons y tiers |

Los objetos que existen en Shopify traen su **GID de Shopify** para cruzarlos con la Admin API: `shopifyOrderId`, `shopifyCustomerId`, `shopifyVariantId`, `shopifyProductId`. Los identificadores de Mercado Pago (`mercadoPagoPreapprovalId`, `mercadoPagoPaymentId`) sirven para conciliar contra tu cuenta de MP; las credenciales nunca se exponen.

### Convenciones

* **Montos** (`Money`) son strings con dos decimales: `"18500.00"`. La moneda está en el campo `currency` del mismo objeto.
* **Fechas** (`DateTime`) son ISO-8601 en UTC: `"2026-11-02T13:00:00.000Z"`.
* **Enums** van en mayúsculas: `ACTIVE`, `PAUSED`, `CANCELLED`, `MONTHS`, `PERCENT`, `DYNAMIC_BOX`.
* Los IDs de Reval son opacos; usá siempre el que devolvió la API.

***

## Paginación

Todos los listados son *connections* al estilo Shopify: pedís `first` elementos y seguís con `after` usando el `endCursor` de la página anterior.

```graphql theme={null}
query ($after: String) {
  orders(first: 100, after: $after, filter: { createdAfter: "2026-10-01T00:00:00Z" }) {
    edges {
      cursor
      node { id shopifyOrderId shopifyOrderName type cycleNumber amount currency createdAt subscription { id customerEmail } }
    }
    pageInfo { hasNextPage endCursor }
    totalCount
  }
}
```

* `nodes` es el atajo cuando no necesitás el cursor de cada fila.
* `totalCount` cuenta todas las filas que cumplen el filtro, no solo la página.
* `reverse: true` invierte el orden (de más antiguo a más nuevo).
* Los listados anidados (`subscription.orders`, `customer.subscriptions`) se paginan igual.

***

## Filtros

Cada listado acepta un `filter` con los campos más usados:

| Listado | Filtros |
| - | - |
| `subscriptions` | `status`, `customerEmail`, `shopifyCustomerId`, `shopifyVariantId`, `planGroupId`, `boxId`, `createdAfter`, `createdBefore`, `updatedAfter`, `nextPaymentAfter`, `nextPaymentBefore`; orden por `CREATED_AT`, `UPDATED_AT` o `NEXT_PAYMENT_DATE` |
| `orders` | `subscriptionId`, `customerEmail`, `type` (`FIRST` / `RENEWAL`), `shopifyOrderId`, `createdAfter`, `createdBefore` |
| `customers` | `emailContains`, `shopifyCustomerId` |
| `planGroups` | `status`, `type` |
| `plans` | `status`, `shopifyVariantId`, `planGroupId` |
| `boxes` | `status`, `type` |

> [!TIP]
>
> Para sincronizar un warehouse de forma incremental, pedí `subscriptions(filter: { updatedAfter: "<última corrida>" }, sortKey: UPDATED_AT, reverse: true)` y `orders(filter: { createdAfter: "<última corrida>" })`.

***

## Límites

Los límites existen para que un integrador que consulta mucho nunca afecte el checkout ni los cobros. Cuando una consulta los supera, la respuesta dice **cuál**:

| Límite | Valor | Código de error |
| - | - | - |
| Requests por minuto, por clave | 2000 | `RATE_LIMITED` (HTTP 429, con `Retry-After`) |
| Profundidad de objetos anidados por consulta | 4 | `QUERY_TOO_DEEP` |
| Nodos por listado (`first`) | 100 | `PAGE_TOO_LARGE` |
| Nodos estimados por request (listados anidados multiplican) | 1000 | `QUERY_TOO_EXPENSIVE` |

La profundidad cuenta objetos del modelo: `subscriptions › items › plan › group` son cuatro niveles. `edges`, `node`, `nodes` y `pageInfo` no cuentan.

Cada respuesta trae los headers `X-RateLimit-Limit`, `X-RateLimit-Remaining` y `X-RateLimit-Reset` (epoch en segundos) para que tu cliente se regule solo.

***

## Errores

Los errores siguen el formato GraphQL, con un `extensions.code` para que tu código decida:

```json theme={null}
{ "errors": [ { "message": "Page size limit exceeded: \"subscriptions\" asks for first: 250, the maximum is 100 nodes per connection.", "extensions": { "code": "PAGE_TOO_LARGE" } } ] }
```

| HTTP | Código | Cuándo |
| - | - | - |
| 401 | `UNAUTHENTICATED` | Clave ausente, inválida o revocada |
| 429 | `RATE_LIMITED` | Más de 2000 requests en el último minuto |
| 400 | `GRAPHQL_PARSE_FAILED`, `GRAPHQL_VALIDATION_FAILED` | La consulta no es GraphQL válido o pide campos que no existen |
| 400 | `QUERY_TOO_DEEP`, `PAGE_TOO_LARGE`, `QUERY_TOO_EXPENSIVE` | Ver límites |
| 200 | `BAD_USER_INPUT`, `NOT_FOUND` | Un campo falló (cursor inválido, fecha mal formada); el resto de `data` llega igual |
| 403 | `FORBIDDEN_READ_ONLY` | Una mutation con una clave de solo lectura |
| 400 | `IDEMPOTENCY_KEY_INVALID` | El header `Idempotency-Key` está vacío, supera 255 caracteres o tiene espacios |
| 422 | `IDEMPOTENCY_KEY_REUSED` | El mismo `Idempotency-Key` ya se usó con otro body |
| 400 | `BAD_REQUEST` | El body no es JSON con la forma `{ "query": "…", "variables": { } }` |
| 500 | `INTERNAL_ERROR` | Falla inesperada del lado de Reval; reintentá y, si persiste, avisanos con la hora y la consulta |

Las reglas de negocio de las acciones (estado inválido, compromiso mínimo, plan archivado, rechazo de Mercado Pago) **no** son errores: vuelven en `userErrors` con `200`. Ver [Acciones](#acciones).

***

## Acciones

Además de consultar, la API permite **operar** las suscripciones de tu tienda con las mismas acciones que el merchant hace en el admin: pausar, reanudar, cancelar, saltear la próxima entrega, cambiar productos y cantidades, actualizar la dirección y generar un link al portal. También permite **armar links de checkout** con varios productos y los datos del cliente ya cargados, para que un bot cierre la venta con un link listo. Son **mutations** GraphQL en el mismo endpoint.

Cada acción tiene exactamente los mismos efectos que desde el admin: Mercado Pago se actualiza primero (si rechaza, no cambia nada en Reval), se recalculan precio y envío cuando corresponde, se emiten los mismos eventos a Klaviyo, Flow y a tus webhooks, y el historial de la suscripción muestra **«API (nombre de la clave)»** como autor.

### Claves de solo lectura

Al generar una clave en **Reval → Integraciones** elegís si es de **lectura y escritura** o de **solo lectura**. Una clave de solo lectura puede ejecutar cualquier consulta, pero toda mutation recibe `403` con el código `FORBIDDEN_READ_ONLY`. Usala para dashboards y reportes; reservá las de escritura para las automatizaciones que realmente actúan.

### Idempotencia

Las acciones mueven cobros y envíos reales, así que un reintento (timeout, caída de tu worker) no puede repetirlas. Mandá un header `Idempotency-Key` con un identificador único por operación (un UUID). Si la misma clave llega de nuevo con el **mismo** body dentro de las 24 horas, la API devuelve la respuesta original sin ejecutar nada y marca el header `Idempotency-Replayed: true`. Si llega con **otro** body, responde `422` con `IDEMPOTENCY_KEY_REUSED`: es un bug de tu lado, no una segunda operación.

```http theme={null}
POST https://api.appreval.com/api/graphql
Authorization: Bearer reval_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Idempotency-Key: 6f1c2a8e-3b7d-4e0f-9a21-5c8d7e6f4a3b
Content-Type: application/json
```

El header es opcional, pero sin él un reintento puede saltear dos ciclos o cancelar dos veces.

### Errores de negocio: `userErrors`

Una mutation nunca falla con un error GraphQL por una regla de negocio. Devuelve `200` con un payload que trae el objeto **o** una lista `userErrors` con un `code` para que tu código decida, y la suscripción queda igual:

```json theme={null}
{ "data": { "subscriptionPause": { "subscription": null, "userErrors": [ { "code": "MIN_CYCLES", "message": "The plan has a minimum commitment of 3 cycles and 1 has been charged. Pass overrideMinCycles: true to act as the merchant.", "field": null } ] } } }
```

| Código | Cuándo |
| - | - |
| `NOT_FOUND` | No hay una suscripción con ese id en tu tienda (los ids de otras tiendas también responden esto) |
| `INVALID_STATE` | El estado no permite la acción: pausar una pausada, reanudar una activa, operar una cancelada o una que llegó a su último ciclo |
| `MIN_CYCLES` | El plan tiene un compromiso mínimo de entregas que todavía no se cumplió. Es la misma regla que aplica el portal; `overrideMinCycles: true` actúa como el merchant y la saltea |
| `SKIP_NOT_ALLOWED` | La tienda tiene los salteos desactivados, ya hay un salteo pendiente o no hay fecha de próximo cobro |
| `PLAN_UNAVAILABLE` | Un `planId` no existe en tu tienda o está archivado (los planes que la suscripción ya tiene pueden quedarse) |
| `FREQUENCY_MISMATCH` | Un plan cobra con otra frecuencia. Los mandatos de Mercado Pago no cambian de cadencia |
| `BELOW_MINIMUM` | El nuevo total queda por debajo del mínimo de cobro de Mercado Pago para la moneda |
| `INVALID_INPUT` | Datos mal formados: lista de productos vacía, cantidad inválida, dirección incompleta. `field` dice cuál |
| `MERCADO_PAGO_ERROR` | Mercado Pago rechazó el cambio. No se aplicó nada; reintentá en unos minutos |
| `REJECTED` | Cualquier otra regla del admin (por ejemplo, las cajas se editan desde el portal). El `message` lo explica |

### Mutations disponibles

| Mutation | Qué hace |
| - | - |
| `subscriptionPause(id, overrideMinCycles)` | Pausa una suscripción activa. Mercado Pago deja de cobrar hasta que se reanude |
| `subscriptionResume(id)` | Reanuda una pausada. Mercado Pago retoma el calendario y fija el próximo cobro |
| `subscriptionCancel(id, reason, note, overrideMinCycles)` | Cancela con un motivo (`TOO_EXPENSIVE`, `PRODUCT_PILED_UP`, `WANT_ANOTHER_PRODUCT`, `DELIVERY_PROBLEMS`, `NO_LONGER_NEEDED`, `OTHER`) y una nota opcional. Aparece en el análisis de bajas |
| `subscriptionSkipNextCycle(id)` | Saltea la próxima entrega: ese ciclo no se cobra ni se envía |
| `subscriptionSetItems(id, items)` | Reemplaza las líneas de producto por la lista dada (`planId` + `quantity`). Misma frecuencia, planes activos. Recalcula total y envío y actualiza Mercado Pago |
| `subscriptionUpdateShippingAddress(id, address)` | Reemplaza la dirección de envío de los próximos pedidos. `address1`, `city` y `zip` son obligatorios; el envío no se recotiza |
| `subscriptionPortalLink(id)` | Un link firmado y con vencimiento que abre el portal del suscriptor en el dominio principal de la tienda. Tratalo como una credencial |
| `checkoutLinkCreate(lines, customer, externalRef)` | Arma un link de checkout con varios productos (cada línea con su `planId` y cantidad, incluso con frecuencias distintas; o solo `variantId` para compra única) y, opcionalmente, los datos del cliente precargados y una referencia tuya que vuelve en la suscripción y en los webhooks. Con datos del cliente, el link vence a los 30 días y es de un solo uso. Ver la [receta del bot](https://docs.appreval.com/desarrolladores/recetas.md#3b-un-bot-que-arma-el-checkout-listo-para-el-cliente) |

Todas devuelven `{ subscription, userErrors }`, salvo `subscriptionPortalLink` y `checkoutLinkCreate`, que devuelven `{ url, expiresAt, userErrors }`.

**Pausar desde un bot de soporte**

```graphql theme={null}
mutation PauseSub($id: ID!) {
  subscriptionPause(id: $id) {
    subscription { id status nextPaymentDate }
    userErrors { code message field }
  }
}
```

**Cancelar con motivo y nota**

```graphql theme={null}
mutation {
  subscriptionCancel(id: "cm2abc…", reason: OTHER, note: "Se muda al exterior") {
    subscription { id status statusHistory { toStatus source reason changedAt } }
    userErrors { code message }
  }
}
```

**Cambiar productos**

```graphql theme={null}
mutation {
  subscriptionSetItems(id: "cm2abc…", items: [{ planId: "pl_mensual_cafe", quantity: 2 }, { planId: "pl_mensual_filtros", quantity: 1 }]) {
    subscription { amount currency items { shopifyVariantId quantity unitAmount } }
    userErrors { code message field }
  }
}
```

**Link al portal para un email de retención**

```graphql theme={null}
mutation { subscriptionPortalLink(id: "cm2abc…") { url expiresAt userErrors { code } } }
```

***

## Ejemplos

**Suscripciones activas con sus productos y el próximo cobro**

```graphql theme={null}
{
  subscriptions(first: 50, filter: { status: [ACTIVE] }, sortKey: NEXT_PAYMENT_DATE, reverse: true) {
    nodes {
      id customerEmail shopifyCustomerId amount currency nextPaymentDate
      frequency frequencyType
      items { shopifyVariantId quantity unitAmount }
      shippingAddress { city province countryCode }
    }
    pageInfo { hasNextPage endCursor }
  }
}
```

**Un cliente con todo su historial**

```graphql theme={null}
{
  customer(email: "ana@example.com") {
    email shopifyCustomerId subscriptionCount activeSubscriptionCount
    subscriptions(first: 20) { nodes { id status createdAt plan { name } } }
    orders(first: 50) { nodes { shopifyOrderName cycleNumber amount createdAt } }
  }
}
```

**Bajas del mes, con motivo**

```graphql theme={null}
{
  subscriptions(first: 100, filter: { status: [CANCELLED], updatedAfter: "2026-10-01T00:00:00Z" }) {
    nodes {
      id customerEmail
      statusHistory { fromStatus toStatus source reason changedAt }
    }
  }
}
```

**Planes y cajas publicados**

```graphql theme={null}
{
  planGroups(filter: { status: ACTIVE }) {
    nodes { id title frequency frequencyType priceType discountPercent amount plans { shopifyVariantId amount } }
  }
  boxes(filter: { status: ACTIVE }) {
    nodes { id title handle type shopifyProductId planGroups { title } options { kind shopifyVariantId } }
  }
}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.