> ## 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.

# Recetas: integraciones típicas con la API y los webhooks

> Guías paso a paso para los casos más comunes: sincronizar un warehouse, conciliar pedidos, un bot de soporte que pausa y cancela, alertas de cobros fallidos, Zapier/Make/n8n y emails de retención con link al portal.

Cada receta combina lo que ya explican la [API](https://docs.appreval.com/desarrolladores/api.md) y los [webhooks](https://docs.appreval.com/desarrolladores/webhooks.md) para resolver un caso concreto de punta a punta. Los ejemplos usan `curl` y Python porque se leen fácil; cualquier lenguaje con HTTP y JSON sirve. Los tipos y campos completos están en la [referencia del esquema](https://docs.appreval.com/desarrolladores/api-referencia.md).

> [!TIP]
>
> Antes de empezar, generá una clave en **Reval → Integraciones** y probá la [primera consulta](https://docs.appreval.com/desarrolladores/api.md#primera-consulta). Para las recetas que solo leen, usá una clave de **solo lectura**.

***

## 1. Sincronizar las suscripciones a tu warehouse

**Objetivo:** tener en BigQuery, Snowflake, Postgres o una planilla una copia de las suscripciones que se actualice sola, sin traer todo cada vez.

**Cómo:** la primera corrida trae todo; las siguientes piden solo lo que cambió desde la última, con `updatedAfter` y orden por `UPDATED_AT`. Guardá la fecha de la última corrida y el `endCursor` por si una página falla a la mitad.

```graphql theme={null}
query Sync($after: String, $since: DateTime) {
  subscriptions(
    first: 100
    after: $after
    filter: { updatedAfter: $since }
    sortKey: UPDATED_AT
    reverse: true
  ) {
    nodes {
      id status customerEmail shopifyCustomerId
      amount currency frequency frequencyType nextPaymentDate
      plan { id name } planGroup { id title }
      items { shopifyVariantId quantity unitAmount role }
      shippingAddress { city province countryCode }
      createdAt updatedAt
    }
    pageInfo { hasNextPage endCursor }
  }
}
```

```python theme={null}
import requests, datetime

API = "https://api.appreval.com/api/graphql"
HEADERS = {"Authorization": "Bearer reval_XXXXXXXX", "Content-Type": "application/json"}

def fetch_changed(since: str):
    after = None
    while True:
        r = requests.post(API, headers=HEADERS, json={"query": QUERY, "variables": {"after": after, "since": since}})
        r.raise_for_status()
        page = r.json()["data"]["subscriptions"]
        yield from page["nodes"]
        if not page["pageInfo"]["hasNextPage"]:
            return
        after = page["pageInfo"]["endCursor"]

started = datetime.datetime.now(datetime.timezone.utc).isoformat()
for sub in fetch_changed(last_run):   # last_run: lo que guardaste la vez anterior
    upsert(sub)                       # tu warehouse, clave primaria = sub["id"]
save_last_run(started)
```

* Pedí `first: 100` (el máximo) y no anides `orders` adentro de `subscriptions`: 100 × 100 supera el tope de nodos por request. Traé los pedidos en una segunda consulta (receta 2).
* `updatedAt` cambia con cualquier edición, cobro, pausa o cancelación, así que una suscripción cancelada entra en la próxima corrida sola.
* Con 2000 requests por minuto por clave, una tienda con 50.000 suscripciones se sincroniza entera en menos de un minuto; el ritmo lo marca tu warehouse, no la API.

***

## 2. Conciliar los pedidos del mes con contabilidad

**Objetivo:** una lista de todos los cobros de suscripción del mes, con el número de pedido de Shopify y el ID de pago de Mercado Pago, para cruzarla con la liquidación de MP.

```graphql theme={null}
{
  orders(first: 100, filter: { createdAfter: "2026-10-01T00:00:00Z", createdBefore: "2026-11-01T00:00:00Z" }) {
    totalCount
    nodes {
      id type cycleNumber amount currency createdAt
      shopifyOrderId shopifyOrderName mercadoPagoPaymentId
      subscription { id customerEmail }
    }
    pageInfo { hasNextPage endCursor }
  }
}
```

* `type` distingue el primer cobro (`FIRST`) de las renovaciones (`RENEWAL`); el descuento de primer pedido solo aplica al primero.
* Un pedido combinado (auto-merge) aparece **una vez por suscripción** con el mismo `shopifyOrderId`: cada fila es un cobro de Mercado Pago, así que la suma de `amount` coincide con lo cobrado aunque el pedido de Shopify sea uno solo.
* Si tu contabilidad necesita el IVA, cada suscripción trae `tax { amount rate title included }`.

***

## 3. Un bot de soporte que pausa, saltea y cancela

**Objetivo:** que tu equipo de atención (o un agente de IA) resuelva «pausame un mes» o «cancelá mi suscripción» desde su propia herramienta, con las mismas reglas que el portal.

**Cómo:** buscá la suscripción por email, ejecutá la mutation con un `Idempotency-Key` y mostrá el `userError` tal cual si lo hay.

```graphql theme={null}
query Find($email: String!) {
  customer(email: $email) {
    subscriptions(first: 10, filter: { status: [ACTIVE, PAUSED] }) {
      nodes { id status nextPaymentDate plan { name } }
    }
  }
}
```

```bash theme={null}
curl https://api.appreval.com/api/graphql \
  -H "Authorization: Bearer reval_XXXXXXXX" \
  -H "Idempotency-Key: ticket-48213-pause" \
  -H "Content-Type: application/json" \
  -d '{"query":"mutation($id: ID!) { subscriptionPause(id: $id) { subscription { id status } userErrors { code message } } }","variables":{"id":"cmg…"}}'
```

* Usá como `Idempotency-Key` algo que identifique la intención (el ID del ticket más la acción): si tu herramienta reintenta, la API devuelve la misma respuesta sin pausar dos veces.
* `MIN_CYCLES` significa que el plan tiene un compromiso mínimo que el cliente no cumplió. Es la misma regla que ve en el portal: decidí en tu bot si el agente puede saltearla con `overrideMinCycles: true` (actúa como el merchant).
* Para cancelar, pedile al agente un motivo de la lista (`TOO_EXPENSIVE`, `DELIVERY_PROBLEMS`…) y pasá el texto libre en `note`: aparece en el análisis de bajas del dashboard.
* El historial de la suscripción muestra «API (nombre de la clave)», así que nombrá la clave con el nombre de la herramienta («Zendesk bot»).

***

## 3b. Un bot que arma el checkout listo para el cliente

**Objetivo:** que tu bot de ventas (WhatsApp, CRM) cierre la conversación con un link donde el cliente solo revisa, carga la tarjeta y paga — productos, cantidades y sus datos ya puestos.

**Cómo:** armá el link con `checkoutLinkCreate` — una línea por producto con su `planId` (los listás con `plans`) — y pasale los datos del cliente y tu referencia:

```bash theme={null}
curl https://api.appreval.com/api/graphql \
  -H "Authorization: Bearer reval_XXXXXXXX" \
  -H "Idempotency-Key: conv-9154-link" \
  -H "Content-Type: application/json" \
  -d '{"query":"mutation { checkoutLinkCreate(lines: [{ planId: \"cmg…\", quantity: 2 }, { planId: \"cmh…\", quantity: 1 }], customer: { email: \"ana@ejemplo.com\", firstName: \"Ana\", phone: \"+5491144445555\", address: { address1: \"Calle 123\", city: \"CABA\", province: \"Buenos Aires\", zip: \"1414\", countryCode: \"AR\" } }, externalRef: \"conv-9154\") { url expiresAt userErrors { code message field } } }"}'
```

* La respuesta trae la **URL lista para mandar** y `expiresAt`. La URL no lleva ningún dato personal — solo un código opaco; los datos viven en Reval, vencen a los 30 días y se borran al usarse.
* Los planes pueden tener **frecuencias distintas**: el cliente paga una vez y cada frecuencia queda como su propia suscripción.
* Cuando el cliente completa la compra, tu `externalRef` aparece en `Subscription.externalRef` y como `external_ref` en el webhook `subscription.created` — ahí tu bot cierra el circuito con su conversación.
* El link es de **un solo uso**: para una compra nueva, generá otro. Un link sin `customer` ni `externalRef` también funciona y es permanente.
* Los errores vuelven por `userErrors` indicando la línea (`lines[1]: …`): plan inactivo (`PLAN_UNAVAILABLE`), bundle o producto-caja (`REJECTED`), input inválido (`INVALID_INPUT`). Nunca se genera un link muerto.

***

## 4. Alertas de cobros fallidos en Slack (o donde quieras)

**Objetivo:** enterarte en el momento cuando Mercado Pago rechaza una renovación, con el motivo en palabras, para contactar al cliente antes de que se cancele.

**Cómo:** un endpoint suscripto solo a `subscription.payment_failed`. El payload trae `reason_text` listo para pegar en un mensaje.

```python theme={null}
from flask import Flask, request, abort
import hmac, hashlib, time, requests

app = Flask(__name__)
SECRET = "whsec_XXXXXXXX"
SEEN = set()  # en producción, una tabla o Redis

@app.post("/reval")
def reval():
    raw = request.get_data()
    parts = dict(kv.split("=", 1) for kv in request.headers["X-Reval-Signature"].split(","))
    expected = hmac.new(SECRET.encode(), f"{parts['t']}.".encode() + raw, hashlib.sha256).hexdigest()
    if abs(time.time() - int(parts["t"])) > 300 or not hmac.compare_digest(expected, parts["v1"]):
        abort(401)
    delivery_id = request.headers["X-Reval-Delivery"]
    if delivery_id in SEEN:
        return "", 200          # reintento de algo ya procesado
    SEEN.add(delivery_id)
    event = request.get_json()
    if event["topic"] == "subscription.payment_failed":
        d = event["data"]
        requests.post(SLACK_WEBHOOK, json={"text": f":warning: Cobro fallido de {d['customer_email']} ({d['amount_formatted']}): {d['reason_text']}"})
    return "", 200
```

* Respondé 200 **antes** de hacer cosas lentas; si Slack tarda más de 10 segundos, Reval cuenta el intento como fallido y reintenta.
* Mercado Pago reintenta el cobro por su cuenta; si después aprueba, recibís `subscription.charged` para la misma suscripción.
* Si querés ver el estado actual sin esperar el próximo evento, consultá `subscription(id) { lastPaymentFailedAt lastPaymentFailReason status }`.

***

## 5. Webhooks en Zapier, Make o n8n, sin escribir código

**Objetivo:** que cada cancelación agregue una fila en Google Sheets, o que cada alta cree un contacto en tu CRM.

1. En tu herramienta, creá un trigger de tipo **Webhook / Catch hook** y copiá la URL que te da (es `https`, así que Reval la acepta).
2. En **Reval → Integraciones → Webhooks**, agregá un endpoint con esa URL y marcá solo los eventos que necesitás (por ejemplo `subscription.cancelled`).
3. Tocá **Enviar prueba**: tu herramienta recibe un evento `test` con la misma forma que uno real, y con eso mapeás los campos (`data.customer_email`, `data.product_names`, `data.reason`…).
4. Guardá el flujo. A partir de ahí, cada evento real lo dispara.

* Estas herramientas responden 2xx al instante, que es lo que Reval espera. No hace falta verificar la firma para empezar; si querés hacerlo, Make y n8n tienen pasos de HMAC con los que podés replicar la [verificación](https://docs.appreval.com/desarrolladores/webhooks.md#verificar-la-firma).
* Usá `data.subscription_id` como clave en tu planilla o CRM para no duplicar filas cuando Reval reintenta una entrega.

***

## 6. Email de retención con link directo al portal

**Objetivo:** mandarle a un cliente que canceló (o que tuvo un cobro fallido) un email con un botón que abre **su** suscripción en el portal, ya autenticado, para que la reactive o cambie la tarjeta.

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

* El link está firmado y vence: generalo al momento de mandar el email, no lo guardes.
* Trátalo como una credencial: quien lo tenga entra al portal de ese cliente. No lo mandes por canales donde pueda quedar expuesto.
* Si usás Klaviyo, los eventos que Reval le manda ya incluyen `portal_subscription_url`, que abre la suscripción en el portal con el login normal del cliente; `subscriptionPortalLink` es para cuando necesitás el acceso directo desde otra herramienta.

***

## Qué elegir: API, webhooks o Klaviyo

| Necesitás… | Usá |
| - | - |
| Una foto actual de los datos, o el histórico | API (consultas) |
| Reaccionar en el momento a algo que pasó | Webhooks |
| Actuar sobre una suscripción desde otra herramienta | API (acciones) |
| Emails automáticos al cliente final | Klaviyo o Shopify Flow, que reciben los mismos eventos sin que programes nada |
| Lo mismo en Zapier / Make / n8n | Webhooks |


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