> ## 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.

# Simulate Webhook

> Test your webhook integration by triggering simulated transaction notifications in sandbox

## Overview

The Simulate Webhook endpoint allows you to test your webhook integration without creating real QR codes or processing actual transactions. It sends encrypted webhook notifications to your registered endpoint, exactly like real transaction webhooks.

<Tip>
  **Sandbox Only:** This endpoint is only available in the sandbox environment for testing purposes. It's not available in production.
</Tip>

<Note>
  The simulated webhook payloads sent to your endpoint are **JWE-encrypted**, identical to real transaction webhooks. See the [Encryption Guide](/docs/qr/encryption) to understand how to decrypt incoming notifications.
</Note>

## Endpoint

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

## Authentication

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

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

## Request

### Headers

| Header          | Value                         | Required |
| --------------- | ----------------------------- | -------- |
| `Authorization` | `Basic {base64(secret_key:)}` | Yes      |
| `Content-Type`  | `application/json`            | Yes      |

### 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                                                                                                                         |
| --------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `qrBody`  | string | Yes      | Raw QRPH payload used to simulate a transaction                                                                                     |
| `useCase` | string | Yes      | Transaction event type: `SUCCESS`, `MISSING_DESTINATION_ACCOUNT`, `MISSING_PARTNER_REVENUE_ACCOUNT`, or `UNSUPPORTED_TRANSFER_TYPE` |

<AccordionGroup>
  <Accordion title="qrBody" icon="qrcode">
    The raw QRPH payload used to simulate a transaction.

    * **Example:** `"00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0"`
  </Accordion>

  <Accordion title="useCase" icon="flask">
    The type of transaction event to simulate:

    * `SUCCESS` - Simulates a successful payment
    * `MISSING_DESTINATION_ACCOUNT` - Simulates a declined payment (missing destination account)
    * `MISSING_PARTNER_REVENUE_ACCOUNT` - Simulates a declined payment (missing partner revenue account)
    * `UNSUPPORTED_TRANSFER_TYPE` - Simulates a declined payment (unsupported transfer type)

    **Example:** `"SUCCESS"`
  </Accordion>
</AccordionGroup>

### Example Payloads (Before Encryption)

<CodeGroup>
  ```json Successful Payment theme={null}
  {
    "qrBody":
      "00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0",
    "useCase": "SUCCESS"
  }
  ```

  ```json Declined Payment theme={null}
  {
    "qrBody":
      "00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0",
    "useCase": "MISSING_DESTINATION_ACCOUNT"
  }
  ```
</CodeGroup>

## Response

### Success Response

**Status Code:** `200 OK`

Returns confirmation that the simulated webhook was sent to your registered endpoint.

<ResponseField name="id" type="string">
  The unique identifier of the original transaction (i.e., the `id` returned from the Create Dynamic QR Ph response).

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

### Response Example

```json theme={null}
{
"id": "a78efd32-de3b-4854-b599-11ae9f98f97e"
}
```

<ResponseExample>
  ```json 200 Success theme={null}
  {
  "id": "a78efd32-de3b-4854-b599-11ae9f98f97e"
  }
  ```

  ```json 400 Bad Request theme={null}
  {
    "code": "20000003",
    "error": "useCase is invalid.",
    "referenceNumber": "097bf60a-bdab-40bf-b615-3c0eae693b86"
  }
  ```

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

  ```json 404 Not Found theme={null}
  {
    "code": "20000005",
    "error": "No enabled webhooks found",
    "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>

### Error Responses

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

    **Causes:**

    * Missing `useCase` field
    * Invalid `useCase` value

    **Response Example:**

    ```json theme={null}
    {
      "code": "20000003",
      "error": "useCase is invalid.",
      "referenceNumber": "abc123-def456-ghi789"
    }
    ```

    **Solution:**

    * Ensure `useCase` is one of: `SUCCESS`, `MISSING_DESTINATION_ACCOUNT`, `MISSING_PARTNER_REVENUE_ACCOUNT`, or `UNSUPPORTED_TRANSFER_TYPE`
    * Check spelling and capitalization
  </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:)}`
  </Accordion>

  <Accordion title="404 Not Found" icon="circle-xmark">
    **Status Code:** `404`

    **Cause:** No enabled webhooks configured

    **Response Example:**

    ```json theme={null}
    {
      "code": "20000005",
      "error": "No enabled webhooks found",
      "referenceNumber": "abc123-def456-ghi789"
    }
    ```

    **Solution:**

    * Create at least one webhook using [Create Webhook API](/api-reference/webhooks/create)
    * Ensure webhook status is `ENABLED`
    * Verify webhook actions include the simulated event type
  </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
  </Accordion>
