Skip to main content

Legacy Sale Webhook

Deprecated

The JWT sale webhook is deprecated and will be removed. It keeps working until then, but new integrations should use Webhooks. See Migrate to Webhooks. The sale notification email described below is not deprecated.

Migrate to Webhooks​

Sale webhookWebhooks
One request per paid productOne charge.paid request per charge, with each product's payment in data.payments
Sale of a product or a planorder.paid, once per order, or the first charge.paid of the order
Recurring paymentcharge.paid with charge.recurring_cycle greater than 1
JWT signed with HS256 in a token parameterJSON body signed with HMAC-SHA256 in X-Pla-Signature
No idempotency keyX-Pla-Idempotency-Key
Not sent for shipping charges or Publica One redemptionscharge.paid is sent for both, and order.paid for redemptions. See Tell charges apart
One URL and key in Settings > IntegrationsUp to 10 endpoints in Settings > Webhooks, each with its own signing key and events

Both can run at the same time, so you can move to Webhooks before turning the sale webhook off.

Supported Events​

The following events trigger webhook notifications:

  • Sale of a single product
  • 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 Product Purchase​

For single product purchases, the event_subtype will be "single" and the issue object will contain product 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