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

# Dynamic Delivery

> Create custom fulfillment workflows by handling product delivery programmatically via webhooks

## Overview

Dynamic Delivery allows you to programmatically handle product fulfillment by implementing a webhook endpoint that receives order information and returns delivery content. This is ideal for delivering digital products, license keys, game codes, or integrating with external fulfillment systems.

When a customer purchases a product with dynamic delivery enabled, Komerza immediately sends a POST request to your configured webhook URL with order details, and your endpoint responds with the content to deliver to the customer.

<Note>
  Dynamic Delivery is configured per product variant in your product settings
  under delivery methods.
</Note>

## Use Cases

<CardGroup cols={2}>
  <Card title="Digital Products" icon="file-code">
    Deliver license keys, download links, or access codes in real-time
  </Card>

  {" "}

  <Card title="Third-Party Integration" icon="puzzle-piece">
    Connect with external fulfillment systems or inventory management
  </Card>

  {" "}

  <Card title="Custom Logic" icon="diagram-project">
    Implement complex delivery rules based on customer, product, or order data
  </Card>

  <Card title="Game Codes" icon="gamepad">
    Deliver game keys, activation codes, or in-game items automatically
  </Card>
</CardGroup>

## How It Works

1. **Customer Purchases** - A customer completes checkout for a product with dynamic delivery enabled
2. **Webhook Triggered** - Komerza sends a POST request to your configured webhook URL
3. **Your Response** - Your endpoint processes the request and returns the delivery content
4. **Customer Receives** - The returned content is delivered to the customer as plain text

## Configuration

### Setting Up Dynamic Delivery

1. Navigate to your product in the Komerza Dashboard
2. Select the variant you want to configure
3. Choose **Dynamic Delivery** as the delivery method
4. Enter your webhook endpoint URL
5. Generate and save your webhook secret

<Warning>
  Keep your webhook secret secure. It's used to verify that requests are
  genuinely from Komerza.
</Warning>

## Webhook Request

### Request Headers

```
Content-Type: application/json
Accept: application/json
User-Agent: Komerza/1.0
X-Signature: <HMAC-SHA256-SIGNATURE>
```

### Signature Verification

All webhook requests include an `X-Signature` header containing an HMAC SHA256 signature. You should verify this signature to ensure the request is from Komerza.

**Signature Calculation:**

```
HMAC-SHA256(secret, request_body) -> HEX encoded
```

