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

# Create Webhook

> Register a new webhook endpoint to receive QR Ph transaction notifications

## Overview

The Create Webhook endpoint allows you to register a new webhook URL that will receive real-time notifications when QR Ph transactions complete. You can specify which transaction events (success or declined) to receive and enable or disable the webhook immediately.

<Warning>
  **Webhooks are required for QR Ph integration.** Without a registered webhook, you cannot receive transaction status updates, which poses significant security risks.
</Warning>

## Endpoint

```
POST https://webhooks.sbx.moduluslabs.io/v1/webhooks
```

## Authentication

This endpoint requires [HTTP Basic Authentication](/docs/qr/authentication) using your Secret Key.

```
Authorization: Basic {base64(secret_key:)}
```

## Request

### Headers

| Header            | Value                         | Required | Description                                        |
| ----------------- | ----------------------------- | -------- | -------------------------------------------------- |
| `Authorization`   | `Basic {base64(secret_key:)}` | Yes      | HTTP Basic Auth with your secret key               |
| `Content-Type`    | `application/json`            | Yes      | Request body format                                |
| `activation-code` | string                        | No       | Merchant activation code for multi-merchant setups |

<ParamField header="activation-code" type="string">
  Optional activation code to identify which merchant this webhook belongs to. Used for multi-merchant environments where a single platform manages webhooks for multiple merchants.

  **Example:** `"MERCHANT_ABC_123"`

  When you create a webhook with an activation code, Modulus Labs includes the same `activation-code` in the header of webhook notifications sent to your endpoint.
</ParamField>

## Prerequisites

Before creating a webhook, ensure you have:

* Your **Secret Key** for HTTP Basic Authentication

<Note>
  See the [Encryption Guide](/docs/qr/encryption) to understand how JWE tokens work — you'll need this to **decrypt incoming webhook notifications** sent to your endpoint.
</Note>

## Encrypted Payload Structure

The request body must contain a `request` object with a JWE-encrypted `Token` that includes your webhook configuration:

```json theme={null}
{
  "request": {
    "Token": "eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0..."
  }
}
```

### Body Parameters (Encrypted Payload)

<Note>
  The following parameters describe the payload that must be JWE-encrypted into the `Token` field. See the [Encryption Guide](/docs/qr/encryption) for details on creating JWE tokens.
</Note>

| Field           | Type   | Required | Description                                                    |
| --------------- | ------ | -------- | -------------------------------------------------------------- |
| `callbackUrl`   | string | Yes      | HTTPS URL where Modulus Labs sends webhook notifications       |
| `webhookAction` | string | Yes      | Transaction events to receive: `QRPH_SUCCESS`, `QRPH_DECLINED` |

<AccordionGroup>
  <Accordion title="callbackUrl" icon="link">
    The HTTPS URL where Modulus Labs sends webhook notifications. Must be publicly accessible and use HTTPS.

    * **Format:** Valid HTTPS URL
    * **Example:** `"https://api.yourcompany.com/webhooks/modulus"`

    **Requirements:**

    * Must use HTTPS protocol (HTTP not supported in production)
    * Must be publicly accessible from the internet
    * Must respond within 10 seconds
    * Should return `200 OK` to acknowledge receipt
  </Accordion>

  <Accordion title="webhookAction" icon="bell">
    Transaction events you want to receive notifications for. Include one or both values:

    * `QRPH_SUCCESS` - Receive notifications when payments succeed
    * `QRPH_DECLINED` - Receive notifications when payments are declined

    **Example:** `"QRPH_SUCCESS"`

    <Tip>
      We recommend including both actions. Handling declined payments allows you to provide better customer support and offer alternative payment methods.
    </Tip>
  </Accordion>
</AccordionGroup>

### Example Payload (Before Encryption)

```json theme={null}
{
  "webhookAction": "QRPH_SUCCESS",
  "callbackUrl": "https://api.yourcompany.com/webhooks/success"
}
```

## Response

### Success Response

**Status Code:** `201 Created`

