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

# Recipes: typical integrations with the API and webhooks

> Step-by-step guides for the most common cases: syncing a warehouse, reconciling orders, a support bot that pauses and cancels, failed-charge alerts, Zapier/Make/n8n and retention emails with a portal link.

Each recipe combines what the [API](https://docs.appreval.com/en/developers/api.md) and the [webhooks](https://docs.appreval.com/en/developers/webhooks.md) pages already explain to solve one concrete case end to end. The examples use `curl` and Python because they read easily; any language with HTTP and JSON works. Every type and field is in the [schema reference](https://docs.appreval.com/en/developers/api-reference.md).

> [!TIP]
>
> Before you start, generate a key in **Reval → Integrations** and run the [first query](https://docs.appreval.com/en/developers/api.md#first-query). For the read-only recipes, use a **read-only** key.

***

## 1. Sync subscriptions to your warehouse

**Goal:** keep a copy of the subscriptions in BigQuery, Snowflake, Postgres or a spreadsheet that updates itself, without pulling everything every time.

**How:** the first run pulls everything; later runs ask only for what changed since the last one, with `updatedAfter` and `UPDATED_AT` ordering. Store the last run's timestamp and the `endCursor` in case a page fails halfway.

```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: what you stored last time
    upsert(sub)                       # your warehouse, primary key = sub["id"]
save_last_run(started)
```

* Ask for `first: 100` (the maximum) and do not nest `orders` inside `subscriptions`: 100 × 100 exceeds the per-request node cap. Pull the orders in a second query (recipe 2).
* `updatedAt` changes with any edit, charge, pause or cancellation, so a cancelled subscription shows up in the next run on its own.
* At 2000 requests per minute per key, a store with 50,000 subscriptions syncs in full in under a minute; your warehouse sets the pace, not the API.

***

## 2. Reconcile the month's orders with accounting

**Goal:** a list of every subscription charge of the month, with the Shopify order number and the Mercado Pago payment id, to match against the MP settlement.

```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` tells the first charge (`FIRST`) from renewals (`RENEWAL`); the first-order discount applies only to the first.
* A combined order (auto-merge) appears **once per subscription** with the same `shopifyOrderId`: each row is one Mercado Pago charge, so the sum of `amount` matches what was charged even though the Shopify order is a single one.
* If your accounting needs VAT, every subscription carries `tax { amount rate title included }`.

***

## 3. A support bot that pauses, skips and cancels

**Goal:** let your support team (or an AI agent) handle "pause me for a month" or "cancel my subscription" from their own tool, with the same rules as the portal.

**How:** find the subscription by email, run the mutation with an `Idempotency-Key`, and show the `userError` verbatim if there is one.

```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…"}}'
```

* Use something that identifies the intent as the `Idempotency-Key` (the ticket id plus the action): if your tool retries, the API returns the same answer without pausing twice.
* `MIN_CYCLES` means the plan has a minimum commitment the customer has not met. It is the same rule they see in the portal: decide in your bot whether an agent may skip it with `overrideMinCycles: true` (acts as the merchant).
* To cancel, ask the agent for a reason from the list (`TOO_EXPENSIVE`, `DELIVERY_PROBLEMS`…) and pass the free text in `note`: it shows up in the dashboard's churn analysis.
* The subscription's history shows "API (key name)", so name the key after the tool ("Zendesk bot").

***

## 3b. A bot that builds the checkout ready for the customer

**Goal:** your sales bot (WhatsApp, CRM) closes the conversation with a link where the customer only reviews, enters the card and pays — products, quantities and their details already in place.

**How:** build the link with `checkoutLinkCreate` — one line per product with its `planId` (list them with `plans`) — passing the customer's details and your reference:

```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@example.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 } } }"}'
```

* The response carries the **URL ready to send** and `expiresAt`. The URL holds no personal data — only an opaque code; the data lives in Reval, expires after 30 days and is deleted on use.
* The plans may bill on **different frequencies**: the customer pays once and each frequency becomes its own subscription.
* When the customer completes the purchase, your `externalRef` shows up on `Subscription.externalRef` and as `external_ref` in the `subscription.created` webhook — that's where your bot closes the loop with its conversation.
* The link is **single-use**: for a new purchase, create another. A link without `customer` or `externalRef` also works and is permanent.
* Errors come back through `userErrors` naming the line (`lines[1]: …`): inactive plan (`PLAN_UNAVAILABLE`), bundle or box product (`REJECTED`), invalid input (`INVALID_INPUT`). A dead link is never minted.

***

## 4. Failed-charge alerts in Slack (or anywhere)

**Goal:** know the moment Mercado Pago rejects a renewal, with the reason in words, to reach the customer before the subscription is cancelled.

**How:** an endpoint subscribed only to `subscription.payment_failed`. The payload carries `reason_text`, ready to paste into a message.

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

app = Flask(__name__)
SECRET = "whsec_XXXXXXXX"
SEEN = set()  # in production, a table or 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          # a retry of something already processed
    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: Failed charge for {d['customer_email']} ({d['amount_formatted']}): {d['reason_text']}"})
    return "", 200
```

* Answer 200 **before** doing anything slow; if Slack takes more than 10 seconds, Reval counts the attempt as failed and retries.
* Mercado Pago retries the charge on its own; if it later approves, you receive `subscription.charged` for the same subscription.
* To see the current state without waiting for the next event, query `subscription(id) { lastPaymentFailedAt lastPaymentFailReason status }`.

***

## 5. Webhooks in Zapier, Make or n8n, no code

**Goal:** have every cancellation add a row to Google Sheets, or every signup create a contact in your CRM.

1. In your tool, create a **Webhook / Catch hook** trigger and copy the URL it gives you (it is `https`, so Reval accepts it).
2. In **Reval → Integrations → Webhooks**, add an endpoint with that URL and tick only the events you need (for example `subscription.cancelled`).
3. Click **Send test**: your tool receives a `test` event shaped like a real one, and you map the fields from it (`data.customer_email`, `data.product_names`, `data.reason`…).
4. Save the flow. From then on, every real event triggers it.

* These tools answer 2xx immediately, which is what Reval expects. You do not need to verify the signature to get started; if you want to, Make and n8n have HMAC steps that replicate the [verification](https://docs.appreval.com/en/developers/webhooks.md#verifying-the-signature).
* Use `data.subscription_id` as the key in your sheet or CRM so retries do not duplicate rows.

***

## 6. Retention email with a direct portal link

**Goal:** send a customer who cancelled (or had a failed charge) an email with a button that opens **their** subscription in the portal, already authenticated, so they can reactivate or change the card.

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

* The link is signed and expires: generate it when you send the email, do not store it.
* Treat it as a credential: whoever has it enters that customer's portal. Do not send it through channels where it could be exposed.
* If you use Klaviyo, the events Reval sends it already include `portal_subscription_url`, which opens the subscription in the portal with the customer's normal login; `subscriptionPortalLink` is for when you need direct access from another tool.

***

## What to pick: API, webhooks or Klaviyo

| You need… | Use |
| - | - |
| A current snapshot of the data, or the history | API (queries) |
| To react the moment something happens | Webhooks |
| To act on a subscription from another tool | API (actions) |
| Automatic emails to the end customer | Klaviyo or Shopify Flow, which receive the same events with no code |
| The same in Zapier / Make / n8n | Webhooks |


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