<CodeGroup>
  ```javascript Node.js theme={null}
  const crypto = require("crypto");

  function verifySignature(secret, body, signature) {
    const calculatedSignature = crypto
      .createHmac("sha256", secret)
      .update(body)
      .digest("hex")
      .toUpperCase();

    return calculatedSignature === signature.toUpperCase();
  }

  // Express middleware example
  app.use("/webhook", express.raw({ type: "application/json" }));

  app.post("/webhook", (req, res) => {
    const signature = req.headers["x-signature"];
    const body = req.body.toString("utf8");

    if (!verifySignature(process.env.WEBHOOK_SECRET, body, signature)) {
      return res.status(401).send("Invalid signature");
    }

    // Process webhook...
  });
  ```

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

  def verify_signature(secret: str, body: str, signature: str) -> bool:
      calculated_signature = hmac.new(
          secret.encode('utf-8'),
          body.encode('utf-8'),
          hashlib.sha256
      ).hexdigest().upper()

      return calculated_signature.upper() == signature.upper()

  # Flask example
  from flask import Flask, request, abort

  @app.route('/webhook', methods=['POST'])
  def webhook():
      signature = request.headers.get('X-Signature')
      body = request.get_data(as_text=True)

      if not verify_signature(os.environ['WEBHOOK_SECRET'], body, signature):
          abort(401, 'Invalid signature')

      # Process webhook...
  ```

  ```php PHP theme={null}
  <?php

  function verifySignature($secret, $body, $signature) {
      $calculatedSignature = strtoupper(
          hash_hmac('sha256', $body, $secret)
      );

      return hash_equals($calculatedSignature, strtoupper($signature));
  }

  // Usage
  $signature = $_SERVER['HTTP_X_SIGNATURE'];
  $body = file_get_contents('php://input');

  if (!verifySignature($_ENV['WEBHOOK_SECRET'], $body, $signature)) {
      http_response_code(401);
      die('Invalid signature');
  }

  // Process webhook...
  ```

  ```go Go theme={null}
  package main

  import (
      "crypto/hmac"
      "crypto/sha256"
      "encoding/hex"
      "io"
      "net/http"
      "strings"
  )

  func verifySignature(secret, body, signature string) bool {
      h := hmac.New(sha256.New, []byte(secret))
      h.Write([]byte(body))
      calculatedSignature := strings.ToUpper(hex.EncodeToString(h.Sum(nil)))

      return calculatedSignature == strings.ToUpper(signature)
  }

  func webhookHandler(w http.ResponseWriter, r *http.Request) {
      signature := r.Header.Get("X-Signature")
      body, _ := io.ReadAll(r.Body)

      if !verifySignature(os.Getenv("WEBHOOK_SECRET"), string(body), signature) {
          http.Error(w, "Invalid signature", http.StatusUnauthorized)
          return
      }

      // Process webhook...
  }
  ```

  ```csharp C# theme={null}
  using System.Security.Cryptography;
  using System.Text;

  public static bool VerifySignature(string secret, string body, string signature)
  {
      var secretBytes = Encoding.UTF8.GetBytes(secret);
      using var hmac = new HMACSHA256(secretBytes);
      var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(body));
      var calculatedSignature = Convert.ToHexString(hash);

      return calculatedSignature.Equals(signature, StringComparison.OrdinalIgnoreCase);
  }

  // ASP.NET Core example
  [HttpPost("webhook")]
  public async Task<IActionResult> Webhook()
  {
      using var reader = new StreamReader(Request.Body);
      var body = await reader.ReadToEndAsync();
      var signature = Request.Headers["X-Signature"].ToString();

      if (!VerifySignature(_configuration["WebhookSecret"], body, signature))
      {
          return Unauthorized("Invalid signature");
      }

      // Process webhook...
  }
  ```
</CodeGroup>

### Payload Structure

The webhook receives a JSON payload with the following structure:

```json theme={null}
{
  "storeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "customerId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "lineItemId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
  "productId": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "variantId": "8f7e6d5c-4b3a-2918-7654-3210fedcba98",
  "quantity": 1,
  "order": {
    "id": "5f3a8b2c-1d4e-5f6a-7b8c-9d0e1f2a3b4c",
    "totalPrice": 29.99,
    "currency": "USD",
    "customer": {
      "email": "customer@example.com",
      "name": "John Doe"
    },
    "items": [
      {
        "id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
        "productName": "Premium License",
        "variantName": "1 Year",
        "quantity": 1,
        "price": 29.99
      }
    ],
    "createdAt": "2025-11-27T10:30:00Z"
  }
}
```

### Payload Fields

<ResponseField name="storeId" type="string (uuid)" required>
  The unique identifier of your store
</ResponseField>

<ResponseField name="customerId" type="string (uuid)" required>
  The unique identifier of the customer who made the purchase
</ResponseField>

<ResponseField name="lineItemId" type="string (uuid)" required>
  The unique identifier of the specific line item in the order
</ResponseField>

<ResponseField name="productId" type="string (uuid)" required>
  The unique identifier of the product being delivered
</ResponseField>

<ResponseField name="variantId" type="string (uuid)" required>
  The unique identifier of the product variant being delivered
</ResponseField>

<ResponseField name="quantity" type="integer" required>
  The quantity of items purchased for this line item
</ResponseField>

<ResponseField name="order" type="object" required>
  Complete order information including customer details, all items, and payment
  information. See the Order object in the [API
  Reference](/api-reference/endpoint/orders/get) for full schema details.
</ResponseField>

## Webhook Response

### Response Format

Your endpoint must respond with **plain text** (`text/plain`) containing the delivery content. This will be displayed to the customer exactly as returned.

```http theme={null}
HTTP/1.1 200 OK
Content-Type: text/plain