Returns the created webhook object with a unique `id` that you can use to update or delete the webhook later.

<ResponseField name="id" type="integer">
  Unique identifier for the webhook. Save this value - you'll need it to update or delete the webhook.

  **Example:** `a78efd32-de3b-4854-b599-11ae9f98f97e`
</ResponseField>

<ResponseField name="webhookAction" type="string">
  String of webhook actions this endpoint will receive.

  **Example:** `"QRPH_SUCCESS" 
                    "QRPH_DECLINED"`
</ResponseField>

<ResponseField name="webhookStatus" type="string">
  Current webhook status: `ENABLED` or `DISABLED`

  **Example:** `"ENABLED"`
</ResponseField>

<ResponseField name="callbackUrl" type="string">
  The webhook URL you registered.

  **Example:** `"https://api.yourcompany.com/webhooks/modulus"`
</ResponseField>

### Response Example

```json theme={null}
{
  "id": "a78efd32-de3b-4854-b599-11ae9f98f97e",
  "webhookAction": "QRPH_SUCCESS",
  "webhookStatus": "ENABLED",
  "callbackUrl": "https://api.yourcompany.com/webhooks/success"
}
```

<ResponseExample>
  ```json 201 Created theme={null}
  {
    "id": "a78efd32-de3b-4854-b599-11ae9f98f97e",
    "webhookAction": "QRPH_SUCCESS",
    "webhookStatus": "ENABLED",
    "callbackUrl": "https://api.yourcompany.com/webhooks/success"
  }
  ```

  ```json 400 Bad Request theme={null}
  {
    "code": "20000002",
    "error": "Invalid callbackUrl: must be a valid HTTPS URL",
    "referenceNumber": "097bf60a-bdab-40bf-b615-3c0eae693b86"
  }
  ```

  ```json 401 Unauthorized theme={null}
  {
    "code": "10000013",
    "error": "Invalid API Key",
    "referenceNumber": "097bf60a-bdab-40bf-b615-3c0eae693b86"
  }
  ```

  ```json 409 Conflict theme={null}
  {
    "code": "20000003",
    "error": "Webhook URL already exists",
    "referenceNumber": "097bf60a-bdab-40bf-b615-3c0eae693b86"
  }
  ```

  ```json 500 Internal Server Error theme={null}
  {
    "code": "10000001",
    "error": "An unexpected error occurred. Please try again later.",
    "referenceNumber": "097bf60a-bdab-40bf-b615-3c0eae693b86"
  }
  ```
</ResponseExample>

<Note>
  **Save the webhook ID!** You'll need the `id` value to update or delete this webhook using the [Update](/api-reference/webhooks/update) or [Delete](/api-reference/webhooks/delete) endpoints.
</Note>

### Error Responses

<AccordionGroup>
  <Accordion title="400 Bad Request" icon="circle-exclamation">
    **Status Code:** `400`

    **Causes:**

    * Invalid webhook URL format
    * Missing required fields
    * Invalid action values
    * Invalid status value

    **Response Example:**

    ```json theme={null}
    {
      "code": "20000002",
      "error": "Invalid callbackUrl: must be a valid HTTPS URL",
      "referenceNumber": "abc123-def456-ghi789"
    }
    ```

    **Solutions:**

    * Ensure callbackUrl uses HTTPS protocol
    * Verify all required fields are present
    * Check that actions contain valid values (`QRPH_SUCCESS`, `QRPH_DECLINED`)
    * Ensure status is either `ENABLED` or `DISABLED`
  </Accordion>

  <Accordion title="401 Unauthorized" icon="key">
    **Status Code:** `401`

    **Cause:** Invalid or missing authentication credentials

    **Solution:**

    * Verify your secret key is correct
    * Ensure Authorization header format: `Basic {base64(secret_key:)}`
    * Check you're using the right environment (sandbox vs production)
  </Accordion>

  <Accordion title="409 Conflict" icon="clone">
    **Status Code:** `409`

    **Cause:** Webhook URL already registered for this merchant

    **Response Example:**

    ```json theme={null}
    {
      "code": "20000003",
      "error": "Webhook URL already exists",
      "referenceNumber": "abc123-def456-ghi789"
    }
    ```

    **Solution:**

    * Use a different webhook URL, or
    * Update the existing webhook using [Update Webhook API](/api-reference/webhooks/update), or
    * Delete the existing webhook and create a new one
  </Accordion>

  <Accordion title="500 Internal Server Error" icon="server">
    **Status Code:** `500`

    **Cause:** Unexpected server error

    **Solution:**

    * Retry the request
    * If the issue persists, contact Modulus Labs support with the reference number
  </Accordion>
