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

# Payment Links, Transactions and Subscriptions

> Create and manage hosted payment links, read transactions and control subscriptions through Cativa's public API.

Cativa has native monetization: you create a **payment link** (paywall), share the public URL, and the buyer pays on a checkout hosted by Cativa itself. The public API gives you programmatic control over those links, over the **transactions** they generate, and over the resulting recurring **subscriptions**.

This concept explains what the API exposes today, what it does **not** expose, and the recommended pattern to turn "paid" into "has access".

Think of a paywall as a preconfigured card reader: you set the amount and label once, then hand out the link as if you were passing the reader to each customer. You don't build the reader (the checkout is hosted by Cativa); you only configure it, share it, and read the receipt.

<Note>
  The **checkout itself** (capturing the card, processing the payment, tokenizing) is **not exposed on the public API at this phase**. The purchase always happens through Cativa's **hosted payment link**, identified by the `customLink`. The public API is for:

  1. **Creating and managing** payment links (paywalls).
  2. **Reading** transactions and subscriptions.
  3. **Cancelling** a subscription.
  4. **Reacting** to the [`paywall-payment-completed`](/en/webhooks/events/paywall-payment-completed) webhook.

  You do not build the card form. You point the buyer to the hosted URL and react to the result.
</Note>

## The model

```
Paywall (payment link)  ----generates---->  Transaction (payment)
     │                                            │
     │ public customLink                          │ paywall-payment-completed webhook
     ▼                                            ▼
Cativa hosted checkout                   Badge granted ----> access unlocked
     │
     ▼ (if recurring)
Subscription ----> renews ----> new Transaction each cycle
```

A **paywall** is the configurable payment link: price, description, whether it's a one-time or recurring charge, and the `customLink` that forms the public URL. Every successful purchase becomes a **transaction**. If the paywall is recurring, the purchase also creates a **subscription**, which generates a new transaction on each billing cycle.

## Base URL and authentication

All admin routes use the Cativa API with your API Key:

```
https://apis.cativalab.digital/tenant/api/v2
```

```
Authorization: Bearer cativa_live_...
```

The API Key is generated in the Console (Developers > API Keys). See [Quick Start: API Key](/en/get-started/quickstart-api-key). The only **anonymous** route (no key) is the public link read, used by the checkout page to render itself.

<Warning>
  Never expose your API Key in the frontend. The `/admin/...` routes are server-to-server. The buyer only touches the public route `/monetization/paywalls/public/{customLink}`.
</Warning>

## Payment links (paywalls)