</AccordionGroup>

<RequestExample>
  ```bash cURL (Success) 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 containing the simulate payload
  ENCRYPTED_TOKEN="eyJhbGciOiJBMjU2S1ciL..."

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

  ```bash cURL (Declined) theme={null}
  # 1. Create encrypted JWE token containing: {"qrBody": "...", "useCase": "MISSING_DESTINATION_ACCOUNT"}
  ENCRYPTED_TOKEN="eyJhbGciOiJBMjU2S1ciL..."

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

  ```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 simulateWebhook(qrBody, useCase) {
    // 1. Prepare the payload
    const payload = { qrBody, useCase };

    // 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/qrph/simulate',
      { request: { Token: jweToken } },
      {
        auth: { username: SECRET_KEY, password: '' },
        headers: { 'Content-Type': 'application/json' }
      }
    );

    console.log('Webhook simulation sent');
    console.log('Response:', response.data);

    return response.data;
  }

  const qrBody = '00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0';

  // Test successful payment
  simulateWebhook(qrBody, 'SUCCESS')
    .catch(error => {
      console.error('Error:', error.response?.data || error.message);
    });

  // Test declined payment
  simulateWebhook(qrBody, 'MISSING_DESTINATION_ACCOUNT');
  ```

  ```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 simulate_webhook(qr_body, use_case):
      # 1. Prepare the payload
      payload = {
          'qrBody': qr_body,
          'useCase': use_case
      }

      # 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/qrph/simulate',
          json={'request': {'Token': jwe_token}},
          auth=(SECRET_KEY, ''),
          headers={'Content-Type': 'application/json'}
      )

      response.raise_for_status()

      result = response.json()
      print('Webhook simulation sent')
      print(f"Response: {result}")

      return result

  qr_body = '00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0'

  # Test successful payment
  simulate_webhook(qr_body, 'SUCCESS')

  # Test declined payment
  simulate_webhook(qr_body, 'MISSING_DESTINATION_ACCOUNT')
  ```

  ```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 simulateWebhook($qrBody, $useCase) {
      global $secretKey, $encryptionKey;

      // 1. Prepare the payload
      $payload = [
          'qrBody' => $qrBody,
          'useCase' => $useCase
      ];

      // 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/qrph/simulate');
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
      curl_setopt($ch, CURLOPT_POST, true);
      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']);

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

      if ($httpCode === 200) {
          $result = json_decode($response, true);
          echo "Webhook simulation sent\n";
          echo "Response: " . json_encode($result) . "\n";
          return $result;
      } else {
          echo "Error: HTTP $httpCode\n";
          echo "Details: $response\n";
          return null;
      }
  }

  $qrBody = '00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0';

  // Test successful payment
  simulateWebhook($qrBody, 'SUCCESS');

  // Test declined payment
  simulateWebhook($qrBody, 'MISSING_DESTINATION_ACCOUNT');
  ```

  ```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 SimulatePayload struct {
  	QRBody  string `json:"qrBody"`
  	UseCase string `json:"useCase"`
  }

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

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

  func simulateWebhook(qrBody, useCase string) error {
  	secretKey := os.Getenv("MODULUS_SECRET_KEY")
  	encryptionKey := os.Getenv("MODULUS_ENCRYPTION_KEY")

  	// 1. Prepare the payload
  	payload := SimulatePayload{
  		QRBody:  qrBody,
  		UseCase: useCase,
  	}

  	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/qrph/simulate", 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 == 200 {
  		fmt.Printf("Webhook simulation sent (%s)\n", useCase)
  		fmt.Printf("Response: %s\n", string(body))
  		return nil
  	}

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

  func main() {
  	qrBody := "00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0"

  	// Test successful payment
  	if err := simulateWebhook(qrBody, "SUCCESS"); err != nil {
  		fmt.Printf("Error: %v\n", err)
  	}

  	// Test declined payment
  	if err := simulateWebhook(qrBody, "MISSING_DESTINATION_ACCOUNT"); err != nil {
  		fmt.Printf("Error: %v\n", err)
  	}
  }
  ```

  ```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)
      {
          var qrBody = "00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0";

          // Test successful payment simulation
          await SimulateWebhook(qrBody, "SUCCESS");

          // Test declined payment simulation
          await SimulateWebhook(qrBody, "MISSING_DESTINATION_ACCOUNT");
      }

      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 SimulateWebhook(string qrBody, string useCase)
      {
          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 and encrypt the payload
              var payload = new { qrBody, useCase };
              var key = Base64UrlDecode(encryptionKey);
              var payloadJson = JsonSerializer.Serialize(payload);
              var jweToken = JWT.Encode(payloadJson, key, JweAlgorithm.A256KW, JweEncryption.A256CBC_HS512);

              // 2. 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/qrph/simulate",
                  content
              );

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

              if (response.IsSuccessStatusCode)
              {
                  Console.WriteLine($"{useCase} webhook simulation sent successfully");
                  Console.WriteLine($"Response: {responseBody}");
              }
              else
              {
                  Console.WriteLine($"Error: HTTP {(int)response.StatusCode}");
                  Console.WriteLine($"Details: {responseBody}");
              }
          }
          catch (Exception e)
          {
              Console.WriteLine($"Error: {e.Message}");
          }
      }
  }
  ```