</AccordionGroup>

<RequestExample>
  ```bash cURL theme={null}
  # Note: You need to encrypt the payload first using a script/tool
  # See the Encryption Guide for JWE token creation

  # 1. Create encrypted JWE token (using external tool/script)
  ENCRYPTED_TOKEN="eyJhbGciOiJBMjU2S1ciL..."

  # 2. Make API request
  curl -X POST https://webhooks.sbx.moduluslabs.io/v1/webhooks \
    -u sk_your_secret_key: \
    -H "Content-Type: application/json" \
    -d "{\"request\": {\"Token\": \"$ENCRYPTED_TOKEN\"}}"

  # 3. Response contains the created webhook object
  ```

  ```javascript Node.js theme={null}
  const axios = require('axios');
  const jose = require('jose');

  const SECRET_KEY = process.env.MODULUS_SECRET_KEY;
  const ENCRYPTION_KEY = process.env.MODULUS_ENCRYPTION_KEY;

  async function createWebhook() {
    // 1. Prepare the payload
    const payload = {
      webhookAction: 'QRPH_SUCCESS',
      callbackUrl: 'https://api.yourcompany.com/webhooks/success'
    };

    // 2. Encrypt payload into JWE token
    const key = Buffer.from(ENCRYPTION_KEY, 'base64');
    const jweToken = await new jose.CompactEncrypt(
      new TextEncoder().encode(JSON.stringify(payload))
    )
      .setProtectedHeader({ alg: 'A256KW', enc: 'A256CBC-HS512' })
      .encrypt(key);

    // 3. Make API request
    const response = await axios.post(
      'https://webhooks.sbx.moduluslabs.io/v1/webhooks',
      { request: { Token: jweToken } },
      {
        auth: { username: SECRET_KEY, password: '' },
        headers: { 'Content-Type': 'application/json' }
      }
    );

    const result = response.data;
    console.log('Webhook created successfully');
    console.log('Webhook ID:', result.id);
    console.log('Webhook URL:', result.callbackUrl);
    console.log('Action:', result.webhookAction);
    console.log('Status:', result.webhookStatus);

    return result;
  }

  createWebhook()
    .catch(error => {
      console.error('Error:', error.response?.data || error.message);
    });
  ```

  ```python Python theme={null}
  import os
  import json
  import requests
  from jose import jwe

  SECRET_KEY = os.getenv('MODULUS_SECRET_KEY')
  ENCRYPTION_KEY = os.getenv('MODULUS_ENCRYPTION_KEY')

  def create_webhook():
      # 1. Prepare the payload
      payload = {
          'webhookAction': 'QRPH_SUCCESS',
          'callbackUrl': 'https://api.yourcompany.com/webhooks/success'
      }

      # 2. Encrypt payload into JWE token
      jwe_token = jwe.encrypt(
          json.dumps(payload),
          ENCRYPTION_KEY,
          algorithm='A256KW',
          encryption='A256CBC-HS512'
      )

      # 3. Make API request
      response = requests.post(
          'https://webhooks.sbx.moduluslabs.io/v1/webhooks',
          json={'request': {'Token': jwe_token}},
          auth=(SECRET_KEY, ''),
          headers={'Content-Type': 'application/json'}
      )

      response.raise_for_status()

      result = response.json()
      print('Webhook created successfully')
      print(f"Webhook ID: {result['id']}")
      print(f"Webhook URL: {result['callbackUrl']}")
      print(f"Status: {result['webhookStatus']}")

      return result

  if __name__ == '__main__':
      try:
          create_webhook()
      except Exception as e:
          print(f'Error: {str(e)}')
  ```

  ```php PHP theme={null}
  <?php
  require 'vendor/autoload.php';

  use Jose\Component\Core\AlgorithmManager;
  use Jose\Component\Core\JWK;
  use Jose\Component\Encryption\Algorithm\KeyEncryption\A256KW;
  use Jose\Component\Encryption\Algorithm\ContentEncryption\A256CBCHS512;
  use Jose\Component\Encryption\JWEBuilder;
  use Jose\Component\Encryption\Serializer\CompactSerializer;

  $secretKey = getenv('MODULUS_SECRET_KEY');
  $encryptionKey = getenv('MODULUS_ENCRYPTION_KEY');

  function createWebhook() {
      global $secretKey, $encryptionKey;

      // 1. Prepare the payload
      $payload = [
          'webhookAction' => 'QRPH_SUCCESS',
          'callbackUrl' => 'https://api.yourcompany.com/webhooks/success'
      ];

      // 2. Encrypt payload (simplified - use proper JWE library)
      // See encryption documentation for full implementation
      $jweToken = encryptToJWE(json_encode($payload), $encryptionKey);

      // 3. Make API request
      $ch = curl_init('https://webhooks.sbx.moduluslabs.io/v1/webhooks');
      curl_setopt($ch, CURLOPT_POST, 1);
      curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['request' => ['Token' => $jweToken]]));
      curl_setopt($ch, CURLOPT_USERPWD, $secretKey . ':');
      curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

      $response = curl_exec($ch);
      $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
      curl_close($ch);

      if ($httpCode === 201) {
          $result = json_decode($response, true);
          echo "Webhook created successfully\n";
          echo "Webhook ID: {$result['id']}\n";
          echo "Webhook URL: {$result['callbackUrl']}\n";
          echo "Status: {$result['webhookStatus']}\n";
          return $result;
      } else {
          echo "Error: HTTP $httpCode\n";
          echo "Details: $response\n";
          return null;
      }
  }

  try {
      createWebhook();
  } catch (Exception $e) {
      echo "Error: " . $e->getMessage() . "\n";
  }
  ```

  ```java Java theme={null}
  import com.nimbusds.jose.*;
  import com.nimbusds.jose.crypto.*;
  import com.nimbusds.jose.jwk.OctetSequenceKey;
  import com.fasterxml.jackson.databind.ObjectMapper;
  import java.net.URI;
  import java.net.http.*;
  import java.util.*;

  public class CreateWebhookExample {
      private static final ObjectMapper mapper = new ObjectMapper();

      public static void main(String[] args) throws Exception {
          createWebhook();
      }

      static void createWebhook() throws Exception {
          String secretKey = System.getenv("MODULUS_SECRET_KEY");
          String encryptionKey = System.getenv("MODULUS_ENCRYPTION_KEY");

          // 1. Prepare the payload
          Map<String, Object> payload = new LinkedHashMap<>();
          payload.put("webhookAction", "QRPH_SUCCESS");
          payload.put("callbackUrl", "https://api.yourcompany.com/webhooks/success");

          String payloadJson = mapper.writeValueAsString(payload);

          // 2. Encrypt payload into JWE token
          byte[] keyBytes = Base64.getUrlDecoder().decode(encryptionKey);
          OctetSequenceKey jwk = new OctetSequenceKey.Builder(keyBytes).build();

          JWEHeader header = new JWEHeader(JWEAlgorithm.A256KW, EncryptionMethod.A256CBC_HS512);
          JWEObject jwe = new JWEObject(header, new Payload(payloadJson));
          jwe.encrypt(new AESEncrypter(jwk));
          String jweToken = jwe.serialize();

          // 3. Make API request with Basic Auth
          HttpClient client = HttpClient.newHttpClient();
          String auth = secretKey + ":";
          String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes());

          String requestBody = mapper.writeValueAsString(Map.of("request", Map.of("Token", jweToken)));

          HttpRequest request = HttpRequest.newBuilder()
              .uri(URI.create("https://webhooks.sbx.moduluslabs.io/v1/webhooks"))
              .header("Authorization", "Basic " + encodedAuth)
              .header("Content-Type", "application/json")
              .POST(HttpRequest.BodyPublishers.ofString(requestBody))
              .build();

          HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

          if (response.statusCode() == 201) {
              Map<String, Object> result = mapper.readValue(response.body(), Map.class);
              System.out.println("Webhook created successfully");
              System.out.println("Webhook ID: " + result.get("id"));
              System.out.println("Webhook URL: " + result.get("callbackUrl"));
              System.out.println("Status: " + result.get("webhookStatus"));
          } else {
              System.out.println("Error: HTTP " + response.statusCode());
              System.out.println("Details: " + response.body());
          }
      }
  }
  ```

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

  import (
  	"bytes"
  	"encoding/base64"
  	"encoding/json"
  	"fmt"
  	"io"
  	"net/http"
  	"os"

  	"github.com/lestrrat-go/jwx/v2/jwa"
  	"github.com/lestrrat-go/jwx/v2/jwe"
  )

  type WebhookPayload struct {
  	WebhookAction string `json:"webhookAction"`
  	CallbackUrl   string `json:"callbackUrl"`
  }

  type TokenWrapper struct {
  	Token string `json:"Token"`
  }

  type TokenRequest struct {
  	Request TokenWrapper `json:"request"`
  }

  type WebhookResponse struct {
  	ID            string `json:"id"`
  	WebhookAction string `json:"webhookAction"`
  	WebhookStatus string `json:"webhookStatus"`
  	CallbackUrl   string `json:"callbackUrl"`
  }

  func main() {
  	if err := createWebhook(); err != nil {
  		fmt.Printf("Error: %v\n", err)
  	}
  }

  func createWebhook() error {
  	secretKey := os.Getenv("MODULUS_SECRET_KEY")
  	encryptionKey := os.Getenv("MODULUS_ENCRYPTION_KEY")

  	// 1. Prepare the payload
  	payload := WebhookPayload{
  		WebhookAction: "QRPH_SUCCESS",
  		CallbackUrl:   "https://api.yourcompany.com/webhooks/success",
  	}

  	payloadJSON, err := json.Marshal(payload)
  	if err != nil {
  		return fmt.Errorf("failed to marshal payload: %w", err)
  	}

  	// 2. Encrypt payload into JWE token
  	keyBytes, err := base64.RawURLEncoding.DecodeString(encryptionKey)
  	if err != nil {
  		return fmt.Errorf("failed to decode encryption key: %w", err)
  	}

  	encrypted, err := jwe.Encrypt(payloadJSON,
  		jwe.WithKey(jwa.A256KW, keyBytes),
  		jwe.WithContentEncryption(jwa.A256CBC_HS512),
  	)
  	if err != nil {
  		return fmt.Errorf("failed to encrypt payload: %w", err)
  	}
  	jweToken := string(encrypted)

  	// 3. Make API request with Basic Auth
  	reqBody := TokenRequest{Request: TokenWrapper{Token: jweToken}}
  	jsonData, _ := json.Marshal(reqBody)

  	req, err := http.NewRequest("POST", "https://webhooks.sbx.moduluslabs.io/v1/webhooks", bytes.NewBuffer(jsonData))
  	if err != nil {
  		return fmt.Errorf("failed to create request: %w", err)
  	}

  	req.SetBasicAuth(secretKey, "")
  	req.Header.Set("Content-Type", "application/json")

  	client := &http.Client{}
  	resp, err := client.Do(req)
  	if err != nil {
  		return fmt.Errorf("request failed: %w", err)
  	}
  	defer resp.Body.Close()

  	body, _ := io.ReadAll(resp.Body)

  	if resp.StatusCode == 201 {
  		var result WebhookResponse
  		json.Unmarshal(body, &result)
  		fmt.Println("Webhook created successfully")
  		fmt.Printf("Webhook ID: %s\n", result.ID)
  		fmt.Printf("Webhook URL: %s\n", result.CallbackUrl)
  		fmt.Printf("Status: %s\n", result.WebhookStatus)
  		return nil
  	}

  	return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
  }
  ```

  ```csharp C# / .NET theme={null}
  using System.Net.Http.Headers;
  using System.Text;
  using System.Text.Json;
  using Jose;

  namespace ModulusWebhooks;

  class Program
  {
      static async Task Main(string[] args)
      {
          await CreateWebhook();
      }

      static byte[] Base64UrlDecode(string base64Url)
      {
          var base64 = base64Url
              .Replace('-', '+')
              .Replace('_', '/');

          switch (base64.Length % 4)
          {
              case 2: base64 += "=="; break;
              case 3: base64 += "="; break;
          }

          return Convert.FromBase64String(base64);
      }

      static async Task CreateWebhook()
      {
          var secretKey = Environment.GetEnvironmentVariable("MODULUS_SECRET_KEY");
          var encryptionKey = Environment.GetEnvironmentVariable("MODULUS_ENCRYPTION_KEY");

          if (string.IsNullOrEmpty(secretKey) || string.IsNullOrEmpty(encryptionKey))
          {
              Console.WriteLine("Error: MODULUS_SECRET_KEY and MODULUS_ENCRYPTION_KEY must be set");
              return;
          }

          try
          {
              // 1. Prepare the payload
              var payload = new
              {
                  webhookAction = "QRPH_SUCCESS",
                  callbackUrl = "https://api.yourcompany.com/webhooks/success"
              };

              // 2. Encrypt payload into JWE token
              var key = Base64UrlDecode(encryptionKey);
              var payloadJson = JsonSerializer.Serialize(payload);
              var jweToken = JWT.Encode(
                  payloadJson,
                  key,
                  JweAlgorithm.A256KW,
                  JweEncryption.A256CBC_HS512
              );

              // 3. Make API request with Basic Auth
              using var client = new HttpClient();
              var authBytes = Encoding.ASCII.GetBytes($"{secretKey}:");
              client.DefaultRequestHeaders.Authorization =
                  new AuthenticationHeaderValue("Basic", Convert.ToBase64String(authBytes));

              var requestBody = new { request = new { Token = jweToken } };
              var content = new StringContent(
                  JsonSerializer.Serialize(requestBody),
                  Encoding.UTF8,
                  "application/json"
              );

              var response = await client.PostAsync(
                  "https://webhooks.sbx.moduluslabs.io/v1/webhooks",
                  content
              );

              var responseBody = await response.Content.ReadAsStringAsync();

              if (response.IsSuccessStatusCode)
              {
                  var result = JsonSerializer.Deserialize<WebhookResponse>(
                      responseBody,
                      new JsonSerializerOptions { PropertyNameCaseInsensitive = true }
                  );

                  Console.WriteLine("Webhook created successfully");
                  Console.WriteLine($"Webhook ID: {result?.Id}");
                  Console.WriteLine($"Webhook URL: {result?.CallbackUrl}");
                  Console.WriteLine($"Action: {result?.WebhookAction}");
                  Console.WriteLine($"Status: {result?.WebhookStatus}");
              }
              else
              {
                  Console.WriteLine($"Error: HTTP {(int)response.StatusCode}");
                  Console.WriteLine($"Details: {responseBody}");
              }
          }
          catch (Exception e)
          {
              Console.WriteLine($"Error: {e.Message}");
          }
      }
  }

  public class WebhookResponse
  {
      public string? Id { get; set; }
      public string? CallbackUrl { get; set; }
      public string? WebhookAction { get; set; }
      public string? WebhookStatus { get; set; }
  }
  ```
</RequestExample>

## Multi-Merchant Setup

If you manage multiple merchants, use the `activation-code` header to associate the webhook with a specific merchant:

```bash theme={null}
# Encrypt your payload into a JWE token first
ENCRYPTED_TOKEN="eyJhbGciOiJBMjU2S1ciL..."

