Webhooks and Notifications
Webhooks and Notifications
What are Webhooks?
Webhooks are HTTP POST notifications sent to your server in real-time when subscription or payment events occur. They allow you to react automatically to changes in subscription status, payment processing, and other important events without polling the API.
Configuration
Webhook URL
You must provide a notificationUrl when creating a subscription. This URL will receive all webhook notifications for that subscription and its payments.
Requirements:
- Must be publicly accessible (HTTPS)
- Must accept POST requests
- Must return 200 OK responses
- Should respond quickly (process asynchronously if needed)
Example:
{
"notificationUrl": "https://your-domain.com/webhooks/subscriptions"
}Authentication
The WebhookSender service handles webhook delivery, including:
- Retry logic for failed deliveries
- Rate limiting
- Webhook signature generation
- Delivery confirmation
Your webhook endpoint should validate the webhook signature if provided by the WebhookSender service.
Event Types
Subscription Events
subscription_activation
Triggered when a subscription is activated (user authorizes the subscription).
When it occurs:
- User authorizes subscription via Journey 1 (push notification)
- User authorizes subscription via Journey 2 (QR code scan)
Payload:
{
"eventType": "subscription_activation",
"eventId": "event-guid-123",
"eventDate": "2025-01-15T10:30:00Z",
"entityId": "subscription-guid",
"status": "ACTIVE",
"statusDate": "2025-01-15T10:30:00Z",
"message": null,
"errorCode": null,
"errorMessage": null,
"pspReference": null,
"end2EndId": null,
"additionalData": []
}subscription_cancellation
Triggered when a subscription is cancelled.
When it occurs:
- Merchant cancels subscription via API
- User cancels subscription via their bank/app
- Subscription expires automatically
Payload:
{
"eventType": "subscription_cancellation",
"eventId": "event-guid-124",
"eventDate": "2025-01-20T14:00:00Z",
"entityId": "subscription-guid",
"status": "CANCELLED",
"statusDate": "2025-01-20T14:00:00Z",
"message": "Cancelled by user",
"errorCode": null,
"errorMessage": null,
"pspReference": null,
"end2EndId": null,
"additionalData": []
}Payment Events
subscription.payment (PAID)
Triggered when a payment is successfully processed.
When it occurs:
- Payment is successfully processed by the payment provider
- Retry attempt succeeds
Payload:
{
"eventType": "subscription.payment",
"eventId": "event-guid-125",
"eventDate": "2025-01-15T10:35:00Z",
"entityId": "payment-guid",
"status": "PAID",
"statusDate": "2025-01-15T10:35:00Z",
"message": null,
"errorCode": null,
"errorMessage": null,
"pspReference": "123456-7890-acasfsa-23424",
"end2EndId": "E-1323434-acac-3214324-adasd",
"additionalData": []
}subscription.payment (FAILED)
Triggered when a payment fails.
When it occurs:
- Payment fails due to insufficient funds
- Payment fails due to account closure
- Payment fails due to authorization revocation
- Payment fails for any other reason
Payload:
{
"eventType": "subscription.payment",
"eventId": "event-guid-126",
"eventDate": "2025-01-15T10:40:00Z",
"entityId": "payment-guid",
"status": "FAILED",
"statusDate": "2025-01-15T10:40:00Z",
"message": null,
"errorCode": "INSUFFICIENT_FUNDS",
"errorMessage": "Insufficient funds in account",
"pspReference": "123456-7890-acasfsa-23425",
"end2EndId": null,
"additionalData": [
{
"key": "retryCount",
"value": "1"
}
]
}subscription.payment (CANCELLED)
Triggered when a payment is cancelled.
When it occurs:
- Merchant cancels payment via API
- Payment is cancelled due to timing restrictions (e.g., PIX cancellation deadline)
- Subscription is cancelled before payment is processed
Payload:
{
"eventType": "subscription.payment",
"eventId": "event-guid-127",
"eventDate": "2025-01-15T11:00:00Z",
"entityId": "payment-guid",
"status": "CANCELLED",
"statusDate": "2025-01-15T11:00:00Z",
"message": "Cancelled by merchant",
"errorCode": null,
"errorMessage": null,
"pspReference": null,
"end2EndId": null,
"additionalData": []
}subscription.payment_schedule
Triggered when the next payment is automatically scheduled.
When it occurs:
- System automatically schedules the next payment (48 hours before due date)
- Only sent if automatic scheduling is enabled
Payload:
{
"eventType": "subscription.payment_schedule",
"eventId": "event-guid-128",
"eventDate": "2025-01-13T10:00:00Z",
"entityId": "payment-guid",
"status": "PENDING",
"statusDate": "2025-01-13T10:00:00Z",
"message": null,
"errorCode": null,
"errorMessage": null,
"pspReference": null,
"end2EndId": null,
"additionalData": [
{
"key": "scheduledDate",
"value": "2025-01-15T10:00:00Z"
}
]
}subscription.payment_cancellation
Triggered when a payment cancellation is requested.
When it occurs:
- Merchant requests payment cancellation
- System processes payment cancellation
Payload:
{
"eventType": "subscription.payment_cancellation",
"eventId": "event-guid-129",
"eventDate": "2025-01-15T12:00:00Z",
"entityId": "payment-guid",
"status": "CANCELLED",
"statusDate": "2025-01-15T12:00:00Z",
"message": "Payment cancellation requested",
"errorCode": null,
"errorMessage": null,
"pspReference": null,
"end2EndId": null,
"additionalData": []
}Payload Structure
All webhook payloads follow this structure:
{
"eventType": "string",
"eventId": "string",
"eventDate": "ISO8601 datetime",
"entityId": "string",
"status": "string",
"statusDate": "ISO8601 datetime",
"message": "string | null",
"errorCode": "string | null",
"errorMessage": "string | null",
"pspReference": "string | null",
"end2EndId": "string | null",
"additionalData": [
{
"key": "string",
"value": "string"
}
]
}Field Descriptions
eventType: Type of event (e.g.,subscription_activation,subscription.payment)eventId: Unique identifier for this event (use for idempotency)eventDate: UTC timestamp when the event occurredentityId: ID of the entity that triggered the event (SubscriptionPayment.Id or Subscription.Id)status: Current status of the entity (e.g.,ACTIVE,PAID,FAILED)statusDate: UTC timestamp when the status changedmessage: Optional message (e.g., cancellation reason)errorCode: Error code if applicable (e.g.,INSUFFICIENT_FUNDS)errorMessage: Human-readable error messagepspReference: Payment Service Provider reference (for payments)end2EndId: End-to-End ID (PIX-specific identifier)additionalData: Additional context data (key-value pairs)
Common Error Codes
Payment Errors
INSUFFICIENT_FUNDS: Account has insufficient fundsREJECTED_BY_BANK: Payment rejected by the bankCANCELLED_BY_USER: User cancelled the paymentACCOUNT_CLOSED: User's account is closedAUTHORIZATION_REVOKED: User revoked subscription authorizationPAYMENT_EXPIRED: Payment expired before processingINVALID_ACCOUNT: Invalid account informationLIMIT_EXCEEDED: Transaction limit exceeded
Subscription Errors
AUTHORIZATION_REJECTED: User rejected subscription authorizationINVALID_CUSTOMER: Customer information is invalidCONFIGURATION_ERROR: Subscription configuration error
Webhook Processing
Receiving Webhooks
- Receive POST Request: Your endpoint receives the webhook payload
- Validate Signature (if implemented): Verify the webhook is from PayRetailers
- Check Idempotency: Use
eventIdto ensure you haven't processed this event - Process Event: Update your system based on the event
- Respond 200 OK: Return success response quickly
Idempotency
Always use the eventId field to ensure idempotency. Store processed event IDs and check before processing:
# Example idempotency check
def process_webhook(payload):
event_id = payload['eventId']
# Check if already processed
if is_event_processed(event_id):
return 200 # Already processed, return success
# Process event
process_event(payload)
# Mark as processed
mark_event_processed(event_id)
return 200Response Requirements
Your webhook endpoint must:
- Respond quickly: Return 200 OK within a few seconds
- Process asynchronously: Do heavy processing in background jobs
- Handle errors gracefully: Return appropriate HTTP status codes
Response Codes:
- 200 OK: Webhook received and processed successfully
- 400 Bad Request: Invalid payload (webhook will not be retried)
- 500 Internal Server Error: Processing error (webhook will be retried)
Retry Logic
The WebhookSender service automatically retries failed webhooks:
- Retries occur if your endpoint doesn't return 200 OK
- Retry intervals increase exponentially
- Maximum retry attempts are configurable
- Failed webhooks are logged for investigation
Best Practices
Idempotency
- Always check
eventIdbefore processing - Store processed event IDs
- Handle duplicate events gracefully
Fast Response
- Respond with 200 OK immediately
- Process events asynchronously
- Use background jobs for heavy processing
Error Handling
- Log all webhook events
- Handle errors gracefully
- Return appropriate HTTP status codes
- Monitor webhook delivery failures
Validation
- Validate
entityIdexists in your system - Verify event structure
- Check required fields
Security
- Validate webhook signatures (if provided)
- Use HTTPS for webhook URLs
- Implement rate limiting
- Monitor for suspicious activity
Monitoring
- Log all webhook events
- Track processing times
- Monitor error rates
- Alert on delivery failures
Example Webhook Handler
from flask import Flask, request, jsonify
import logging
app = Flask(__name__)
logger = logging.getLogger(__name__)
# Store processed event IDs (use Redis in production)
processed_events = set()
@app.route('/webhooks/subscriptions', methods=['POST'])
def handle_webhook():
try:
payload = request.json
event_id = payload.get('eventId')
event_type = payload.get('eventType')
entity_id = payload.get('entityId')
status = payload.get('status')
# Idempotency check
if event_id in processed_events:
logger.info(f"Event {event_id} already processed")
return jsonify({'status': 'ok'}), 200
# Process event based on type
if event_type == 'subscription_activation':
handle_subscription_activation(payload)
elif event_type == 'subscription.payment':
handle_payment_event(payload)
elif event_type == 'subscription_cancellation':
handle_subscription_cancellation(payload)
# Mark as processed
processed_events.add(event_id)
return jsonify({'status': 'ok'}), 200
except Exception as e:
logger.error(f"Error processing webhook: {e}")
return jsonify({'error': 'Internal error'}), 500
def handle_subscription_activation(payload):
subscription_id = payload['entityId']
# Update subscription status in your system
logger.info(f"Subscription {subscription_id} activated")
def handle_payment_event(payload):
payment_id = payload['entityId']
status = payload['status']
if status == 'PAID':
# Update payment status, send confirmation email, etc.
logger.info(f"Payment {payment_id} paid")
elif status == 'FAILED':
# Handle failed payment, notify customer, etc.
error_code = payload.get('errorCode')
logger.warning(f"Payment {payment_id} failed: {error_code}")
def handle_subscription_cancellation(payload):
subscription_id = payload['entityId']
# Update subscription status, handle cancellation
logger.info(f"Subscription {subscription_id} cancelled")Next Steps
- Subscription Concepts: Review subscription entities and statuses
- Automatic Scheduling: Understand payment scheduling
- Retry Policies: Learn about retry handling
Updated about 8 hours ago