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

# Webhooks & Sales Notifications

Webhooks allow your systems to receive real-time notifications about events occurring in your Publica.la store. This guide provides detailed implementation instructions for setting up and processing webhook notifications.

:::info
For a high-level overview of notification options, see the [Notifications Overview](./overview.md).
:::

## Supported Events

The following events trigger webhook notifications:

- Sale of a single publication
- Sale of a prepaid plan
- Monthly payment of a recurring plan

## How Webhooks Work

A webhook is a POST request sent to your specified URL. The request body contains a parameter called "token" which is a [JWT](https://jwt.io/) (JSON Web Token) containing all event information.

The JWT is signed using the HS256 algorithm, allowing you to verify that the webhook is genuinely from Publica.la.

## Configuration

To set up webhooks:

1. Provide a publicly accessible URL endpoint
2. Create a signing key for authentication
3. Configure both in your store dashboard at `/dashboard/settings#integrations`

Your endpoint must return a 2xx response code (200, 201, etc.) to acknowledge receipt. If any other response is returned, our system will consider the notification failed and will make 2 additional attempts with a 3-hour delay between each.

## Implementation Details

### Request Format

The webhook request body uses the following JSON format:

```json
{
  "json": {
    "token_type": "jwt",
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
  }
}
```

### JWT Properties

All webhook JWTs share these characteristics:

- **Issuer** (`iss`): Always set to "farfalla"
- **Validity**: Tokens are valid for 5 minutes from creation
- **Subject** (`sub`): Indicates the event type (e.g., "sale")
- **Audience** (`aud`): Contains your store's slug (e.g., "yourstore")

### Verifying JWTs

You should always verify the JWT signature using your signing key before processing the webhook data. This ensures the notification is authentic and hasn't been tampered with.

### Helpful Resources

- [JWT Introduction](https://jwt.io/introduction)
- [JWT Debugger](https://jwt.io/#debugger)
- [JWT Libraries](https://jwt.io/#libraries)
- [Webhook Testing Tool](https://webhook.site)

## Sales Notifications

Sales notifications can be delivered through webhooks or by email. The notification payload is identical regardless of delivery method.

### Timing

- Notifications are sent 20 minutes after a sale is confirmed
- Offline payments (e.g., Rapipago, Easy Payment) may take up to 72 hours to be confirmed by providers
- For recurring subscriptions, notifications are sent each month when an invoice is paid successfully

### Payload Format

```json
{
  "event_type": "sale",
  "event_subtype": "single",
  "uuid": "0d2d4b9a-1660-4418-8f51-96840b780190",
  "job_id": "1231412",
  "queued_at": 1554388826,
  "environment": "live",
  "payload": {
    "user": {
      "external_id": null,
      "uuid": "3909be20-ff61-4e14-8c8b-9fd334a9ecf6",
      "email": "user@example.com",
      "name": "Charles N Salinas",
      "national_id": "123456",
      "billing_information": {
        "address_line_1": "123 Main Street",
        "address_line_2": "Apt 4B",
        "city": "Austin",
        "state": "Texas",
        "country": "USA",
        "postal_code": "78701",
        "other_information": null
      }
    },
    "payment_details": {
      "gateway": "stripe",
      "method": "card",
      "recurring_cycle": null,
      "store": "Example Store",
      "content_owner": "Example Store",
      "status": "approved",
      "sale_type": "retail",
      "publicala_percentage": 10,
      "exchange_rate": 1,
      "payout_currency_id": "USD",
      "payout_amount_in_cents": 1800
    },
    "user_plan": {
      "uuid": "699ed863-0445-4570-9f6b-4fa0fd350052",
      "plan_type": "single",
      "currency_id": "USD",
      "amount": 2000,
      "gateway": "stripe",
      "own_gateway": 0
    },
    "issue": {
      "external_id": "9781234567890",
      "name": "Example Publication",
      "description": "",
      "publication_date": "2023-07-16",
      "prices": [
        {
          "currency_id": "USD",
          "amount": 2000
        },
        {
          "currency_id": "EUR",
          "amount": 1800
        }
      ],
      "file_type": "pdf",
      "reader_url": "https://example.publica.la/reader/example-publication",
      "is_free_until": null,
      "require_login_when_free": 0,
      "is_free": 0,
      "publishers": ["Publisher Name"],
      "publishing_group": null
    },
    "plan": {
      "type": "recurring",
      "name": "Premium Subscription",
      "detail": null,
      "id": 432,
      "external_id": 4412
    },
    "coupon": {
      "code": "WELCOME25",
      "amount": 25,
      "uses": 1,
      "use_limit": 10,
      "valid_to": "2024-05-01"
    }
  }
}
```

## Field Definitions

### Root Properties

| Field         | Type    | Description                                 |
| ------------- | ------- | ------------------------------------------- |
| event_type    | string  | Type of event (e.g., "sale")                |
| event_subtype | string  | Subtype specification (e.g., "single")      |
| uuid          | string  | Unique identifier for this notification     |
| job_id        | string  | Internal job identifier                     |
| queued_at     | integer | Unix timestamp when notification was queued |
| environment   | string  | "live" or "test" environment indicator      |
| payload       | object  | Main content of the notification            |

### User Object

| Field               | Type   | Description                              |
| ------------------- | ------ | ---------------------------------------- |
| uuid                | string | Unique user identifier                   |
| email               | string | User's email address (may be null)       |
| external_id         | string | External system identifier (may be null) |
| name                | string | User's name from billing profile         |
| national_id         | string | User's tax or ID number                  |
| billing_information | object | User's billing address and details       |

### Payment Details

| Field                  | Type    | Description                                       |
| ---------------------- | ------- | ------------------------------------------------- |
| gateway                | string  | Payment processor used (e.g., "stripe")           |
| method                 | string  | Payment method (e.g., "card")                     |
| recurring_cycle        | integer | Payment cycle number (null for one-time payments) |
| store                  | string  | Store name                                        |
| status                 | string  | Payment status (e.g., "approved")                 |
| publicala_percentage   | integer | Platform fee percentage                           |
| payout_amount_in_cents | integer | Amount to be paid to the store (in cents)         |

### User Plan / Issue / Plan

These objects contain details about what was purchased, including pricing, access, and identification information.

## Handling Different Notification Types

### Single Publication Purchase

For single publication purchases, the `event_subtype` will be "single" and the `issue` object will contain publication details.

### Subscription Plan Purchase

For subscription purchases, the `event_subtype` will be "plan" and the `plan` object will contain subscription details.

### Recurring Payment

For recurring payments, the `event_subtype` will be "recurring" and the `recurring_cycle` field in `payment_details` will indicate which payment cycle it represents.

## Implementing Webhook Receivers

### Example Implementation (Node.js)

```javascript
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();

app.use(express.json());

const WEBHOOK_SECRET = 'your_signing_key_here';

app.post('/webhook/publica', (req, res) => {
  try {
    // Extract JWT token
    const { token } = req.body.json;

    // Verify JWT signature
    const decoded = jwt.verify(token, WEBHOOK_SECRET, {
      algorithms: ['HS256'],
      issuer: 'farfalla',
    });

    // Process event based on type
    if (decoded.sub === 'sale') {
      // Handle sale event
      console.log('Sale notification received:', decoded);
      // Process sale data...
    }

    // Acknowledge receipt
    res.status(200).send('OK');
  } catch (error) {
    console.error('Webhook processing error:', error);
    // Still return 200 to prevent retries if we received the data
    // but had issues processing it
    res.status(200).send('Received but error in processing');
  }
});

app.listen(3000, () => {
  console.log('Webhook receiver listening on port 3000');
});
```

## Best Practices

1. **Idempotency**: Process notifications idempotently to handle potential duplicate deliveries
2. **Signature Verification**: Always verify the JWT signature
3. **Error Handling**: Implement robust error handling in your webhook processor
4. **Monitoring**: Set up monitoring to detect failed webhook processing
5. **Logging**: Maintain detailed logs of received webhooks for troubleshooting

## Related Documentation

- [Notifications Overview](./overview.md)
- [Instant Payment Notifications](./ipns.md)
- [API Reference](../rest-api/overview.md)