Admin routes for the link lifecycle. All IDs are [ULIDs](https://github.com/ulid/spec).

| Action      | Method and route                                  |
| ----------- | ------------------------------------------------- |
| List links  | `GET /admin/monetization/paywalls`                |
| Create link | `POST /admin/monetization/paywalls`               |
| Read a link | `GET /admin/monetization/paywalls/{paywallId}`    |
| Update link | `PUT /admin/monetization/paywalls/{paywallId}`    |
| Delete link | `DELETE /admin/monetization/paywalls/{paywallId}` |
| Statistics  | `GET /admin/monetization/paywalls/stats`          |

### From a created link to unlocked access

<Steps>
  <Step title="Create the paywall">
    `POST /admin/monetization/paywalls` with name, amount, `customLink` and whether it's recurring. The response carries the `id` and the `customLink`.
  </Step>

  <Step title="Share the public URL">
    Build the hosted checkout URL from the `customLink` and hand it to the buyer. The checkout is Cativa's; you don't build the card form.
  </Step>

  <Step title="React to completion">
    Instead of polling, subscribe to the [`paywall-payment-completed`](/en/webhooks/events/paywall-payment-completed) webhook. It lands the moment the payment completes.
  </Step>

  <Step title="Grant access via a badge">
    Configure the paywall to grant a **badge** on completion. The badge is what unlocks the group/course/space; the money (transaction) and the access (badge) stay decoupled.
  </Step>
</Steps>

### Create a payment link

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST 'https://apis.cativalab.digital/tenant/api/v2/admin/monetization/paywalls' \
    -H 'Authorization: Bearer YOUR_API_KEY' \
    -H 'Content-Type: application/json' \
    -d '{
      "name": "Premium Course",
      "description": "Lifetime access to the Premium Course",
      "amount": 197.00,
      "customLink": "premium-course",
      "recurring": false
    }'
  ```

  ```js Node theme={null}
  const res = await fetch(
    'https://apis.cativalab.digital/tenant/api/v2/admin/monetization/paywalls',
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.CATIVA_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        name: 'Premium Course',
        description: 'Lifetime access to the Premium Course',
        amount: 197.0,
        customLink: 'premium-course',
        recurring: false
      })
    }
  );

  if (!res.ok) throw new Error(`Create paywall failed: ${res.status}`);
  const paywall = await res.json();
  ```
</CodeGroup>

Illustrative response (the authoritative schema lives in the API Reference, tag **Paywall**). The returned `customLink` is what forms the public URL:

```json theme={null}
{
  "id": "01HQ6PAYWALL1234567890XYZ",
  "name": "Premium Course",
  "customLink": "premium-course",
  "price": 197.00,
  "isActive": true
}
```

<Note>
  The exact request and response body (every field accepted and returned) lives in the API Reference, under the **Paywall** and **Payment** tags. Don't assume field names from the examples above — check the reference.
</Note>

The `customLink` forms the public URL you share with the buyer. Once created, the link is ready to take payments.

## The public link (anonymous)

A single route is public and requires **no API Key**. The hosted checkout page consumes it to fetch the link's data (name, price, description) and render itself:

```
GET /monetization/paywalls/public/{customLink}
```

You normally do **not** call this route directly. You share the hosted checkout URL and let Cativa handle the rest. It exists in case you want to display link data outside the standard checkout (e.g. a price card on your own site).

## Transactions (payments)

Every successful purchase becomes a transaction. You read transactions to reconcile, audit or react to a purchase.

| Action         | Method and route                               |
| -------------- | ---------------------------------------------- |
| List payments  | `GET /admin/monetization/payments`             |
| Read a payment | `GET /admin/monetization/payments/{paymentId}` |

### List payments

<CodeGroup>
  ```bash cURL theme={null}
  curl 'https://apis.cativalab.digital/tenant/api/v2/admin/monetization/payments' \
    -H 'Authorization: Bearer YOUR_API_KEY'
  ```

  ```js Node theme={null}
  const res = await fetch(
    'https://apis.cativalab.digital/tenant/api/v2/admin/monetization/payments',
    {
      headers: { Authorization: `Bearer ${process.env.CATIVA_API_KEY}` }
    }
  );

  if (!res.ok) throw new Error(`List payments failed: ${res.status}`);
  const payments = await res.json();
  ```
</CodeGroup>

Illustrative response (the authoritative schema, with pagination and every field, lives in the API Reference, tag **Payment**):

```json theme={null}
{
  "items": [
    {
      "id": "01HQ7PAYMENT1234567890XYZ",
      "paywallId": "01HQ6PAYWALL1234567890XYZ",
      "status": "confirmed",
      "amount": 197.00,
      "customerEmail": "john@company.com",
      "paidAt": "2026-07-10T13:05:00Z"
    }
  ],
  "total": 1
}
```

<Note>
  Query filters (pagination, date range, status) and the response shape live in the API Reference under the **Payment** tag. Don't invent parameters from the example.
</Note>

To **react** to a payment in real time, don't poll this route. Subscribe to the [`paywall-payment-completed`](/en/webhooks/events/paywall-payment-completed) webhook, which lands on your server the moment the payment completes.

## Subscriptions

A recurring paywall creates a subscription on the first purchase. The subscription renews on its own each cycle, generating a new transaction per charge.

| Action              | Method and route                                                 |
| ------------------- | ---------------------------------------------------------------- |
| List subscriptions  | `GET /admin/monetization/subscriptions`                          |
| Statistics          | `GET /admin/monetization/subscriptions/stats`                    |
| Cancel subscription | `POST /admin/monetization/subscriptions/{subscriptionId}/cancel` |

Cancel is the only subscription write operation exposed on the public API. Use it when a customer requests cancellation in your app, or when an external flow (chargeback, support request) needs to end the recurrence.

<Note>
  What cancellation does to already-issued charges, the grace period, and the statistics shape live in the API Reference under the **Subscription** tag. Post-cancellation access behavior depends on how you tied a badge to the paywall (see below).
</Note>

## Recommended pattern: from "paid" to "has access"

The monetization API records the **money**. It is not the **access** mechanism. In Cativa, access is always governed by [badge as permission](/en/concepts/badges-as-permissions).

The recommended pattern is:

1. Configure the paywall to **grant a badge** on payment completion (done in the Console, in the paywall settings).
2. That badge is configured as the access requirement on the group, course or space the purchase unlocks.
3. When the payment completes, the badge is assigned and access appears. When the subscription is cancelled and the badge is removed, access is gone.

This way you don't wire access manually to each transaction. The money (transaction/subscription) and the access (badge) stay decoupled, each on its own route. The [Grant access via purchase](/en/guides/grant-access-via-purchase) guide shows the end-to-end architecture, including purchases made on **external** gateways.

To react to each payment (welcome email, CRM, analytics), subscribe to the [`paywall-payment-completed`](/en/webhooks/events/paywall-payment-completed) webhook.

## Common errors and questions

<AccordionGroup>
  <Accordion title="The buyer paid but didn't get access">
    The monetization API records the **money**, not access. Access is always governed by a badge. Confirm the paywall is configured to **grant a badge** on completion and that this badge is the group/course requirement. Without that link, the transaction exists and access doesn't appear.
  </Accordion>

  <Accordion title="Can I capture the card through the public API?">
    Not at this phase. The checkout (capturing the card, processing, tokenizing) is not exposed. The purchase always happens through Cativa's **hosted link**, identified by the `customLink`. The public API creates/manages links, reads transactions and subscriptions, and cancels a subscription.
  </Accordion>

  <Accordion title="Should I poll /payments to know when someone pays?">
    No. Polling wastes calls and delays the reaction. Subscribe to the [`paywall-payment-completed`](/en/webhooks/events/paywall-payment-completed) webhook, which lands on your server the moment the payment completes. Use transaction reads for reconciliation and auditing, not for real-time reaction.
  </Accordion>

  <Accordion title="What happens to access when I cancel the subscription?">
    Cancelling ends the recurrence. What happens to **access** depends on how you tied the badge: if cancellation removes the badge, access goes with it. The rules for already-issued charges and grace period are in the API Reference, tag **Subscription**.
  </Accordion>

  <Accordion title="I got a 403 on an /admin/... route">
    The `/admin/...` routes are server-to-server and require a valid API Key with admin scope. Check the `Authorization: Bearer cativa_live_...` header and never expose the key in the frontend. The only anonymous route is the public link read (`/monetization/paywalls/public/{customLink}`).
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Badges as Permissions" icon="id-badge" href="/en/concepts/badges-as-permissions">
    Understand why access in Cativa is governed by a badge, not wired directly to the transaction.
  </Card>

  <Card title="Grant access via purchase" icon="cart-shopping" href="/en/guides/grant-access-via-purchase">
    End-to-end "purchase unlocks access" architecture, including external gateways.
  </Card>

  <Card title="paywall-payment-completed webhook" icon="webhook" href="/en/webhooks/events/paywall-payment-completed">
    React the instant a payment completes instead of polling transactions.
  </Card>
</CardGroup>
