## Overview BlockBee sends webhooks to notify your application about subscription events. There are two types of webhook notifications: 1. **Payment Notification** (`action=renew`) - Sent when a user makes a payment to extend or create a subscription 2. **Expiration Notification** (`action=expired`) - Sent when a subscription reaches its end date > **INFO** >**Important:** Always verify webhook signatures to ensure requests are from BlockBee. See our [Verify Webhook Signature guide](/webhooks/verify-webhook-signature) for implementation details. ## Payment Notification Webhook Sent when a user makes a payment (initial subscription payment or renewal). ### Webhook Fields - **`action`** (`string`) (required) - Example: `renew`: Action performed: `renew` if user made a payment, either the initial one or the extension. **Notes:** - Important to check the `subscription_end_date_ts` as it will be updated with the current subscription end date. - **`subscription_id`** (`string`) (required) - Example: `I55hhRINHptLJPwsqwYNxJOKBItRiq1o`: Unique token of the subscription. - **`subscription_url`** (`string`) (required) - Example: `https://pay.blockbee.io/subscription/I55hhRINHptLJPwsqwYNxJOKBItRiq1o`: Link to the subscription page. - **`subscription_start_date_ts`** (`integer`) (required) - Example: `1718131200`: Timestamp when the subscription started. - **`subscription_end_date_ts`** (`integer`) (required) - Example: `1720723200`: Timestamp when the subscription will expire. - **`subscription_option_slug`** (`string`) (required) - Example: `pro-plan`: Slug of the subscription option selected by the user. - **`subscription_option_duration_ts`** (`number`) (required) - Example: `2592000`: Duration of the subscription option selected by the user, in seconds. - **`subscription_option_value`** (`number`) (required) - Example: `5`: Value of the subscription option in fiat you selected in the Payment Settings. - **`subscription_user_id`** (`string`) (required) - Example: `user_123`: User identifier of your system provided in the here. - **`subscription_user_email`** (`string`) (required) - Example: `user@example.com`: Email address of the user, provided here. - **`payment_url`** (`string`) (required) - Example: `https://pay.blockbee.io/subscription/payment/ls25hhRINHptLJPwsqwYNxJOKBItRiq1o`: Payment link associated with this subscription renewal. - **`payment_redirect_url`** (`string`) (required) - Example: `https://example.com/payment-successful`: URL where the user will be redirected after payment. - **`payment_value`** (`number`) (required) - Example: `5`: Value expected to be paid in fiat. - **`payment_success_token`** (`string`) (required) - Example: `sU7EGOlRRxsxJu4zZoYLa69UXrSijzb73eP6nbQQgpJYAfSL3NiI407lpYqsMbR2`: Success token used to validate the payment. - **`payment_currency`** (`string`) (required) - Example: `usd`: Currency code (e.g. usd, eur). - **`payment_is_paid`** (`integer`) (required) - Example: `1`: Whether the payment is completed (1) or not (0). - **`payment_paid_amount`** (`number`) (required) - Example: `5`: Amount paid in crypto. - **`payment_paid_amount_fiat`** (`number`) (required) - Example: `4.85`: Amount paid in fiat. - **`payment_received_amount`** (`number`) (required) - Example: `5`: Amount received after fee deductions in crypto. - **`payment_received_amount_fiat`** (`number`) (required) - Example: `4.85`: Amount received after fee deductions in fiat. - **`payment_paid_coin`** (`string`) (required) - Example: `btc`: Coin used for payment (e.g. btc, trc20_usdt, etc...). - **`payment_exchange_rate`** (`number`) (required) - Example: `0.97`: Exchange rate at the time of payment. - **`payment_txid`** (`string`) (required) - Example: `0xa1234...,0xa5678...`: Comma-separated transaction IDs of the payment. - **`payment_address`** (`string`) (required) - Example: `0xabc123...`: Blockchain address that received the payment. - **`payment_type`** (`string`) (required) - Example: `payment`: Always `payment` for subscription renewals. - **`payment_status`** (`string`) (required) - Example: `done`: Payment status string. ### Example Payload ```json { "action": "renew", "subscription_id": "I55hhRINHptLJPwsqwYNxJOKBItRiq1o", "subscription_url": "https://pay.blockbee.io/subscription/I55hhRINHptLJPwsqwYNxJOKBItRiq1o", "subscription_start_date_ts": 1718131200, "subscription_end_date_ts": 1720723200, "subscription_option_slug": "pro-plan", "subscription_option_duration_ts": 2592000, "subscription_option_value": 5, "subscription_user_id": "user_123", "subscription_user_email": "user@example.com", "payment_url": "https://pay.blockbee.io/subscription/payment/ls25hhRINHptLJPwsqwYNxJOKBItRiq1o", "payment_redirect_url": "https://example.com/payment-successful", "payment_value": 5, "payment_success_token": "sU7EGOlRRxsxJu4zZoYLa69UXrSijzb73eP6nbQQgpJYAfSL3NiI407lpYqsMbR2", "payment_currency": "usd", "payment_is_paid": 1, "payment_paid_amount": 5, "payment_paid_amount_fiat": 4.85, "payment_received_amount": 5, "payment_received_amount_fiat": 4.85, "payment_paid_coin": "btc", "payment_exchange_rate": 0.97, "payment_txid": "0xa1234...,0xa5678...", "payment_address": "0xabc123...", "payment_type": "payment", "payment_status": "done" } ``` ## Expiration Notification Webhook Sent when a subscription has reached its end date and is no longer active. ### Webhook Fields - **`action`** (`string`) (required) - Example: `expired`: Indicates that the subscription has reached its end date and is no longer active. No further payments will be processed for expired subscriptions. - **`subscription_id`** (`string`) (required) - Example: `I55hhRINHptLJPwsqwYNxJOKBItRiq1o`: Unique token of the subscription. - **`subscription_url`** (`string`) (required) - Example: `https://pay.blockbee.io/subscription/I55hhRINHptLJPwsqwYNxJOKBItRiq1o`: Link to the subscription page. - **`subscription_start_date_ts`** (`integer`) (required) - Example: `1718131200`: Timestamp when the subscription started. - **`subscription_end_date_ts`** (`integer`) (required) - Example: `1720723200`: Timestamp when the subscription will expire. - **`subscription_option_slug`** (`string`) (required) - Example: `pro-plan`: Slug of the subscription option selected by the user. - **`subscription_option_duration_ts`** (`number`) (required) - Example: `2592000`: Duration of the subscription option selected by the user, in seconds. - **`subscription_option_value`** (`number`) (required) - Example: `5`: Value of the subscription option in fiat you selected in the Payment Settings. - **`subscription_user_id`** (`string`) (required) - Example: `user_123`: User identifier of your system provided in the here. - **`subscription_user_email`** (`string`) (required) - Example: `user@example.com`: Email address of the user, provided here. ### Example Payload ```json { "action": "expired", "subscription_id": "I55hhRINHptLJPwsqwYNxJOKBItRiq1o", "subscription_url": "https://pay.blockbee.io/subscription/I55hhRINHptLJPwsqwYNxJOKBItRiq1o", "subscription_start_date_ts": 1718131200, "subscription_end_date_ts": 1720723200, "subscription_option_slug": "pro-plan", "subscription_option_duration_ts": 2592000, "subscription_option_value": 5, "subscription_user_id": "user_123", "subscription_user_email": "user@example.com" } ``` ## Webhook Handling Here are code examples for handling subscription webhooks in different programming languages: ```javascript // Express.js webhook handler app.post('/subscription-webhook', express.json(), (req, res) => { // 1. Verify the webhook signature if (!verifyWebhookSignature(req)) { return res.status(401).send('Unauthorized'); } const { action, subscription_id, subscription_user_id, payment_status } = req.body; // Use subscription_id to prevent duplicate processing if (isWebhookAlreadyProcessed(subscription_id)) { return res.status(200).send('*ok*'); } if (action === 'renew' && payment_status === 'done') { // Grant or extend access for the user // Update subscription end date in your database // Send confirmation email to user console.log(`Subscription renewed for user: ${subscription_user_id}`); } else if (action === 'expired') { // Revoke access for the user // Update subscription status in your database // Send expiration notification to user console.log(`Subscription expired for user: ${subscription_user_id}`); } // Mark this webhook as processed markWebhookAsProcessed(subscription_id); // Always respond with *ok* res.status(200).send('*ok*'); }); ``` ```php ``` ```python # Flask webhook handler @app.route('/subscription-webhook', methods=['POST']) def subscription_webhook(): # 1. Verify the webhook signature if not verify_webhook_signature(request): return 'Unauthorized', 401 # 2. Extract data from the payload data = request.get_json() action = data.get('action') subscription_id = data.get('subscription_id') user_id = data.get('subscription_user_id') payment_status = data.get('payment_status') # 3. Use subscription_id to prevent duplicate processing if is_webhook_already_processed(subscription_id): return '*ok*', 200 if action == 'renew' and payment_status == 'done': # Grant or extend access for the user # Update subscription end date in your database # Send confirmation email to user print(f"Subscription renewed for user: {user_id}") elif action == 'expired': # Revoke access for the user # Update subscription status in your database # Send expiration notification to user print(f"Subscription expired for user: {user_id}") # 4. Mark this webhook as processed mark_webhook_as_processed(subscription_id) # 5. Always respond with *ok* return '*ok*', 200 ``` ```ruby # Ruby on Rails webhook handler class SubscriptionWebhooksController < ApplicationController skip_before_action :verify_authenticity_token def webhook # 1. Verify the webhook signature unless verify_webhook_signature(request) render plain: 'Unauthorized', status: :unauthorized return end # 2. Extract data from the payload action = params[:action] subscription_id = params[:subscription_id] user_id = params[:subscription_user_id] payment_status = params[:payment_status] # 3. Use subscription_id to prevent duplicate processing if webhook_already_processed?(subscription_id) render plain: '*ok*', status: :ok return end if action == 'renew' && payment_status == 'done' # Grant or extend access for the user # Update subscription end date in your database # Send confirmation email to user Rails.logger.info("Subscription renewed for user: #{user_id}") elsif action == 'expired' # Revoke access for the user # Update subscription status in your database # Send expiration notification to user Rails.logger.info("Subscription expired for user: #{user_id}") end # 4. Mark this webhook as processed mark_webhook_as_processed(subscription_id) # 5. Always respond with *ok* render plain: '*ok*', status: :ok end end ``` ```csharp // ASP.NET Core webhook handler [ApiController] [Route("webhook")] public class SubscriptionWebhookController : ControllerBase { [HttpPost("subscription")] public async Task SubscriptionWebhook([FromBody] SubscriptionWebhookPayload payload) { // 1. Verify the webhook signature if (!VerifyWebhookSignature(Request)) { return Unauthorized("Unauthorized"); } // 2. Extract data from the payload var action = payload.Action; var subscriptionId = payload.SubscriptionId; var userId = payload.SubscriptionUserId; var paymentStatus = payload.PaymentStatus; // 3. Use subscription_id to prevent duplicate processing if (IsWebhookAlreadyProcessed(subscriptionId)) { return Ok("*ok*"); } if (action == "renew" && paymentStatus == "done") { // Grant or extend access for the user // Update subscription end date in your database // Send confirmation email to user _logger.LogInformation($"Subscription renewed for user: {userId}"); } else if (action == "expired") { // Revoke access for the user // Update subscription status in your database // Send expiration notification to user _logger.LogInformation($"Subscription expired for user: {userId}"); } // 4. Mark this webhook as processed MarkWebhookAsProcessed(subscriptionId); // 5. Always respond with *ok* return Ok("*ok*"); } } public class SubscriptionWebhookPayload { [JsonProperty("action")] public string Action { get; set; } [JsonProperty("subscription_id")] public string SubscriptionId { get; set; } [JsonProperty("subscription_user_id")] public string SubscriptionUserId { get; set; } [JsonProperty("payment_status")] public string PaymentStatus { get; set; } } ``` ```java // Spring Boot webhook handler @RestController @RequestMapping("/webhook") public class SubscriptionWebhookController { @PostMapping("/subscription") public ResponseEntity subscriptionWebhook(@RequestBody SubscriptionWebhookPayload payload) { // 1. Verify the webhook signature if (!verifyWebhookSignature(request)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Unauthorized"); } // 2. Extract data from the payload String action = payload.getAction(); String subscriptionId = payload.getSubscriptionId(); String userId = payload.getSubscriptionUserId(); String paymentStatus = payload.getPaymentStatus(); // 3. Use subscription_id to prevent duplicate processing if (isWebhookAlreadyProcessed(subscriptionId)) { return ResponseEntity.ok("*ok*"); } if ("renew".equals(action) && "done".equals(paymentStatus)) { // Grant or extend access for the user // Update subscription end date in your database // Send confirmation email to user log.info("Subscription renewed for user: {}", userId); } else if ("expired".equals(action)) { // Revoke access for the user // Update subscription status in your database // Send expiration notification to user log.info("Subscription expired for user: {}", userId); } // 4. Mark this webhook as processed markWebhookAsProcessed(subscriptionId); // 5. Always respond with *ok* return ResponseEntity.ok("*ok*"); } } public class SubscriptionWebhookPayload { private String action; private String subscriptionId; private String subscriptionUserId; private String paymentStatus; // Getters and setters public String getAction() { return action; } public void setAction(String action) { this.action = action; } public String getSubscriptionId() { return subscriptionId; } public void setSubscriptionId(String subscriptionId) { this.subscriptionId = subscriptionId; } public String getSubscriptionUserId() { return subscriptionUserId; } public void setSubscriptionUserId(String subscriptionUserId) { this.subscriptionUserId = subscriptionUserId; } public String getPaymentStatus() { return paymentStatus; } public void setPaymentStatus(String paymentStatus) { this.paymentStatus = paymentStatus; } } ``` ```go // Go webhook handler package main import ( "encoding/json" "log" "net/http" ) type SubscriptionWebhookPayload struct { Action string `json:"action"` SubscriptionID string `json:"subscription_id"` SubscriptionUserID string `json:"subscription_user_id"` PaymentStatus string `json:"payment_status"` } func subscriptionWebhookHandler(w http.ResponseWriter, r *http.Request) { // 1. Verify the webhook signature if !verifyWebhookSignature(r) { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // 2. Extract data from the payload var payload SubscriptionWebhookPayload if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } // 3. Use subscription_id to prevent duplicate processing if isWebhookAlreadyProcessed(payload.SubscriptionID) { w.WriteHeader(http.StatusOK) w.Write([]byte("*ok*")) return } if payload.Action == "renew" && payload.PaymentStatus == "done" { // Grant or extend access for the user // Update subscription end date in your database // Send confirmation email to user log.Printf("Subscription renewed for user: %s", payload.SubscriptionUserID) } else if payload.Action == "expired" { // Revoke access for the user // Update subscription status in your database // Send expiration notification to user log.Printf("Subscription expired for user: %s", payload.SubscriptionUserID) } // 4. Mark this webhook as processed markWebhookAsProcessed(payload.SubscriptionID) // 5. Always respond with *ok* w.WriteHeader(http.StatusOK) w.Write([]byte("*ok*")) } ``` ```bash # Test webhook with curl (for development/testing) # Test payment notification webhook curl -X POST http://localhost:3000/webhook/subscription \ -H "Content-Type: application/json" \ -d '{ "action": "renew", "subscription_id": "I55hhRINHptLJPwsqwYNxJOKBItRiq1o", "subscription_url": "https://pay.blockbee.io/subscription/I55hhRINHptLJPwsqwYNxJOKBItRiq1o", "subscription_start_date_ts": 1718131200, "subscription_end_date_ts": 1720723200, "subscription_option_slug": "pro-plan", "subscription_option_duration_ts": 2592000, "subscription_option_value": 5, "subscription_user_id": "user_123", "subscription_user_email": "user@example.com", "payment_url": "https://pay.blockbee.io/subscription/payment/ls25hhRINHptLJPwsqwYNxJOKBItRiq1o", "payment_redirect_url": "https://example.com/payment-successful", "payment_value": 5, "payment_success_token": "sU7EGOlRRxsxJu4zZoYLa69UXrSijzb73eP6nbQQgpJYAfSL3NiI407lpYqsMbR2", "payment_currency": "usd", "payment_is_paid": 1, "payment_paid_amount": 5, "payment_paid_amount_fiat": 4.85, "payment_received_amount": 5, "payment_received_amount_fiat": 4.85, "payment_paid_coin": "btc", "payment_exchange_rate": 0.97, "payment_txid": "0xa1234...,0xa5678...", "payment_address": "0xabc123...", "payment_type": "payment", "payment_status": "done" }' # Test expiration notification webhook curl -X POST http://localhost:3000/webhook/subscription \ -H "Content-Type: application/json" \ -d '{ "action": "expired", "subscription_id": "I55hhRINHptLJPwsqwYNxJOKBItRiq1o", "subscription_url": "https://pay.blockbee.io/subscription/I55hhRINHptLJPwsqwYNxJOKBItRiq1o", "subscription_start_date_ts": 1718131200, "subscription_end_date_ts": 1720723200, "subscription_option_slug": "pro-plan", "subscription_option_duration_ts": 2592000, "subscription_option_value": 5, "subscription_user_id": "user_123", "subscription_user_email": "user@example.com" }' # Expected response: *ok* ``` ### Best Practices - **Verify Signatures:** Always verify webhook signatures to ensure requests are from BlockBee - **Idempotency:** Use the `subscription_id` to prevent processing the same webhook multiple times - **Respond Quickly:** Always respond with `*ok*` and a `200` status code to prevent BlockBee from resending the webhook - **Asynchronous Processing:** For long-running tasks, process them in a background job after responding to the webhook - **Error Handling:** Log errors but don't let them prevent the webhook response - **Database Updates:** Update your database with the new subscription end date when processing renewal webhooks > **TIP** >**Implementation Guide:** For detailed instructions on how to verify webhook signatures, see our [Verify Webhook Signature guide](/webhooks/verify-webhook-signature). ## Webhook Verification Always verify that webhooks are coming from BlockBee by checking the signature. This prevents malicious requests from impersonating BlockBee. > **WARNING** >**Security:** Never skip signature verification in production. This is crucial for the security of your application. For implementation details, see our [Verify Webhook Signature guide](/webhooks/verify-webhook-signature). ## Webhook Payload Example ```json { "action": "renew", "subscription_id": "I55hhRINHptLJPwsqwYNxJOKBItRiq1o", "subscription_url": "https://pay.blockbee.io/subscription/I55hhRINHptLJPwsqwYNxJOKBItRiq1o", "subscription_start_date_ts": 1718131200, "subscription_end_date_ts": 1720723200, "subscription_option_slug": "pro-plan", "subscription_option_duration_ts": 2592000, "subscription_option_value": 5, "subscription_user_id": "user_123", "subscription_user_email": "user@example.com", "payment_url": "https://pay.blockbee.io/subscription/payment/ls25hhRINHptLJPwsqwYNxJOKBItRiq1o", "payment_redirect_url": "https://example.com/payment-successful", "payment_value": 5, "payment_success_token": "sU7EGOlRRxsxJu4zZoYLa69UXrSijzb73eP6nbQQgpJYAfSL3NiI407lpYqsMbR2", "payment_currency": "usd", "payment_is_paid": 1, "payment_paid_amount": 5, "payment_paid_amount_fiat": 4.85, "payment_received_amount": 5, "payment_received_amount_fiat": 4.85, "payment_paid_coin": "btc", "payment_exchange_rate": 0.97, "payment_txid": "0xa1234...,0xa5678...", "payment_address": "0xabc123...", "payment_type": "payment", "payment_status": "done" } ```