LICENSE-KEY-ABC123-XYZ789-PREMIUM
Download: https://example.com/download/abc123
Valid until: 2026-11-27
```

### Response Requirements

<ParamField body="Content-Type" type="string" required>
  Must be `text/plain`
</ParamField>

<ParamField body="Status Code" type="integer" required>
  Must be `200` for successful delivery. Any other status code will be treated
  as a failure and trigger retry logic.
</ParamField>

<ParamField body="Body" type="string" required>
  The actual delivery content to show the customer. Can be multi-line. Maximum
  recommended length is 8,192 characters.
</ParamField>

### Example Implementations

<CodeGroup>
  ```javascript Node.js/Express theme={null}
  const express = require("express");
  const crypto = require("crypto");
  const app = express();

  // Important: Use raw body parser for signature verification
  app.use("/webhook", express.raw({ type: "application/json" }));

  app.post("/webhook", async (req, res) => {
    const signature = req.headers["x-signature"];
    const body = req.body.toString("utf8");

    // Verify signature
    if (!verifySignature(process.env.WEBHOOK_SECRET, body, signature)) {
      return res.status(401).send("Invalid signature");
    }

    // Parse the payload
    const payload = JSON.parse(body);

    // Generate or fetch delivery content based on your logic
    const licenseKey = await generateLicenseKey(
      payload.productId,
      payload.customerId,
    );
    const downloadUrl = await createDownloadLink(payload.variantId);

    // Return plain text delivery content
    res.setHeader("Content-Type", "text/plain");
    res
      .status(200)
      .send(
        `License Key: ${licenseKey}\n` +
          `Download: ${downloadUrl}\n` +
          `Valid for: 1 Year\n` +
          `Customer: ${payload.order.customer.email}`,
      );
  });

  function verifySignature(secret, body, signature) {
    const calculatedSignature = crypto
      .createHmac("sha256", secret)
      .update(body)
      .digest("hex")
      .toUpperCase();
    return calculatedSignature === signature.toUpperCase();
  }

  app.listen(3000);
  ```

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

  app = Flask(__name__)

  @app.route('/webhook', methods=['POST'])
  def webhook():
      signature = request.headers.get('X-Signature')
      body = request.get_data(as_text=True)

      # Verify signature
      if not verify_signature(os.environ['WEBHOOK_SECRET'], body, signature):
          abort(401, 'Invalid signature')

      # Parse payload
      payload = json.loads(body)

      # Generate delivery content
      license_key = generate_license_key(payload['productId'], payload['customerId'])
      download_url = create_download_link(payload['variantId'])

      # Return plain text response
      response = f"""License Key: {license_key}
  Download: {download_url}
  Valid for: 1 Year
  Customer: {payload['order']['customer']['email']}"""

      return response, 200, {'Content-Type': 'text/plain'}

  def verify_signature(secret: str, body: str, signature: str) -> bool:
      calculated = hmac.new(
          secret.encode('utf-8'),
          body.encode('utf-8'),
          hashlib.sha256
      ).hexdigest().upper()
      return calculated == signature.upper()

  if __name__ == '__main__':
      app.run(port=3000)
  ```

  ```php PHP theme={null}
  <?php

  function verifySignature($secret, $body, $signature) {
      $calculated = strtoupper(hash_hmac('sha256', $body, $secret));
      return hash_equals($calculated, strtoupper($signature));
  }

  // Get raw POST body
  $body = file_get_contents('php://input');
  $signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';

  // Verify signature
  if (!verifySignature($_ENV['WEBHOOK_SECRET'], $body, $signature)) {
      http_response_code(401);
      die('Invalid signature');
  }

  // Parse payload
  $payload = json_decode($body, true);

  // Generate delivery content
  $licenseKey = generateLicenseKey($payload['productId'], $payload['customerId']);
  $downloadUrl = createDownloadLink($payload['variantId']);

  // Return plain text response
  header('Content-Type: text/plain');
  http_response_code(200);

  echo "License Key: {$licenseKey}\n";
  echo "Download: {$downloadUrl}\n";
  echo "Valid for: 1 Year\n";
  echo "Customer: {$payload['order']['customer']['email']}";
  ```

  ```go Go theme={null}
  package main

  import (
      "crypto/hmac"
      "crypto/sha256"
      "encoding/hex"
      "encoding/json"
      "fmt"
      "io"
      "net/http"
      "os"
      "strings"
  )

  type Payload struct {
      StoreId    string `json:"storeId"`
      CustomerId string `json:"customerId"`
      ProductId  string `json:"productId"`
      VariantId  string `json:"variantId"`
      Quantity   int    `json:"quantity"`
      Order      struct {
          Customer struct {
              Email string `json:"email"`
          } `json:"customer"`
      } `json:"order"`
  }

  func webhookHandler(w http.ResponseWriter, r *http.Request) {
      signature := r.Header.Get("X-Signature")
      body, _ := io.ReadAll(r.Body)

      // Verify signature
      if !verifySignature(os.Getenv("WEBHOOK_SECRET"), string(body), signature) {
          http.Error(w, "Invalid signature", http.StatusUnauthorized)
          return
      }

      // Parse payload
      var payload Payload
      json.Unmarshal(body, &payload)

      // Generate delivery content
      licenseKey := generateLicenseKey(payload.ProductId, payload.CustomerId)
      downloadUrl := createDownloadLink(payload.VariantId)

      // Return plain text response
      w.Header().Set("Content-Type", "text/plain")
      w.WriteHeader(http.StatusOK)
      fmt.Fprintf(w, "License Key: %s\n", licenseKey)
      fmt.Fprintf(w, "Download: %s\n", downloadUrl)
      fmt.Fprintf(w, "Valid for: 1 Year\n")
      fmt.Fprintf(w, "Customer: %s", payload.Order.Customer.Email)
  }

  func verifySignature(secret, body, signature string) bool {
      h := hmac.New(sha256.New, []byte(secret))
      h.Write([]byte(body))
      calculated := strings.ToUpper(hex.EncodeToString(h.Sum(nil)))
      return calculated == strings.ToUpper(signature)
  }

  func main() {
      http.HandleFunc("/webhook", webhookHandler)
      http.ListenAndServe(":3000", nil)
  }
  ```