curl -X POST https://webhooks.sbx.moduluslabs.io/v1/webhooks \
  -u sk_your_secret_key: \
  -H "Content-Type: application/json" \
  -H "activation-code: MERCHANT_ABC_123" \
  -d "{\"request\": {\"Token\": \"$ENCRYPTED_TOKEN\"}}"
```

When Modulus Labs sends webhook notifications for this merchant, the `activation-code` header will be included in the request:

```javascript theme={null}
app.post('/webhooks/modulus', (req, res) => {
  const activationCode = req.headers['activation-code'];
  // "MERCHANT_ABC_123"

  // Route to appropriate merchant handler
  const merchant = getMerchantByActivationCode(activationCode);
  processWebhook(merchant, req.body);
});
```

## Use Cases

<AccordionGroup>
  <Accordion title="E-commerce Integration" icon="shopping-cart">
    Register a webhook to receive payment notifications for online orders:

    ```javascript theme={null}
    const webhook = await createWebhook({
      callbackUrl: 'https://shop.example.com/api/payments/webhook',
      webhookAction: 'QRPH_SUCCESS',
      status: 'ENABLED'
    });

    // Webhook will notify you instantly when customers complete QR Ph payments
    ```
  </Accordion>

  <Accordion title="POS System" icon="cash-register">
    Register a webhook for in-store point-of-sale transactions:

    ```javascript theme={null}
    const webhook = await createWebhook({
      callbackUrl: 'https://pos.example.com/webhooks/payment',
      webhookAction: ['QRPH_SUCCESS'],  // Only success events for POS
      status: 'ENABLED'
    });
    ```
  </Accordion>

  <Accordion title="Development and Testing" icon="flask">
    Create a disabled webhook during development, enable it when ready:

    ```javascript theme={null}
    // Create disabled webhook for testing
    const webhook = await createWebhook({
      callbackUrl: 'https://dev.example.com/webhooks/test',
      webhookAction: 'QRPH_SUCCESS',
      status: 'DISABLED'  // Won't receive notifications yet
    });

    // Enable later using Update Webhook API
    await updateWebhook(webhook.id, { status: 'ENABLED' });
    ```
  </Accordion>
</AccordionGroup>

## Best Practices

<CardGroup cols={2}>
  <Card title="Use HTTPS Only" icon="lock">
    Always use HTTPS URLs for webhooks. HTTP is insecure and not supported in production.
  </Card>

  <Card title="Save Webhook ID" icon="bookmark">
    Store the returned `id` in your database. You'll need it to update or delete the webhook.
  </Card>

  <Card title="Test Before Enabling" icon="flask">
    Create webhooks with `DISABLED` status during development. Test with the Simulate API before enabling.
  </Card>

  <Card title="Include Both Actions" icon="list-check">
    Register for both `QRPH_SUCCESS` and `QRPH_DECLINED` to handle all transaction outcomes.
  </Card>

  <Card title="Unique URLs per Environment" icon="code-branch">
    Use different webhook URLs for sandbox and production to avoid mixing test and live data.
  </Card>

  <Card title="Verify Connectivity First" icon="signal">
    Test your webhook endpoint is accessible before registering it with the API.
  </Card>
</CardGroup>

## Validation Checklist

Before creating a webhook, verify:

<Steps>
  <Step title="Webhook Endpoint is Ready">
    * [ ] Endpoint accepts POST requests
    * [ ] Endpoint responds within 10 seconds
    * [ ] Endpoint can decrypt JWE payloads
    * [ ] Endpoint implements idempotency
  </Step>

  <Step title="URL is Accessible">
    * [ ] URL uses HTTPS protocol
    * [ ] URL is publicly accessible from the internet
    * [ ] Firewall allows incoming requests
    * [ ] SSL certificate is valid
  </Step>

  <Step title="Authentication is Configured">
    * [ ] Secret key is correct
    * [ ] Authorization header format is valid
    * [ ] Using correct environment (sandbox vs production)
  </Step>

  <Step title="Configuration is Correct">
    * [ ] Webhook URL is a valid HTTPS URL
    * [ ] Actions include at least one valid value
    * [ ] Status is either ENABLED or DISABLED
  </Step>
</Steps>

## What Happens Next?

After creating a webhook:

<Steps>
  <Step title="Webhook is Registered">
    Modulus Labs saves your webhook configuration and begins monitoring for transaction events.
  </Step>

  <Step title="Transactions Trigger Notifications">
    When QR Ph transactions complete, Modulus Labs sends encrypted webhook notifications to your URL.
  </Step>

  <Step title="Automatic Retries">
    If your endpoint doesn't respond with `200 OK`, Modulus Labs automatically retries up to 3 more times at 15-minute intervals.
  </Step>

  <Step title="You Process Webhooks">
    Your endpoint decrypts payloads, updates order status, and fulfills transactions in real-time.
  </Step>
</Steps>

## Next Steps

<CardGroup cols={2}>
  <Card title="Test Your Webhook" icon="flask" href="/api-reference/webhooks/simulate">
    Use the Simulate API to send test webhooks
  </Card>

  <Card title="View Webhooks" icon="list" href="/api-reference/webhooks/list">
    Get all registered webhooks for your account
  </Card>

  <Card title="Update Webhook" icon="pen" href="/api-reference/webhooks/update">
    Modify webhook URL, webhook Action, or status
  </Card>

  <Card title="Receiving Webhooks" icon="webhook" href="/docs/webhooks/receiving">
    Learn how to process webhook notifications
  </Card>
</CardGroup>


## OpenAPI

````yaml POST /v1/webhooks
openapi: 3.1.0
info:
  title: Modulus Labs Webhooks API
  description: >-
    API for managing webhook endpoints to receive QR Ph transaction
    notifications
  version: 1.0.0
servers:
  - url: https://webhooks.sbx.moduluslabs.io
    description: Sandbox
security: []
paths:
  /v1/webhooks:
    post:
      tags:
        - Webhooks
      summary: Create Webhook
      description: >-
        Register a new webhook endpoint to receive QR Ph transaction
        notifications.
      operationId: createWebhook
      parameters:
        - name: activation-code
          in: header
          required: false
          description: >-
            Optional activation code to identify which merchant this webhook
            belongs to. Used for multi-merchant environments.
          schema:
            type: string
          example: MERCHANT_ABC_123
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TokenRequest'
            example:
              request:
                Token: eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0...
      responses:
        '201':
          description: Webhook created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Webhook'
        '400':
          description: Bad Request - Invalid webhook URL or actions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized - Invalid or missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Conflict - Webhook URL already registered
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - basicAuth: []
components:
  schemas:
    TokenRequest:
      type: object
      required:
        - request
      properties:
        request:
          type: object
          required:
            - Token
          properties:
            Token:
              type: string
              description: >-
                JWE-encrypted token containing the request payload. Algorithm:
                A256KW | Encryption: A256CBC-HS512. See the Encryption Guide for
                details.
    Webhook:
      type: object
      properties:
        id:
          type: string
          description: Unique identifier for the webhook
        webhookAction:
          type: array
          items:
            type: string
            enum:
              - QRPH_SUCCESS
              - QRPH_DECLINED
          description: List of transaction events this webhook receives
        webhookStatus:
          type: string
          enum:
            - ENABLED
            - DISABLED
          description: Current status of the webhook
        webhookUrl:
          type: string
          format: uri
          description: The HTTPS URL where notifications are sent
    ErrorResponse:
      type: object
      properties:
        statusCode:
          type: integer
          description: HTTP status code
        message:
          type: string
          description: Error message
        error:
          type: string
          description: Error type
  securitySchemes:
    basicAuth:
      type: http
      scheme: basic
      description: >-
        HTTP Basic Authentication using your Secret Key as the username and an
        empty password

````