# 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**: ```json { "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**: ```json { "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**: ```json { "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**: ```json { "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**: ```json { "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**: ```json { "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**: ```json { "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**: ```json { "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: ```json { "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 occurred * **`entityId`**: 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 changed * **`message`**: Optional message (e.g., cancellation reason) * **`errorCode`**: Error code if applicable (e.g., `INSUFFICIENT_FUNDS`) * **`errorMessage`**: Human-readable error message * **`pspReference`**: 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 funds * **`REJECTED_BY_BANK`**: Payment rejected by the bank * **`CANCELLED_BY_USER`**: User cancelled the payment * **`ACCOUNT_CLOSED`**: User's account is closed * **`AUTHORIZATION_REVOKED`**: User revoked subscription authorization * **`PAYMENT_EXPIRED`**: Payment expired before processing * **`INVALID_ACCOUNT`**: Invalid account information * **`LIMIT_EXCEEDED`**: Transaction limit exceeded ### Subscription Errors * **`AUTHORIZATION_REJECTED`**: User rejected subscription authorization * **`INVALID_CUSTOMER`**: Customer information is invalid * **`CONFIGURATION_ERROR`**: Subscription configuration error ## Webhook Processing ### Receiving Webhooks 1. **Receive POST Request**: Your endpoint receives the webhook payload 2. **Validate Signature** (if implemented): Verify the webhook is from PayRetailers 3. **Check Idempotency**: Use `eventId` to ensure you haven't processed this event 4. **Process Event**: Update your system based on the event 5. **Respond 200 OK**: Return success response quickly ### Idempotency Always use the `eventId` field to ensure idempotency. Store processed event IDs and check before processing: ```python # 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 200 ``` ### Response 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 `eventId` before 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 `entityId` exists 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 ```python 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](subscription-concepts.md)**: Review subscription entities and statuses * **[Automatic Scheduling](automatic-scheduling.md)**: Understand payment scheduling * **[Retry Policies](retry-policies.md)**: Learn about retry handling