</RequestExample>

## Testing Workflow

<Steps>
  <Step title="Register Webhook">
    Create a webhook endpoint pointing to your test server:

    ```javascript theme={null}
    const webhook = await createWebhook(
      'https://your-test-server.com/webhooks/modulus',
      ['QRPH_SUCCESS', 'QRPH_DECLINED'],
      'ENABLED'
    );
    ```
  </Step>

  <Step title="Simulate Success Event">
    Send a successful payment webhook:

    ```javascript theme={null}
    await simulateWebhook('SUCCESS');
    ```

    Check your webhook endpoint receives and processes the notification correctly.
  </Step>

  <Step title="Simulate Declined Event">
    Send a declined payment webhook:

    ```javascript theme={null}
    await simulateWebhook('MISSING_DESTINATION_ACCOUNT');
    ```

    Verify your endpoint handles payment failures gracefully.
  </Step>

  <Step title="Test Error Scenarios">
    Test your webhook error handling:

    * Return 5xx status to trigger retries
    * Delay response beyond 10 seconds to test timeouts
    * Verify idempotency by sending duplicate webhooks
  </Step>

  <Step title="Verify Decryption">
    Confirm your endpoint can decrypt JWE payloads successfully using your secret key.
  </Step>
</Steps>

## Use Cases

<AccordionGroup>
  <Accordion title="Initial Integration Testing" icon="flask">
    Test your webhook handler during development:

    ```javascript theme={null}
    console.log('Testing webhook integration...');

    // Test success case
    await simulateWebhook('SUCCESS');
    await wait(2000);

    // Test decline case
    await simulateWebhook('MISSING_DESTINATION_ACCOUNT');
    await wait(2000);

    console.log('Webhook integration test complete');
    ```
  </Accordion>

  <Accordion title="CI/CD Automated Testing" icon="gears">
    Include webhook simulation in your test suite:

    ```javascript theme={null}
    describe('Webhook Integration', () => {
      it('should handle successful payments', async () => {
        const result = await simulateWebhook('SUCCESS');
        expect(result.webhooksSent).toBeGreaterThan(0);

        // Wait for webhook to be processed
        await wait(3000);

        // Verify order was marked as paid
        const order = await db.orders.findOne({ status: 'paid' });
        expect(order).toBeDefined();
      });

      it('should handle declined payments', async () => {
        const result = await simulateWebhook('MISSING_DESTINATION_ACCOUNT');
        expect(result.webhooksSent).toBeGreaterThan(0);

        // Verify order was marked as failed
        await wait(3000);
        const order = await db.orders.findOne({ status: 'payment_failed' });
        expect(order).toBeDefined();
      });
    });
    ```
  </Accordion>

  <Accordion title="Verify Decryption Implementation" icon="lock">
    Test your JWE decryption logic:

    ```javascript theme={null}
    // Send simulated webhook
    await simulateWebhook('SUCCESS');

    // Check server logs to verify decryption succeeded
    console.log('Check your webhook endpoint logs for:');
    console.log('- JWE decryption success message');
    console.log('- Decrypted payload structure');
    console.log('- Webhook processing result');
    ```
  </Accordion>

  <Accordion title="Test Error Handling" icon="triangle-exclamation">
    Verify your webhook handler manages errors gracefully:

    ```javascript theme={null}
    // Test idempotency by sending same webhook twice
    await simulateWebhook('SUCCESS');
    await wait(1000);
    await simulateWebhook('SUCCESS');

    // Verify duplicate webhook was ignored
    // (should only process once)
    ```
  </Accordion>

  <Accordion title="Demo and Presentations" icon="presentation">
    Demonstrate webhook functionality without real transactions:

    ```javascript theme={null}
    console.log('Starting live webhook demo...');

    // Simulate successful payment
    console.log('Simulating successful payment...');
    await simulateWebhook('SUCCESS');

    await wait(2000);

    // Simulate declined payment
    console.log('Simulating declined payment...');
    await simulateWebhook('MISSING_DESTINATION_ACCOUNT');

    console.log('Demo complete!');
    ```
  </Accordion>
