# Checkout Payments Webhook Real-time webhook notifications sent when payments are completed via BlockBee's hosted checkout solution. These webhooks notify you when a customer has successfully paid for their order and you should fulfill the purchase. **Method:** You choose when creating the payment: - `GET` (default) - Webhook data sent as URL query parameters - `POST` (set `post=1`) - Webhook data sent in request body **Content-Type:** You choose when creating the payment: - `application/x-www-form-urlencoded` (default) - Standard form encoding - `application/json` (set `json=1`) - JSON format in request body --- ## Webhook Overview Unlike custom payment flow webhooks, checkout payment webhooks are sent only once when the payment is complete and confirmed. There are no separate pending and confirmed states - you receive a single notification when the payment is ready to be processed. The webhook contains all the information you need to: - Verify the payment was legitimate - Update your order status - Fulfill the customer's purchase - Store transaction details for your records --- ## Webhook Fields - **`payment_url`** (`string`) (required): Payment link where the payment came from. **Example:** `https://pay.blockbee.io/payment/fG78jtx96ugjtu0eIbeLmFB9z0feJf9N` - **`redirect_url`** (`string`) (required): Redirect URL provided when requesting a new payment link. **Note:** Any query parameters you add to this URL (like `?order_id=12345`) will be included in the webhook payload, allowing you to track which order the payment belongs to. **Example:** `https://example.com/success/?order_id=12345` - **`value`** (`number`) (required): Amount in FIAT you requested when creating the payment link. **Example:** `20000` (represents $200.00 in cents) - **`payment_id`** (`string`) (required): Unique identifier for the payment that can be used to track and identify the transaction. **Important:** You can use this unique identifier for idempotency to prevent processing duplicate webhooks. Store this ID when creating the payment and verify it matches in the webhook to ensure you're processing the correct transaction. **Example:** `fG78jtx96ugjtu0eIbeLmFB9z0feJf9N` - **`currency`** (`string`) (required): FIAT currency. Should be the same that you set in your Payment Settings at BlockBee's Dashboard. **Example:** `usd` - **`is_paid`** (`string`) (required): Should always be `1` for completed payments. **Example:** `1` - **`paid_amount`** (`string`) (required): Amount paid in cryptocurrency by the user. **Note:** The cryptocurrency/token used to make the payment is described in the parameter `paid_coin`. **Example:** `1.23` - **`paid_amount_fiat`** (`string`) (required): Amount paid in the FIAT currency described in the parameter `currency`. **Example:** `21234.32` (represents $212.34) - **`received_amount`** (`string`) (required): Value forwarded to you, after fees deducted. **Note:** The cryptocurrency/token used to make the payment is described in the parameter `paid_coin`. **Example:** `9.24` - **`received_amount_fiat`** (`string`) (required): Value forwarded to you, after fees deducted in the FIAT currency selected. **Note:** FIAT currency. Should be the same that you set in your Payment Settings at BlockBee's Dashboard. **Example:** `9.24` (represents $9.24) - **`paid_coin`** (`string`) (required): Cryptocurrency/token used to make the payment. **Note:** The amount paid will be available in the parameter `paid_amount`. **Format:** - Native coins: `btc`, `eth`, `ltc`, `bch`, `trx` - ERC-20 tokens: `erc20_usdt`, `erc20_usdc` - TRC-20 tokens: `trc20_usdt`, `trc20_usdc` - BEP-20 tokens: `bep20_usdt`, `bep20_usdc` - Polygon tokens: `polygon_usdt`, `polygon_usdc` **Example:** `btc` - **`exchange_rate`** (`number`) (required): Exchange rate at the time of the payment. **Example:** `20000` - **`txid`** (`string`) (required): Transaction hash(es) of your client's payment(s). **Note:** If multiple, it means your customer had to make multiple transactions to fulfill the payment, since the Checkout page supports partial payments. **Example:** `0xa7551df44e487f9c0507d68d90193cde2604dfcefdc975bae54535a2e0f80b32,0x6e8b278e3db1948d2c694b7f709dd4e864ae80d516970ebfd05a98629b6efe15` - **`address`** (`string`) (required): Address generated by BlockBee where your client's payment was received. **Example:** `3PFoGK63cVVUWnd2vu7W1kM83NXUfvzMqM` - **`type`** (`string`) (required): Type of the IPN. **Example:** `payment` - **`status`** (`string`) (required): Status of the transaction. **Example:** `done` --- ## Security: Verify Webhook Signatures > **WARNING** >**Security Warning!** Always verify incoming webhook signatures. For a complete guide, see our **[Verify Webhook Signatures Guide](/webhooks/verify-webhook-signature)**. --- ## Reliability and Best Practices For important information on how BlockBee handles webhook delivery, retries, and timeouts, along with essential best practices for building a reliable webhook handler, please see our main guide. **➡️ [Read the Webhooks Overview](/webhooks)** --- ## Implementation Examples ```javascript // Express.js webhook handler app.post('/webhook', express.json(), (req, res) => { // 1. Verify the webhook signature if (!verifyWebhookSignature(req)) { return res.status(401).send('Unauthorized'); } // 2. Extract data from the payload const { is_paid, payment_id, value, paid_amount_fiat, paid_coin, txid, status, redirect_url } = req.body; // 3. Check for duplicates using payment_id if (isWebhookAlreadyProcessed(payment_id)) { return res.status(200).send('*ok*'); } // 4. Verify the payment is complete if (is_paid !== '1' || status !== 'done') { return res.status(200).send('*ok*'); } // 5. Verify the payment ID matches your records if (!verifyPaymentId(payment_id)) { console.error('Invalid payment ID received'); return res.status(200).send('*ok*'); } // 6. Extract order ID from redirect URL const urlParams = new URLSearchParams(redirect_url.split('?')[1]); const orderId = urlParams.get('order_id'); // 7. Process the successful payment // Update order status, fulfill purchase, etc. processSuccessfulPayment({ orderId, amount: paid_amount_fiat, coin: paid_coin, txid: txid }); // 8. Mark this webhook as processed using the payment_id markWebhookAsProcessed(payment_id); // Always respond to stop retries res.status(200).send('*ok*'); }); ``` ```php $orderId, 'amount' => $paid_amount_fiat, 'coin' => $paid_coin, 'txid' => $txid ]); // 8. Mark this webhook as processed using the payment_id markWebhookAsProcessed($payment_id); // Always respond to stop retries http_response_code(200); echo '*ok*'; ?> ``` ```python # Flask webhook handler @app.route('/webhook', methods=['POST']) def 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() is_paid = data.get('is_paid') payment_id = data.get('payment_id') value = data.get('value') paid_amount_fiat = data.get('paid_amount_fiat') paid_coin = data.get('paid_coin') txid = data.get('txid') status = data.get('status') redirect_url = data.get('redirect_url') # 3. Use the payment_id to prevent processing the same webhook twice if is_webhook_already_processed(payment_id): return '*ok*', 200 # 4. Verify the payment is complete if is_paid != '1' or status != 'done': return '*ok*', 200 # 5. Verify the payment ID matches your records if not verify_payment_id(payment_id): print('Invalid payment ID received') return '*ok*', 200 # 6. Extract order ID from redirect URL from urllib.parse import urlparse, parse_qs parsed_url = urlparse(redirect_url) query_params = parse_qs(parsed_url.query) order_id = query_params.get('order_id', [None])[0] # 7. Process the successful payment # Update order status, fulfill purchase, etc. process_successful_payment({ 'order_id': order_id, 'amount': paid_amount_fiat, 'coin': paid_coin, 'txid': txid }) # 8. Mark this webhook as processed using the payment_id mark_webhook_as_processed(payment_id) # Always respond to stop retries return '*ok*', 200 ``` ```ruby # Ruby on Rails webhook handler class WebhooksController < ApplicationController skip_before_action :verify_authenticity_token def payment # 1. Verify the webhook signature unless verify_webhook_signature(request) render plain: 'Unauthorized', status: :unauthorized return end # 2. Extract data from the payload is_paid = params[:is_paid] payment_id = params[:payment_id] value = params[:value] paid_amount_fiat = params[:paid_amount_fiat] paid_coin = params[:paid_coin] txid = params[:txid] status = params[:status] redirect_url = params[:redirect_url] # 3. Use the payment_id to prevent processing the same webhook twice if webhook_already_processed?(payment_id) render plain: '*ok*', status: :ok return end # 4. Verify the payment is complete unless is_paid == '1' && status == 'done' render plain: '*ok*', status: :ok return end # 5. Verify the payment ID matches your records unless verify_payment_id(payment_id) Rails.logger.error 'Invalid payment ID received' render plain: '*ok*', status: :ok return end # 6. Extract order ID from redirect URL uri = URI(redirect_url) query_params = URI.decode_www_form(uri.query || '') order_id = query_params.assoc('order_id')&.last # 7. Process the successful payment # Update order status, fulfill purchase, etc. process_successful_payment( order_id: order_id, amount: paid_amount_fiat, coin: paid_coin, txid: txid ) # 8. Mark this webhook as processed using the payment_id mark_webhook_as_processed(payment_id) # Always respond to stop retries render plain: '*ok*', status: :ok end end ``` ```csharp // ASP.NET Core webhook handler [ApiController] [Route("webhook")] public class WebhookController : ControllerBase { [HttpPost("payment")] public async Task PaymentWebhook([FromBody] PaymentWebhookPayload payload) { // 1. Verify the webhook signature if (!VerifyWebhookSignature(Request)) { return Unauthorized("Unauthorized"); } // 2. Extract data from the payload var isPaid = payload.IsPaid; var paymentId = payload.PaymentId; var value = payload.Value; var paidAmountFiat = payload.PaidAmountFiat; var paidCoin = payload.PaidCoin; var txid = payload.Txid; var status = payload.Status; var redirectUrl = payload.RedirectUrl; // 3. Use the payment_id to prevent processing the same webhook twice if (IsWebhookAlreadyProcessed(paymentId)) { return Ok("*ok*"); } // 4. Verify the payment is complete if (isPaid != "1" || status != "done") { return Ok("*ok*"); } // 5. Verify the payment ID matches your records if (!VerifyPaymentId(paymentId)) { _logger.LogError("Invalid payment ID received"); return Ok("*ok*"); } // 6. Extract order ID from redirect URL var uri = new Uri(redirectUrl); var queryParams = HttpUtility.ParseQueryString(uri.Query); var orderId = queryParams["order_id"]; // 7. Process the successful payment // Update order status, fulfill purchase, etc. await ProcessSuccessfulPayment(new PaymentData { OrderId = orderId, Amount = paidAmountFiat, Coin = paidCoin, Txid = txid }); // 8. Mark this webhook as processed using the payment_id MarkWebhookAsProcessed(paymentId); // Always respond to stop retries return Ok("*ok*"); } } public class PaymentWebhookPayload { [JsonProperty("is_paid")] public string IsPaid { get; set; } [JsonProperty("payment_id")] public string PaymentId { get; set; } [JsonProperty("value")] public int Value { get; set; } [JsonProperty("paid_amount_fiat")] public string PaidAmountFiat { get; set; } [JsonProperty("paid_coin")] public string PaidCoin { get; set; } [JsonProperty("txid")] public string Txid { get; set; } [JsonProperty("status")] public string Status { get; set; } [JsonProperty("redirect_url")] public string RedirectUrl { get; set; } } ``` ```java // Spring Boot webhook handler @RestController @RequestMapping("/webhook") public class WebhookController { @PostMapping("/payment") public ResponseEntity paymentWebhook(@RequestBody PaymentWebhookPayload payload) { // 1. Verify the webhook signature if (!verifyWebhookSignature(request)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Unauthorized"); } // 2. Extract data from the payload String isPaid = payload.getIsPaid(); String paymentId = payload.getPaymentId(); Integer value = payload.getValue(); String paidAmountFiat = payload.getPaidAmountFiat(); String paidCoin = payload.getPaidCoin(); String txid = payload.getTxid(); String status = payload.getStatus(); String redirectUrl = payload.getRedirectUrl(); // 3. Use the payment_id to prevent processing the same webhook twice if (isWebhookAlreadyProcessed(paymentId)) { return ResponseEntity.ok("*ok*"); } // 4. Verify the payment is complete if (!"1".equals(isPaid) || !"done".equals(status)) { return ResponseEntity.ok("*ok*"); } // 5. Verify the payment ID matches your records if (!verifyPaymentId(paymentId)) { log.error("Invalid payment ID received"); return ResponseEntity.ok("*ok*"); } // 6. Extract order ID from redirect URL String orderId = null; try { URI uri = new URI(redirectUrl); String query = uri.getQuery(); if (query != null) { String[] pairs = query.split("&"); for (String pair : pairs) { String[] keyValue = pair.split("="); if (keyValue.length == 2 && "order_id".equals(keyValue[0])) { orderId = URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8); break; } } } } catch (Exception e) { log.error("Error parsing redirect URL", e); } // 7. Process the successful payment // Update order status, fulfill purchase, etc. processSuccessfulPayment(new PaymentData(orderId, paidAmountFiat, paidCoin, txid)); // 8. Mark this webhook as processed using the payment_id markWebhookAsProcessed(paymentId); // Always respond to stop retries return ResponseEntity.ok("*ok*"); } } public class PaymentWebhookPayload { private String isPaid; private String paymentId; private Integer value; private String paidAmountFiat; private String paidCoin; private String txid; private String status; private String redirectUrl; // Getters and setters public String getIsPaid() { return isPaid; } public void setIsPaid(String isPaid) { this.isPaid = isPaid; } public String getPaymentId() { return paymentId; } public void setPaymentId(String paymentId) { this.paymentId = paymentId; } public Integer getValue() { return value; } public void setValue(Integer value) { this.value = value; } public String getPaidAmountFiat() { return paidAmountFiat; } public void setPaidAmountFiat(String paidAmountFiat) { this.paidAmountFiat = paidAmountFiat; } public String getPaidCoin() { return paidCoin; } public void setPaidCoin(String paidCoin) { this.paidCoin = paidCoin; } public String getTxid() { return txid; } public void setTxid(String txid) { this.txid = txid; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public String getRedirectUrl() { return redirectUrl; } public void setRedirectUrl(String redirectUrl) { this.redirectUrl = redirectUrl; } } ``` ```go // Go webhook handler package main import ( "encoding/json" "log" "net/http" "net/url" "strconv" ) type PaymentWebhookPayload struct { IsPaid string `json:"is_paid"` PaymentID string `json:"payment_id"` Value int `json:"value"` PaidAmountFiat string `json:"paid_amount_fiat"` PaidCoin string `json:"paid_coin"` Txid string `json:"txid"` Status string `json:"status"` RedirectURL string `json:"redirect_url"` } func paymentWebhookHandler(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 PaymentWebhookPayload if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } // 3. Use the payment_id to prevent processing the same webhook twice if isWebhookAlreadyProcessed(payload.PaymentID) { w.WriteHeader(http.StatusOK) w.Write([]byte("*ok*")) return } // 4. Verify the payment is complete if payload.IsPaid != "1" || payload.Status != "done" { w.WriteHeader(http.StatusOK) w.Write([]byte("*ok*")) return } // 5. Verify the payment ID matches your records if !verifyPaymentID(payload.PaymentID) { log.Printf("Invalid payment ID received: %s", payload.PaymentID) w.WriteHeader(http.StatusOK) w.Write([]byte("*ok*")) return } // 6. Extract order ID from redirect URL orderID := "" if parsedURL, err := url.Parse(payload.RedirectURL); err == nil { if queryParams := parsedURL.Query(); queryParams != nil { orderID = queryParams.Get("order_id") } } // 7. Process the successful payment // Update order status, fulfill purchase, etc. processSuccessfulPayment(PaymentData{ OrderID: orderID, Amount: payload.PaidAmountFiat, Coin: payload.PaidCoin, Txid: payload.Txid, }) // 8. Mark this webhook as processed using the payment_id markWebhookAsProcessed(payload.PaymentID) // Always respond to stop retries w.WriteHeader(http.StatusOK) w.Write([]byte("*ok*")) } type PaymentData struct { OrderID string Amount string Coin string Txid string } ``` ```bash # Test webhook with curl (for development/testing) curl -X POST http://localhost:3000/webhook \ -H "Content-Type: application/json" \ -d '{ "is_paid": "1", "payment_id": "fG78jtx96ugjtu0eIbeLmFB9z0feJf9N", "value": 20000, "paid_amount_fiat": "21234.32", "paid_coin": "btc", "txid": "0xa7551df44e487f9c0507d68d90193cde2604dfcefdc975bae54535a2e0f80b32", "status": "done", "redirect_url": "https://example.com/success/?order_id=12345" }' # Expected response: *ok* ``` ### Key Security Checks 1. **Verify Webhook Signature** - Ensure the request is from BlockBee 2. **Check Payment Status** - Only process when `is_paid=1` and `status=done` 3. **Validate Payment ID** - Match against your stored payment ID to ensure you're processing the correct transaction 4. **Extract Order ID** - Use the `redirect_url` to get your order identifier ### Best Practices - **Idempotency:** Use the `payment_id` to ensure you only process each payment once - **Respond Quickly:** Always respond with `*ok*` and a `200` status code - **Asynchronous Processing:** Handle long-running tasks in background jobs - **Error Handling:** Log errors but don't fail the webhook response - **Validation:** Verify all critical fields before processing the payment ## Webhook Payload Example ```json { "payment_id": "fG78jtx96ugjtu0eIbeLmFB9z0feJf9N", "payment_url": "https://pay.blockbee.io/payment/fG78jtx96ugjtu0eIbeLmFB9z0feJf9N", "redirect_url": "https://example.com/success/?order_id=12345", "value": 20000, "currency": "usd", "is_paid": "1", "paid_amount": "1.23", "paid_amount_fiat": "21234.32", "received_amount": "9.24", "received_amount_fiat": "9.24", "paid_coin": "btc", "exchange_rate": 20000, "txid": "0xa7551df44e487f9c0507d68d90193cde2604dfcefdc975bae54535a2e0f80b32,0x6e8b278e3db1948d2c694b7f709dd4e864ae80d516970ebfd05a98629b6efe15", "address": "3PFoGK63cVVUWnd2vu7W1kM83NXUfvzMqM", "type": "payment", "status": "done" } ```