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