> ## Documentation Index
> Fetch the complete documentation index at: https://docs.satsuma.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Real-time notifications for order updates and inventory changes

## Overview

Satsuma uses webhooks to notify your application about important events in real-time. This enables you to provide immediate updates to users about their orders and respond to inventory changes as they happen.

## Supported Events

### Order Events

* `order.created` - New order has been placed
* `order.confirmed` - Merchant has confirmed the order
* `order.ready` - Order is ready for pickup/delivery
* `order.completed` - Order has been fulfilled
* `order.cancelled` - Order has been cancelled
* `order.failed` - Order processing failed

### Inventory Events

* `inventory.updated` - Product inventory levels changed
* `inventory.low_stock` - Product inventory below threshold
* `inventory.out_of_stock` - Product is now unavailable

### Cart Events

* `cart.abandoned` - Cart inactive for specified period
* `cart.expires_soon` - Cart approaching expiration time

## Setting Up Webhooks

### Creating a Webhook Endpoint

<Steps>
  <Step title="Create Endpoint">
    Set up an HTTP endpoint in your application to receive webhook events
  </Step>

  <Step title="Configure in Dashboard">
    Add your endpoint URL and select events in the [Satsuma Dashboard](https://dashboard.satsuma.ai/webhooks)
  </Step>

  <Step title="Verify Signatures">
    Implement signature verification for security
  </Step>

  <Step title="Test Reception">
    Use our test events to verify your endpoint works correctly
  </Step>
</Steps>

### Dashboard Configuration

1. Navigate to **Developer Settings** > **Webhooks**
2. Click **Add Webhook Endpoint**
3. Configure your endpoint:
   * **URL**: Your HTTPS endpoint URL
   * **Events**: Select which events to receive
   * **Secret**: Optional signing secret for verification

<Warning>
  Webhook URLs must use HTTPS and return a 2xx status code within 10 seconds to be considered successful.
</Warning>

## Webhook Structure

All webhooks follow the same structure:

```json theme={null}
{
  "id": "evt_1234567890",
  "type": "order.confirmed",
  "created": "2024-01-15T10:30:00Z",
  "data": {
    "object": "order",
    "id": "ord_abcd1234",
    "status": "confirmed",
    "merchant_id": "merch_xyz789",
    "user_id": "user_123",
    "total": 24.99,
    "currency": "USD",
    "items": [
      {
        "product_id": "prod_456",
        "quantity": 2,
        "price": 12.49
      }
    ],
    "estimated_ready_time": "2024-01-15T11:00:00Z"
  }
}
```

## Event Examples

### Order Confirmed

```json theme={null}
{
  "id": "evt_order_confirmed_123",
  "type": "order.confirmed",
  "created": "2024-01-15T10:30:00Z",
  "data": {
    "object": "order",
    "id": "ord_abcd1234",
    "status": "confirmed",
    "merchant_id": "merch_xyz789",
    "estimated_ready_time": "2024-01-15T11:00:00Z",
    "fulfillment_method": "pickup",
    "special_instructions": "Extra hot, no foam"
  }
}
```

### Inventory Updated

```json theme={null}
{
  "id": "evt_inventory_updated_456", 
  "type": "inventory.updated",
  "created": "2024-01-15T10:35:00Z",
  "data": {
    "object": "inventory",
    "product_id": "prod_789",
    "merchant_id": "merch_xyz789",
    "previous_quantity": 50,
    "current_quantity": 48,
    "location_id": "loc_abc123"
  }
}
```

### Cart Abandoned

```json theme={null}
{
  "id": "evt_cart_abandoned_789",
  "type": "cart.abandoned", 
  "created": "2024-01-15T12:00:00Z",
  "data": {
    "object": "cart",
    "id": "cart_def456",
    "user_id": "user_123",
    "last_activity": "2024-01-15T10:00:00Z",
    "total_value": 42.50,
    "item_count": 3
  }
}
```

## Implementing Webhook Handlers

<CodeGroup>
  ```bash curl theme={null}
  # Test webhook endpoint with curl
  curl -X POST https://your-app.com/webhooks/satsuma \
    -H 'Content-Type: application/json' \
    -H 'X-Signature: your-computed-hmac-signature' \
    -d '{
      "id": "evt_test_123",
      "type": "order.confirmed",
      "created": "2024-01-15T10:30:00Z",
      "data": {
        "object": "order",
        "id": "ord_test_123",
        "status": "confirmed",
        "user_id": "user_123"
      }
    }'

  # Verify webhook endpoint health
  curl -X GET https://your-app.com/webhooks/health
  ```

  ```python Python theme={null}
  from flask import Flask, request, abort
  import hmac
  import hashlib
  import json
  import os

  app = Flask(__name__)

  @app.route('/webhooks/satsuma', methods=['POST'])
  def handle_webhook():
      signature = request.headers.get('X-Signature')
      payload = request.get_data()
      
      # Verify webhook signature
      if not verify_signature(payload, signature):
          abort(401)
      
      event = json.loads(payload)
      
      # Route to appropriate handler
      handlers = {
          'order.confirmed': handle_order_confirmed,
          'order.ready': handle_order_ready,
          'inventory.updated': handle_inventory_updated,
      }
      
      handler = handlers.get(event['type'])
      if handler:
          handler(event['data'])
      else:
          app.logger.info(f"Unhandled event type: {event['type']}")
      
      return 'OK', 200

  def handle_order_confirmed(order):
      # Send email notification
      email_service.send(
          to=get_user_email(order['user_id']),
          subject='Order Confirmed',
          template='order_confirmed',
          data=order
      )
      
      # Update order status
      db.orders.update(
          {'id': order['id']},
          {'$set': {'status': 'confirmed', 'updated_at': datetime.utcnow()}}
      )

  def verify_signature(payload: bytes, signature: str) -> bool:
      secret = os.environ['SATSUMA_WEBHOOK_SECRET']
      expected_signature = hmac.new(
          secret.encode('utf-8'),
          payload,
          hashlib.sha256
      ).hexdigest()
      
      return hmac.compare_digest(signature, expected_signature)
  ```

  ```typescript TypeScript theme={null}
  import express, { Request, Response } from 'express';
  import crypto from 'crypto';

  interface WebhookEvent {
    id: string;
    type: string;
    created: string;
    data: Record<string, any>;
  }

  interface OrderData {
    object: 'order';
    id: string;
    status: string;
    user_id: string;
    estimated_ready_time?: string;
    [key: string]: any;
  }

  const app = express();

  app.use(express.raw({ type: 'application/json' }));

  app.post('/webhooks/satsuma', (req: Request, res: Response) => {
    const signature = req.headers['x-signature'] as string;
    const payload = req.body as Buffer;
    
    // Verify webhook signature
    if (!verifySignature(payload, signature)) {
      return res.status(401).send('Unauthorized');
    }

    const event: WebhookEvent = JSON.parse(payload.toString());
    
    // Handle different event types
    switch (event.type) {
      case 'order.confirmed':
        handleOrderConfirmed(event.data as OrderData);
        break;
      case 'order.ready':
        handleOrderReady(event.data as OrderData);
        break;
      case 'inventory.updated':
        handleInventoryUpdated(event.data);
        break;
      default:
        console.log(`Unhandled event type: ${event.type}`);
    }

    res.status(200).send('OK');
  });

  function handleOrderConfirmed(order: OrderData): void {
    // Send push notification to user
    notificationService.send(order.user_id, {
      title: 'Order Confirmed!',
      body: `Your order #${order.id} has been confirmed and will be ready at ${order.estimated_ready_time}`,
      data: { order_id: order.id }
    });
    
    // Update local database
    database.updateOrder(order.id, { status: 'confirmed' });
  }

  function verifySignature(payload: Buffer, signature: string): boolean {
    const secret = process.env.SATSUMA_WEBHOOK_SECRET!;
    const expectedSignature = crypto
      .createHmac('sha256', secret)
      .update(payload)
      .digest('hex');
      
    return crypto.timingSafeEqual(
      Buffer.from(signature, 'hex'),
      Buffer.from(expectedSignature, 'hex')
    );
  }
  ```

  ```java Java theme={null}
  import org.springframework.boot.SpringApplication;
  import org.springframework.boot.autoconfigure.SpringBootApplication;
  import org.springframework.http.HttpStatus;
  import org.springframework.http.ResponseEntity;
  import org.springframework.web.bind.annotation.*;

  import javax.crypto.Mac;
  import javax.crypto.spec.SecretKeySpec;
  import java.nio.charset.StandardCharsets;
  import java.security.MessageDigest;
  import java.util.Map;

  @SpringBootApplication
  @RestController
  public class WebhookController {

      private final String WEBHOOK_SECRET = System.getenv("SATSUMA_WEBHOOK_SECRET");

      @PostMapping("/webhooks/satsuma")
      public ResponseEntity<String> handleWebhook(
              @RequestBody String payload,
              @RequestHeader("X-Signature") String signature) {
          
          // Verify webhook signature
          if (!verifySignature(payload, signature)) {
              return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                  .body("Unauthorized");
          }

          try {
              ObjectMapper mapper = new ObjectMapper();
              Map<String, Object> event = mapper.readValue(payload, Map.class);
              
              String eventType = (String) event.get("type");
              Map<String, Object> data = (Map<String, Object>) event.get("data");
              
              // Handle different event types
              switch (eventType) {
                  case "order.confirmed":
                      handleOrderConfirmed(data);
                      break;
                  case "order.ready":
                      handleOrderReady(data);
                      break;
                  case "inventory.updated":
                      handleInventoryUpdated(data);
                      break;
                  default:
                      System.out.println("Unhandled event type: " + eventType);
              }
              
              return ResponseEntity.ok("OK");
              
          } catch (Exception e) {
              System.err.println("Error processing webhook: " + e.getMessage());
              return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                  .body("Processing failed");
          }
      }

      private void handleOrderConfirmed(Map<String, Object> order) {
          String userId = (String) order.get("user_id");
          String orderId = (String) order.get("id");
          String estimatedReadyTime = (String) order.get("estimated_ready_time");
          
          // Send push notification
          notificationService.send(userId, Map.of(
              "title", "Order Confirmed!",
              "body", "Your order #" + orderId + " has been confirmed and will be ready at " + estimatedReadyTime,
              "data", Map.of("order_id", orderId)
          ));
          
          // Update database
          orderRepository.updateStatus(orderId, "confirmed");
      }

      private boolean verifySignature(String payload, String signature) {
          try {
              Mac mac = Mac.getInstance("HmacSHA256");
              SecretKeySpec secretKey = new SecretKeySpec(
                  WEBHOOK_SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
              mac.init(secretKey);
              
              byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
              String expectedSignature = bytesToHex(hash);
              
              return MessageDigest.isEqual(
                  signature.getBytes(StandardCharsets.UTF_8),
                  expectedSignature.getBytes(StandardCharsets.UTF_8)
              );
          } catch (Exception e) {
              System.err.println("Error verifying signature: " + e.getMessage());
              return false;
          }
      }

      private String bytesToHex(byte[] bytes) {
          StringBuilder result = new StringBuilder();
          for (byte b : bytes) {
              result.append(String.format("%02x", b));
          }
          return result.toString();
      }

      public static void main(String[] args) {
          SpringApplication.run(WebhookController.class, args);
      }
  }
  ```
</CodeGroup>

## Signature Verification

Verify webhook authenticity using HMAC-SHA256 signatures:

<CodeGroup>
  ```bash curl theme={null}
  # Test signature verification endpoint
  curl -X POST https://your-app.com/webhooks/verify \
    -H 'Content-Type: application/json' \
    -H 'X-Signature: computed-hmac-signature' \
    -d '{"test": "payload"}'

  # Generate HMAC signature for testing (requires openssl)
  echo -n '{"test": "payload"}' | \
    openssl dgst -sha256 -hmac "your-webhook-secret" -binary | \
    xxd -p -c 256
  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  import os

  def verify_signature(payload: bytes, signature: str) -> bool:
      """Verify webhook signature using HMAC-SHA256"""
      secret = os.environ['SATSUMA_WEBHOOK_SECRET']
      expected_signature = hmac.new(
          secret.encode('utf-8'),
          payload,
          hashlib.sha256
      ).hexdigest()
      
      return hmac.compare_digest(signature, expected_signature)

  # Example usage
  if __name__ == "__main__":
      test_payload = b'{"test": "payload"}'
      test_signature = "your-computed-signature"
      
      is_valid = verify_signature(test_payload, test_signature)
      print(f"Signature valid: {is_valid}")
  ```

  ```typescript TypeScript theme={null}
  import crypto from 'crypto';

  function verifySignature(payload: Buffer, signature: string): boolean {
    const secret = process.env.SATSUMA_WEBHOOK_SECRET!;
    const expectedSignature = crypto
      .createHmac('sha256', secret)
      .update(payload)
      .digest('hex');
      
    return crypto.timingSafeEqual(
      Buffer.from(signature, 'hex'),
      Buffer.from(expectedSignature, 'hex')
    );
  }

  // Type-safe signature verification with error handling
  function safeVerifySignature(
    payload: Buffer, 
    signature: string | undefined
  ): { isValid: boolean; error?: string } {
    if (!signature) {
      return { isValid: false, error: 'Missing signature' };
    }
    
    if (!process.env.SATSUMA_WEBHOOK_SECRET) {
      return { isValid: false, error: 'Missing webhook secret' };
    }
    
    try {
      return { isValid: verifySignature(payload, signature) };
    } catch (error) {
      return { 
        isValid: false, 
        error: `Signature verification failed: ${error}` 
      };
    }
  }
  ```

  ```java Java theme={null}
  import javax.crypto.Mac;
  import javax.crypto.spec.SecretKeySpec;
  import java.nio.charset.StandardCharsets;
  import java.security.MessageDigest;
  import java.security.NoSuchAlgorithmException;
  import java.security.InvalidKeyException;

  public class SignatureVerifier {
      
      private final String webhookSecret;
      
      public SignatureVerifier(String webhookSecret) {
          this.webhookSecret = webhookSecret;
      }
      
      /**
       * Verify webhook signature using HMAC-SHA256
       */
      public boolean verifySignature(String payload, String signature) {
          try {
              Mac mac = Mac.getInstance("HmacSHA256");
              SecretKeySpec secretKey = new SecretKeySpec(
                  webhookSecret.getBytes(StandardCharsets.UTF_8), 
                  "HmacSHA256"
              );
              mac.init(secretKey);
              
              byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
              String expectedSignature = bytesToHex(hash);
              
              return MessageDigest.isEqual(
                  signature.getBytes(StandardCharsets.UTF_8),
                  expectedSignature.getBytes(StandardCharsets.UTF_8)
              );
          } catch (NoSuchAlgorithmException | InvalidKeyException e) {
              System.err.println("Error verifying signature: " + e.getMessage());
              return false;
          }
      }
      
      private String bytesToHex(byte[] bytes) {
          StringBuilder result = new StringBuilder();
          for (byte b : bytes) {
              result.append(String.format("%02x", b));
          }
          return result.toString();
      }
      
      // Example usage
      public static void main(String[] args) {
          String secret = System.getenv("SATSUMA_WEBHOOK_SECRET");
          SignatureVerifier verifier = new SignatureVerifier(secret);
          
          String testPayload = "{\"test\": \"payload\"}";
          String testSignature = "your-computed-signature";
          
          boolean isValid = verifier.verifySignature(testPayload, testSignature);
          System.out.println("Signature valid: " + isValid);
      }
  }
  ```
</CodeGroup>

## Error Handling & Retries

### Delivery Guarantees

Satsuma guarantees webhook delivery through:

* **Automatic Retries**: Failed webhooks are retried with exponential backoff
* **Retry Schedule**: Immediate, 5min, 15min, 1hr, 6hr, 24hr
* **Manual Retry**: Retry failed deliveries from the dashboard
* **Dead Letter Queue**: Webhooks that fail all retries are stored for investigation

### Handling Failures

Implement robust error handling:

<CodeGroup>
  ```bash curl theme={null}
  # Test error scenarios
  curl -X POST https://your-app.com/webhooks/satsuma \
    -H 'Content-Type: application/json' \
    -H 'X-Signature: invalid-signature' \
    -d '{"test": "invalid payload"}' \
    -w "HTTP Status: %{http_code}\n"

  # Test timeout scenario
  curl -X POST https://your-app.com/webhooks/satsuma \
    --max-time 5 \
    -H 'Content-Type: application/json' \
    -d '{"type": "slow.event"}'
  ```

  ```python Python theme={null}
  from flask import Flask, request, jsonify
  import asyncio
  import logging
  from typing import Dict, Any

  app = Flask(__name__)
  logger = logging.getLogger(__name__)

  @app.route('/webhooks/satsuma', methods=['POST'])
  async def handle_webhook():
      try:
          payload = request.get_data()
          signature = request.headers.get('X-Signature')
          
          if not verify_signature(payload, signature):
              return jsonify({"error": "Invalid signature"}), 401
              
          event = json.loads(payload)
          
          # Process with timeout
          try:
              await asyncio.wait_for(
                  process_event(event), 
                  timeout=8.0
              )
              return 'OK', 200
              
          except asyncio.TimeoutError:
              logger.error('Webhook processing timeout')
              return jsonify({"error": "Processing timeout"}), 500
              
      except Exception as error:
          logger.error(f'Webhook processing failed: {error}')
          
          # Return appropriate status code
          status_code = 500 if is_retryable_error(error) else 400
          return jsonify({"error": str(error)}), status_code

  def is_retryable_error(error: Exception) -> bool:
      """Determine if error should trigger retry"""
      retryable_errors = [
          'TimeoutError', 'ConnectionError', 
          'DatabaseUnavailable', 'ServiceUnavailable'
      ]
      return type(error).__name__ in retryable_errors
  ```

  ```typescript TypeScript theme={null}
  import express, { Request, Response } from 'express';

  interface WebhookError extends Error {
    code?: string;
    retryable?: boolean;
  }

  app.post('/webhooks/satsuma', async (req: Request, res: Response) => {
    try {
      const payload = req.body as Buffer;
      const signature = req.headers['x-signature'] as string;
      
      if (!verifySignature(payload, signature)) {
        return res.status(401).json({ error: 'Invalid signature' });
      }
      
      const event: WebhookEvent = JSON.parse(payload.toString());
      
      // Process event with timeout
      await Promise.race([
        processEvent(event),
        new Promise<never>((_, reject) => 
          setTimeout(() => reject(new Error('Timeout')), 8000)
        )
      ]);
      
      res.status(200).send('OK');
    } catch (error) {
      console.error('Webhook processing failed:', error);
      
      // Return 5xx for retryable errors, 4xx for permanent failures
      const statusCode = isRetryableError(error as WebhookError) ? 500 : 400;
      res.status(statusCode).json({ 
        error: error instanceof Error ? error.message : 'Unknown error' 
      });
    }
  });

  function isRetryableError(error: WebhookError): boolean {
    // Retry for network issues, timeouts, temporary database errors
    const retryableCodes = ['TIMEOUT', 'ECONNREFUSED', 'DATABASE_UNAVAILABLE'];
    return retryableCodes.includes(error.code || '') || error.retryable === true;
  }

  async function processEvent(event: WebhookEvent): Promise<void> {
    // Implement your event processing logic here
    // Throw errors with appropriate codes for retry logic
    switch (event.type) {
      case 'order.confirmed':
        await handleOrderConfirmed(event.data);
        break;
      default:
        console.log(`Unhandled event type: ${event.type}`);
    }
  }
  ```

  ```java Java theme={null}
  import org.springframework.http.HttpStatus;
  import org.springframework.http.ResponseEntity;
  import org.springframework.web.bind.annotation.*;
  import java.util.concurrent.*;
  import java.util.Map;
  import java.util.HashMap;

  @RestController
  public class WebhookController {
      
      private final ExecutorService executor = Executors.newFixedThreadPool(10);
      private final Logger logger = LoggerFactory.getLogger(WebhookController.class);
      
      @PostMapping("/webhooks/satsuma")
      public ResponseEntity<Map<String, String>> handleWebhook(
              @RequestBody String payload,
              @RequestHeader(value = "X-Signature", required = false) String signature) {
          
          try {
              // Verify signature
              if (signature == null || !verifySignature(payload, signature)) {
                  return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                      .body(Map.of("error", "Invalid signature"));
              }
              
              ObjectMapper mapper = new ObjectMapper();
              Map<String, Object> event = mapper.readValue(payload, Map.class);
              
              // Process with timeout
              CompletableFuture<Void> processingFuture = CompletableFuture
                  .supplyAsync(() -> {
                      try {
                          processEvent(event);
                          return null;
                      } catch (Exception e) {
                          throw new RuntimeException(e);
                      }
                  }, executor);
              
              // Wait with timeout
              processingFuture.get(8, TimeUnit.SECONDS);
              
              return ResponseEntity.ok(Map.of("status", "OK"));
              
          } catch (TimeoutException e) {
              logger.error("Webhook processing timeout", e);
              return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                  .body(Map.of("error", "Processing timeout"));
                  
          } catch (Exception e) {
              logger.error("Webhook processing failed", e);
              
              // Return appropriate status code
              HttpStatus status = isRetryableError(e) ? 
                  HttpStatus.INTERNAL_SERVER_ERROR : HttpStatus.BAD_REQUEST;
              
              return ResponseEntity.status(status)
                  .body(Map.of("error", e.getMessage()));
          }
      }
      
      private boolean isRetryableError(Exception error) {
          // Retry for network issues, timeouts, temporary database errors
          String errorClass = error.getClass().getSimpleName();
          return errorClass.contains("Timeout") ||
                 errorClass.contains("Connection") ||
                 errorClass.contains("Database") ||
                 error.getMessage().contains("unavailable");
      }
      
      private void processEvent(Map<String, Object> event) throws Exception {
          String eventType = (String) event.get("type");
          Map<String, Object> data = (Map<String, Object>) event.get("data");
          
          switch (eventType) {
              case "order.confirmed":
                  handleOrderConfirmed(data);
                  break;
              case "order.ready":
                  handleOrderReady(data);
                  break;
              default:
                  logger.info("Unhandled event type: {}", eventType);
          }
      }
  }
  ```
</CodeGroup>

## Testing Webhooks

### Local Testing with ngrok

```bash theme={null}
# Install ngrok
npm install -g ngrok

# Start your local server
node server.js

# Expose local server
ngrok http 3000

# Use the ngrok URL in your webhook configuration
# https://abc123.ngrok.io/webhooks/satsuma
```

### Test Events

Send test events from the dashboard to verify your endpoint:

<CodeGroup>
  ```bash curl theme={null}
  # Test order confirmation webhook
  curl -X POST https://your-app.com/webhooks/satsuma \
    -H 'Content-Type: application/json' \
    -H 'X-Signature: your-computed-hmac-signature' \
    -d '{
      "id": "evt_test_123",
      "type": "order.confirmed",
      "created": "2024-01-15T10:30:00Z",
      "data": {
        "object": "order",
        "id": "ord_test_123",
        "status": "confirmed",
        "user_id": "user_123"
      }
    }'

  # Test inventory update webhook
  curl -X POST https://your-app.com/webhooks/satsuma \
    -H 'Content-Type: application/json' \
    -H 'X-Signature: your-computed-hmac-signature' \
    -d '{
      "id": "evt_inventory_test_456",
      "type": "inventory.updated",
      "created": "2024-01-15T10:35:00Z",
      "data": {
        "object": "inventory",
        "product_id": "prod_789",
        "current_quantity": 48
      }
    }'

  # Test webhook endpoint health
  curl -X GET https://your-app.com/webhooks/health \
    -H 'Accept: application/json'
  ```

  ```python Python theme={null}
  import requests
  import json
  import hmac
  import hashlib
  import os

  def test_webhook_endpoint(endpoint_url: str, event_data: dict):
      """Test webhook endpoint with proper signature"""
      payload = json.dumps(event_data)
      secret = os.environ.get('SATSUMA_WEBHOOK_SECRET', 'test-secret')
      
      # Generate signature
      signature = hmac.new(
          secret.encode('utf-8'),
          payload.encode('utf-8'),
          hashlib.sha256
      ).hexdigest()
      
      headers = {
          'Content-Type': 'application/json',
          'X-Signature': signature
      }
      
      response = requests.post(endpoint_url, data=payload, headers=headers)
      print(f"Status: {response.status_code}, Response: {response.text}")
      return response

  # Test order confirmed event
  test_event = {
      "id": "evt_test_123",
      "type": "order.confirmed",
      "created": "2024-01-15T10:30:00Z",
      "data": {
          "object": "order",
          "id": "ord_test_123",
          "status": "confirmed",
          "user_id": "user_123"
      }
  }

  test_webhook_endpoint("https://your-app.com/webhooks/satsuma", test_event)
  ```

  ```typescript TypeScript theme={null}
  import axios from 'axios';
  import crypto from 'crypto';

  interface TestWebhookOptions {
    endpointUrl: string;
    eventData: WebhookEvent;
    secret?: string;
  }

  async function testWebhookEndpoint({
    endpointUrl,
    eventData,
    secret = process.env.SATSUMA_WEBHOOK_SECRET || 'test-secret'
  }: TestWebhookOptions): Promise<void> {
    const payload = JSON.stringify(eventData);
    
    // Generate signature
    const signature = crypto
      .createHmac('sha256', secret)
      .update(payload, 'utf8')
      .digest('hex');
      
    try {
      const response = await axios.post(endpointUrl, payload, {
        headers: {
          'Content-Type': 'application/json',
          'X-Signature': signature
        },
        timeout: 10000
      });
      
      console.log(`✅ Test passed: ${response.status} ${response.statusText}`);
      console.log('Response:', response.data);
    } catch (error) {
      if (axios.isAxiosError(error)) {
        console.error(`❌ Test failed: ${error.response?.status} ${error.response?.statusText}`);
        console.error('Response:', error.response?.data);
      } else {
        console.error('❌ Network error:', error);
      }
    }
  }

  // Test order confirmed event
  const testEvent: WebhookEvent = {
    id: 'evt_test_123',
    type: 'order.confirmed',
    created: '2024-01-15T10:30:00Z',
    data: {
      object: 'order',
      id: 'ord_test_123',
      status: 'confirmed',
      user_id: 'user_123'
    }
  };

  testWebhookEndpoint({
    endpointUrl: 'https://your-app.com/webhooks/satsuma',
    eventData: testEvent
  });
  ```

  ```java Java theme={null}
  import okhttp3.*;
  import com.fasterxml.jackson.databind.ObjectMapper;
  import javax.crypto.Mac;
  import javax.crypto.spec.SecretKeySpec;
  import java.nio.charset.StandardCharsets;
  import java.io.IOException;
  import java.util.Map;
  import java.util.HashMap;

  public class WebhookTester {
      
      private final OkHttpClient client = new OkHttpClient();
      private final ObjectMapper mapper = new ObjectMapper();
      private final String secret;
      
      public WebhookTester(String secret) {
          this.secret = secret;
      }
      
      public void testWebhookEndpoint(String endpointUrl, Map<String, Object> eventData) 
              throws Exception {
          
          String payload = mapper.writeValueAsString(eventData);
          String signature = generateSignature(payload);
          
          RequestBody body = RequestBody.create(
              payload, 
              MediaType.get("application/json")
          );
          
          Request request = new Request.Builder()
              .url(endpointUrl)
              .post(body)
              .addHeader("Content-Type", "application/json")
              .addHeader("X-Signature", signature)
              .build();
              
          try (Response response = client.newCall(request).execute()) {
              System.out.printf("Status: %d, Response: %s%n", 
                  response.code(), response.body().string());
                  
              if (response.isSuccessful()) {
                  System.out.println("✅ Test passed");
              } else {
                  System.out.println("❌ Test failed");
              }
          }
      }
      
      private String generateSignature(String payload) throws Exception {
          Mac mac = Mac.getInstance("HmacSHA256");
          SecretKeySpec secretKey = new SecretKeySpec(
              secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
          mac.init(secretKey);
          
          byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
          return bytesToHex(hash);
      }
      
      private String bytesToHex(byte[] bytes) {
          StringBuilder result = new StringBuilder();
          for (byte b : bytes) {
              result.append(String.format("%02x", b));
          }
          return result.toString();
      }
      
      public static void main(String[] args) throws Exception {
          WebhookTester tester = new WebhookTester(
              System.getenv("SATSUMA_WEBHOOK_SECRET")
          );
          
          // Test order confirmed event
          Map<String, Object> testEvent = new HashMap<>();
          testEvent.put("id", "evt_test_123");
          testEvent.put("type", "order.confirmed");
          testEvent.put("created", "2024-01-15T10:30:00Z");
          
          Map<String, Object> data = new HashMap<>();
          data.put("object", "order");
          data.put("id", "ord_test_123");
          data.put("status", "confirmed");
          data.put("user_id", "user_123");
          testEvent.put("data", data);
          
          tester.testWebhookEndpoint(
              "https://your-app.com/webhooks/satsuma", 
              testEvent
          );
      }
  }
  ```
</CodeGroup>

## Monitoring & Debugging

### Webhook Logs

View delivery logs in your dashboard:

* **Status**: Success, failure, or pending retry
* **Response**: HTTP status and response body from your endpoint
* **Timing**: Request duration and retry attempts
* **Payload**: Full webhook payload for debugging

### Health Monitoring

Monitor your webhook endpoint health:

<CodeGroup>
  ```bash curl theme={null}
  # Check webhook endpoint health
  curl -X GET https://your-app.com/webhooks/health \
    -H 'Accept: application/json' \
    -w "Status: %{http_code}, Time: %{time_total}s\n"

  # Monitor webhook processing metrics
  curl -X GET https://your-app.com/webhooks/metrics \
    -H 'Accept: application/json'

  # Test webhook endpoint availability
  curl -X OPTIONS https://your-app.com/webhooks/satsuma \
    -H 'Access-Control-Request-Method: POST'
  ```

  ```python Python theme={null}
  from flask import Flask, jsonify
  import time
  import psutil
  import logging
  from typing import Dict, Any

  app = Flask(__name__)
  logger = logging.getLogger(__name__)

  # Metrics storage (use Redis or proper metrics system in production)
  metrics_store = {
      'webhook_received': 0,
      'webhook_success': 0,
      'webhook_failure': 0,
      'processing_times': []
  }

  @app.route('/webhooks/health', methods=['GET'])
  def health_check():
      """Health check endpoint for webhook service"""
      return jsonify({
          'status': 'healthy',
          'timestamp': datetime.utcnow().isoformat(),
          'uptime': time.time() - start_time,
          'memory_usage': psutil.virtual_memory().percent,
          'cpu_usage': psutil.cpu_percent()
      })

  @app.route('/webhooks/metrics', methods=['GET'])
  def webhook_metrics():
      """Webhook processing metrics"""
      success_rate = (
          metrics_store['webhook_success'] / 
          max(metrics_store['webhook_received'], 1)
      ) * 100
      
      avg_processing_time = (
          sum(metrics_store['processing_times']) / 
          max(len(metrics_store['processing_times']), 1)
      )
      
      return jsonify({
          'total_received': metrics_store['webhook_received'],
          'success_count': metrics_store['webhook_success'],
          'failure_count': metrics_store['webhook_failure'],
          'success_rate': f"{success_rate:.2f}%",
          'avg_processing_time': f"{avg_processing_time:.3f}s"
      })

  def log_webhook_metrics(event_type: str, processing_time: float, success: bool):
      """Log webhook processing metrics"""
      metrics_store['webhook_received'] += 1
      metrics_store['processing_times'].append(processing_time)
      
      if success:
          metrics_store['webhook_success'] += 1
      else:
          metrics_store['webhook_failure'] += 1
      
      logger.info(
          f"Webhook processed: type={event_type}, "
          f"time={processing_time:.3f}s, success={success}"
      )
  ```

  ```typescript TypeScript theme={null}
  import express, { Request, Response } from 'express';
  import os from 'os';

  interface HealthStatus {
    status: 'healthy' | 'unhealthy';
    timestamp: string;
    uptime: number;
    memoryUsage: NodeJS.MemoryUsage;
    cpuUsage: number;
  }

  interface WebhookMetrics {
    totalReceived: number;
    successCount: number;
    failureCount: number;
    successRate: string;
    avgProcessingTime: string;
  }

  // Metrics storage (use Redis or proper metrics system in production)
  const metricsStore = {
    webhookReceived: 0,
    webhookSuccess: 0,
    webhookFailure: 0,
    processingTimes: [] as number[]
  };

  const startTime = Date.now();

  // Health check endpoint
  app.get('/webhooks/health', (req: Request, res: Response<HealthStatus>) => {
    const uptime = (Date.now() - startTime) / 1000;
    const memoryUsage = process.memoryUsage();
    
    res.status(200).json({
      status: 'healthy',
      timestamp: new Date().toISOString(),
      uptime,
      memoryUsage,
      cpuUsage: os.loadavg()[0] // 1-minute load average as CPU proxy
    });
  });

  // Metrics endpoint
  app.get('/webhooks/metrics', (req: Request, res: Response<WebhookMetrics>) => {
    const successRate = (
      metricsStore.webhookSuccess / 
      Math.max(metricsStore.webhookReceived, 1)
    ) * 100;
    
    const avgProcessingTime = 
      metricsStore.processingTimes.length > 0
        ? metricsStore.processingTimes.reduce((a, b) => a + b, 0) / 
          metricsStore.processingTimes.length
        : 0;
    
    res.status(200).json({
      totalReceived: metricsStore.webhookReceived,
      successCount: metricsStore.webhookSuccess,
      failureCount: metricsStore.webhookFailure,
      successRate: `${successRate.toFixed(2)}%`,
      avgProcessingTime: `${avgProcessingTime.toFixed(3)}s`
    });
  });

  function logWebhookMetrics(
    eventType: string, 
    processingTime: number, 
    success: boolean
  ): void {
    metricsStore.webhookReceived++;
    metricsStore.processingTimes.push(processingTime);
    
    // Keep only last 1000 processing times
    if (metricsStore.processingTimes.length > 1000) {
      metricsStore.processingTimes = metricsStore.processingTimes.slice(-1000);
    }
    
    if (success) {
      metricsStore.webhookSuccess++;
    } else {
      metricsStore.webhookFailure++;
    }
    
    console.log(
      `Webhook processed: type=${eventType}, ` +
      `time=${processingTime.toFixed(3)}s, success=${success}`
    );
  }
  ```

  ```java Java theme={null}
  import org.springframework.web.bind.annotation.*;
  import org.springframework.http.ResponseEntity;
  import java.lang.management.ManagementFactory;
  import java.lang.management.MemoryMXBean;
  import java.time.Instant;
  import java.util.concurrent.atomic.AtomicInteger;
  import java.util.concurrent.ConcurrentLinkedQueue;
  import java.util.Map;
  import java.util.HashMap;

  @RestController
  public class WebhookMonitoringController {
      
      private final long startTime = System.currentTimeMillis();
      private final AtomicInteger webhookReceived = new AtomicInteger(0);
      private final AtomicInteger webhookSuccess = new AtomicInteger(0);
      private final AtomicInteger webhookFailure = new AtomicInteger(0);
      private final ConcurrentLinkedQueue<Double> processingTimes = new ConcurrentLinkedQueue<>();
      
      @GetMapping("/webhooks/health")
      public ResponseEntity<Map<String, Object>> healthCheck() {
          MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
          Runtime runtime = Runtime.getRuntime();
          
          Map<String, Object> health = new HashMap<>();
          health.put("status", "healthy");
          health.put("timestamp", Instant.now().toString());
          health.put("uptime", (System.currentTimeMillis() - startTime) / 1000.0);
          health.put("memory_used", memoryBean.getHeapMemoryUsage().getUsed());
          health.put("memory_max", memoryBean.getHeapMemoryUsage().getMax());
          health.put("processors", runtime.availableProcessors());
          
          return ResponseEntity.ok(health);
      }
      
      @GetMapping("/webhooks/metrics")
      public ResponseEntity<Map<String, Object>> webhookMetrics() {
          int received = webhookReceived.get();
          int success = webhookSuccess.get();
          int failure = webhookFailure.get();
          
          double successRate = received > 0 ? (success * 100.0) / received : 0;
          
          double avgProcessingTime = processingTimes.isEmpty() ? 0 :
              processingTimes.stream().mapToDouble(Double::doubleValue).average().orElse(0);
          
          Map<String, Object> metrics = new HashMap<>();
          metrics.put("total_received", received);
          metrics.put("success_count", success);
          metrics.put("failure_count", failure);
          metrics.put("success_rate", String.format("%.2f%%", successRate));
          metrics.put("avg_processing_time", String.format("%.3fs", avgProcessingTime));
          
          return ResponseEntity.ok(metrics);
      }
      
      public void logWebhookMetrics(String eventType, double processingTime, boolean success) {
          webhookReceived.incrementAndGet();
          processingTimes.offer(processingTime);
          
          // Keep only last 1000 processing times
          while (processingTimes.size() > 1000) {
              processingTimes.poll();
          }
          
          if (success) {
              webhookSuccess.incrementAndGet();
          } else {
              webhookFailure.incrementAndGet();
          }
          
          System.out.printf(
              "Webhook processed: type=%s, time=%.3fs, success=%b%n",
              eventType, processingTime, success
          );
      }
  }
  ```
</CodeGroup>

## Best Practices

### Performance

* Return HTTP 200 within 10 seconds
* Process webhooks asynchronously when possible
* Implement idempotency using the event ID
* Use database transactions for data consistency

### Security

* Always verify webhook signatures
* Use HTTPS endpoints only
* Whitelist Satsuma IP addresses if needed
* Implement rate limiting on your webhook endpoint

### Reliability

* Handle duplicate events gracefully (idempotency)
* Implement exponential backoff for downstream failures
* Log all webhook events for debugging
* Monitor webhook processing metrics

## Need Help?

* Review webhook delivery logs in your [dashboard](https://dashboard.satsuma.ai/webhooks)
* Test your endpoint with our webhook testing tool
* Check the [Error Handling](/integration/error-handling) guide for common issues
* Contact [support@satsuma.ai](mailto:support@satsuma.ai) for assistance
