Skip to main content

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.

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 (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": {
"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

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

{
"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": "[email protected]",
"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

FieldTypeDescription
event_typestringType of event (e.g., "sale")
event_subtypestringSubtype specification (e.g., "single")
uuidstringUnique identifier for this notification
job_idstringInternal job identifier
queued_atintegerUnix timestamp when notification was queued
environmentstring"live" or "test" environment indicator
payloadobjectMain content of the notification

User Object

FieldTypeDescription
uuidstringUnique user identifier
emailstringUser's email address (may be null)
external_idstringExternal system identifier (may be null)
namestringUser's name from billing profile
national_idstringUser's tax or ID number
billing_informationobjectUser's billing address and details

Payment Details

FieldTypeDescription
gatewaystringPayment processor used (e.g., "stripe")
methodstringPayment method (e.g., "card")
recurring_cycleintegerPayment cycle number (null for one-time payments)
storestringStore name
statusstringPayment status (e.g., "approved")
publicala_percentageintegerPlatform fee percentage
payout_amount_in_centsintegerAmount 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)

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
X

Graph View