</CodeGroup>

## Timeout and Retry Logic

### Timeout

Your webhook endpoint has **20 seconds** to respond. If your endpoint doesn't respond within this timeframe, the request will be considered failed and will be retried.

<Tip>
  For operations that take longer than 20 seconds, consider using an
  asynchronous pattern: 1. Immediately return a success response 2. Process the
  delivery in the background 3. Use the Komerza API to update the delivery
  status when ready
</Tip>

### Retry Policy

Komerza implements an automatic retry mechanism with exponential backoff for failed webhook deliveries:

* **Retry Attempts:** 3 automatic retries
* **Backoff Strategy:** Exponential (2^retry seconds)
  * 1st retry: after 2 seconds
  * 2nd retry: after 4 seconds
  * 3rd retry: after 8 seconds

### Circuit Breaker

To protect your endpoint from being overwhelmed, Komerza implements a circuit breaker:

* **Threshold:** 5 consecutive failures
* **Break Duration:** 30 seconds
* **Behavior:** After 5 consecutive failures, requests are paused for 30 seconds before attempting again

<Warning>
  Ensure your endpoint is highly available. Repeated failures may trigger the
  circuit breaker, temporarily preventing delivery webhooks from being sent.
</Warning>

## Error Handling

### Common Error Scenarios

<AccordionGroup>
  <Accordion title="Invalid Signature (401)">
    **Cause:** The signature verification failed

    **Solution:**

    * Verify you're using the correct webhook secret
    * Ensure you're reading the raw request body (not parsed JSON)
    * Check your HMAC implementation matches the algorithm (SHA256, HEX encoded)
  </Accordion>

  {" "}

  <Accordion title="Timeout (408)">
    **Cause:** Your endpoint didn't respond within 20 seconds **Solution:** -
    Optimize your delivery generation logic - Use caching for frequently accessed
    data - Consider asynchronous processing for complex operations
  </Accordion>

  {" "}

  <Accordion title="Server Error (500)">
    **Cause:** Your endpoint returned an error response **Solution:** - Check your
    application logs for errors - Implement proper error handling and logging -
    Validate the payload structure before processing
  </Accordion>

  <Accordion title="Circuit Breaker Triggered">
    **Cause:** Too many consecutive failures (5+)

    **Solution:**

    * Check your server health and availability
    * Review error logs to identify the root cause
    * Implement health checks and monitoring
    * The circuit breaker will automatically reset after 30 seconds
  </Accordion>
</AccordionGroup>

### Logging and Debugging

Komerza logs all webhook requests and responses for debugging purposes:

* **Request Body:** Stored (truncated to 8,192 characters)
* **Response Body:** Stored (truncated to 900 characters)
* **Response Code:** Recorded for each attempt
* **Signature:** Stored for verification

You can view these logs in your Komerza Dashboard under Webhook Execution Logs.

## Testing

### Test Webhook Locally

Use a tool like [ngrok](https://ngrok.com/) or [cloudflared](https://github.com/cloudflare/cloudflared) to expose your local development server:

```bash theme={null}
# Start your local server
npm start  # or your framework's dev command

# In another terminal, start ngrok
ngrok http 3000

# Use the ngrok URL (e.g., https://abc123.ngrok.io/webhook) in your Komerza product configuration
```

### Manual Testing

You can manually test your webhook endpoint by simulating a request:

```bash theme={null}
# Generate signature
SECRET="your-webhook-secret"
PAYLOAD='{"storeId":"...","customerId":"...","order":{...}}'
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print toupper($2)}')

# Send test request
curl -X POST https://your-endpoint.com/webhook \
  -H "Content-Type: application/json" \
  -H "X-Signature: $SIGNATURE" \
  -d "$PAYLOAD"
```

### Production Testing

<Warning>
  Test in your development/staging environment first before enabling dynamic
  delivery in production.
</Warning>

1. Create a test product with dynamic delivery enabled
2. Point it to your staging webhook endpoint
3. Make a test purchase (use test mode if available)
4. Verify the delivery content is generated correctly
5. Check webhook execution logs in your dashboard

## Support

<CardGroup cols={2}>
  <Card title="Help Center" icon="messages-question" href="https://docs.komerza.com">
    Browse our knowledge base and guides
  </Card>

  {" "}

  <Card title="Discord Community" icon="discord" href="https://discord.gg/komerza">
    Get help from the community and Komerza team
  </Card>

  {" "}

  <Card title="System Status" icon="wave-pulse" href="https://status.komerza.com">
    Check webhook and API system status
  </Card>

  <Card title="Telegram" icon="telegram" href="https://t.me/komerzaecom">
    Join our Telegram community for support
  </Card>
</CardGroup>
