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.
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:
- Provide a publicly accessible URL endpoint
- Create a signing key for authentication
- 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
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 |
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)
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
- Idempotency: Process notifications idempotently to handle potential duplicate deliveries
- Signature Verification: Always verify the JWT signature
- Error Handling: Implement robust error handling in your webhook processor
- Monitoring: Set up monitoring to detect failed webhook processing
- Logging: Maintain detailed logs of received webhooks for troubleshooting