</AccordionGroup>

## Testing Checklist

<Steps>
  <Step title="Setup">
    * [ ] Webhook endpoint is accessible
    * [ ] Webhook is registered and enabled
    * [ ] Webhook actions include `QRPH_SUCCESS` and `QRPH_DECLINED`
  </Step>

  <Step title="Test Successful Payment">
    * [ ] Simulate SUCCESS webhook
    * [ ] Webhook received at endpoint
    * [ ] JWE payload decrypted successfully
    * [ ] Order marked as paid
    * [ ] Confirmation email sent
  </Step>

  <Step title="Test Declined Payment">
    * [ ] Simulate declined webhook (e.g., `MISSING_DESTINATION_ACCOUNT`)
    * [ ] Webhook received at endpoint
    * [ ] Decline reason parsed correctly
    * [ ] Order marked as failed
    * [ ] Customer notified
  </Step>

  <Step title="Test Error Handling">
    * [ ] Duplicate webhooks ignored (idempotency)
    * [ ] Invalid webhooks rejected
    * [ ] Timeout scenarios handled
    * [ ] Retry mechanism tested
  </Step>

  <Step title="Verify Logging">
    * [ ] All webhooks logged
    * [ ] Processing results recorded
    * [ ] Errors captured
  </Step>
</Steps>

## Best Practices

<CardGroup cols={2}>
  <Card title="Test Both Scenarios" icon="check-double">
    Always test both success and decline webhooks:

    ```javascript theme={null}
    async function runFullWebhookTest() {
      // Test success
      await simulateWebhook('SUCCESS');
      await verifySuccessHandling();

      // Test declined
      await simulateWebhook('MISSING_DESTINATION_ACCOUNT');
      await verifyDeclineHandling();

      console.log('All webhook tests passed');
    }
    ```
  </Card>

  <Card title="Use in CI/CD" icon="robot">
    Automate webhook testing in your deployment pipeline:

    ```yaml theme={null}
    # .github/workflows/test.yml
    - name: Test Webhook Integration
      run: |
        npm run start:test-server &
        sleep 5
        npm run test:webhooks
    ```
  </Card>

  <Card title="Monitor Test Results" icon="chart-line">
    Track webhook simulation outcomes:

    ```javascript theme={null}
    const testResults = {
      successTests: 0,
      declinedTests: 0,
      failures: 0
    };

    try {
      await simulateWebhook('SUCCESS');
      testResults.successTests++;
    } catch (error) {
      testResults.failures++;
    }

    console.log('Test results:', testResults);
    ```
  </Card>

  <Card title="Test with Delays" icon="clock">
    Allow time for webhook processing between tests:

    ```javascript theme={null}
    await simulateWebhook('SUCCESS');
    await wait(2000); // Wait 2 seconds

    await simulateWebhook('MISSING_DESTINATION_ACCOUNT');
    await wait(2000);

    // Now verify results
    ```
  </Card>
</CardGroup>

## Differences from Real Webhooks

| Aspect               | Simulate API       | Real Webhooks                        |
| -------------------- | ------------------ | ------------------------------------ |
| **Trigger**          | Manual API call    | Actual QR Ph transaction             |
| **Reference Number** | `SIM-REF-*` prefix | Real reference from QR creation      |
| **Transaction Data** | Test data          | Real customer payment data           |
| **Timing**           | Immediate          | Depends on payment processing        |
| **Retries**          | Same as real       | Up to 4 attempts (0, 15, 30, 45 min) |
| **Encryption**       | JWE encrypted      | JWE encrypted                        |
| **Availability**     | Sandbox only       | Sandbox and production               |

<Note>
  Simulated webhooks behave identically to real webhooks except for the data content. Your webhook handler should process them the same way.
</Note>

## Troubleshooting

<AccordionGroup>
  <Accordion title="404: No Enabled Webhooks Found" icon="circle-xmark">
    **Symptom:** Receive 404 error when calling Simulate API

    **Possible Causes:**

    * No webhooks registered
    * All webhooks are disabled
    * Webhook actions don't include the simulated event

    **Solutions:**

    ```javascript theme={null}
    // Check existing webhooks
    const webhooks = await getWebhooks();
    console.log('Webhooks:', webhooks);

    // Ensure at least one is enabled
    const enabled = webhooks.filter(w => w.status === 'ENABLED');

    if (enabled.length === 0) {
      // Create or enable a webhook
      await createWebhook(
        'https://your-server.com/webhooks',
        ['QRPH_SUCCESS', 'QRPH_DECLINED'],
        'ENABLED'
      );
    }
    ```
  </Accordion>

  <Accordion title="Webhook Not Received" icon="inbox">
    **Symptom:** Simulate API returns success but webhook not received

    **Possible Causes:**

    * Webhook URL not accessible
    * Firewall blocking requests
    * Endpoint not listening

    **Solutions:**

    * Test webhook URL accessibility externally
    * Check server logs for incoming requests
    * Verify endpoint is running and listening
    * Test with ngrok or similar during development
  </Accordion>

  <Accordion title="Decryption Fails" icon="lock">
    **Symptom:** Webhook received but cannot decrypt payload

    **Possible Causes:**

    * Wrong secret key
    * JWE library issue
    * Incorrect algorithm configuration

    **Solutions:**

    ```javascript theme={null}
    // Verify secret key
    console.log('Secret key length:', SECRET_KEY.length);
    console.log('Secret key starts with:', SECRET_KEY.substring(0, 3));

    // Test decryption
    try {
      const payload = await decryptWebhook(webhookData, SECRET_KEY);
      console.log('Decryption successful:', payload);
    } catch (error) {
      console.error('Decryption failed:', error);
    }
    ```
  </Accordion>

  <Accordion title="Multiple Webhooks Sent" icon="clone">
    **Symptom:** Receive multiple webhook deliveries for one simulation

    **Cause:** Multiple enabled webhooks registered

    **Expected Behavior:**
    The Simulate API sends notifications to **all** enabled webhooks that are configured for the simulated event.

    **Verification:**

    ```javascript theme={null}
    const result = await simulateWebhook('SUCCESS');
    console.log(`Webhooks sent: ${result.webhooksSent}`);
    // This tells you how many webhooks received the notification
    ```
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Create Webhook" icon="plus" href="/api-reference/webhooks/create">
    Register a webhook to test with simulations
  </Card>

  <Card title="Receiving Webhooks" icon="webhook" href="/docs/webhooks/receiving">
    Implement webhook processing logic
  </Card>

  <Card title="Payload Structure" icon="brackets-curly" href="/docs/webhooks/payload">
    Understand webhook payload format
  </Card>

  <Card title="Setup Guide" icon="gear" href="/docs/webhooks/setup">
    Complete webhook integration guide
  </Card>
</CardGroup>


## OpenAPI

````yaml POST /v1/webhooks/qrph/simulate
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/qrph/simulate:
    post:
      tags:
        - Testing
      summary: Simulate Webhook
      description: >-
        Test your webhook integration by triggering simulated transaction
        notifications. This endpoint is only available in the sandbox
        environment.
      operationId: simulateWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TokenRequest'
            example:
              request:
                Token: eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0...
      responses:
        '200':
          description: Webhook simulation sent successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimulateWebhookResponse'
              example:
                message: Webhook simulation sent successfully
        '400':
          description: Bad Request - Invalid useCase value
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized - Invalid or missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not Found - No webhook registered to receive simulation
          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.
    SimulateWebhookResponse:
      type: object
      properties:
        id:
          type: string
          description: >-
            The unique identifier of the original transaction (i.e., the id
            returned from the Create Dynamic QR Ph response).
    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

````