> For the complete documentation index, see [llms.txt](https://docs.one-cp.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.one-cp.com/configuration-and-setup/webhooks.md).

# Webhooks & Events

Reporting Webhook Configuration

This document provides detailed instructions for configuring the Reporting Webhook within your contract creation, signing, and payment system. This webhook allows real-time updates and notifications based on specified triggers and conditions.

### Accessing the Webhook Configuration

To configure the Reporting Webhook, follow these steps:

1. Navigate to **Dashboard** -> **Configurations** -> **Reporting**.
2. Click **New Reporting**.
3. From the list, choose **Webhook** and click **Next**. You will be presented with the webhook configuration page.

*Note: Screenshot of webhook configuration interface will be added here.*

### Webhook Configuration Settings

The webhook configuration page contains several settings that you need to fill out. Below is a detailed description of each setting:

#### Business Units (Required)

Select the Business Units to which this configuration will be linked. This is a mandatory field.

#### Triggers (Required)

Select the triggers that will activate the webhook. You can choose multiple triggers from the following options:

| Trigger     | When it fires                                               |
| ----------- | ----------------------------------------------------------- |
| `create`    | Contract (transaction) is created                           |
| `update`    | Any field on the contract (transaction) is updated          |
| `send`      | Contract (transaction) email is dispatched to the recipient |
| `review`    | Recipient reviews the contract through the checkout         |
| `expired`   | Contract reaches its expiry date/time                       |
| `confirmed` | Contract is confirmed/accepted by the recipient             |
| `paid`      | Payment is successfully completed                           |
| `canceled`  | Contract is canceled                                        |

#### Payload URL (Required)

Enter the Webhook URL where the payload will be sent. This is a mandatory field.

#### Content Type (Required)

Select the content type for the payload. This is a mandatory field. The available options are:

* `application/json`
* `application/x-www-form-urlencoded`

#### Secret (Optional)

Enter a secret value that will be used to compute an HMAC-SHA256 hash.

#### SSL Verification (Required)

Select the SSL verification option. This is a mandatory field and contains the following options:

* **Enable SSL verification** (default)
* **Disable SSL verification**

This setting determines if SSL verification is required when making HTTP or HTTPS requests.

#### Data to Receive (Required)

Select the type of data you want the webhook to receive. This is a mandatory field and contains the following options:

* **Only Update Status**
* **Send me everything** (default)

Choose whether to receive only the status update of the contract or all the data of the contract along with the status.

#### Assignments Fields (Optional)

Provide an array of field names that define which extra field data to report for each company that belongs to the Business Units. This field is optional.

#### Data Field Mapping (Optional)

Provide an array of field names that define how each field in the transaction (contract) is mapped to the properties in the pushback report. This field is optional.

### Example Configuration

Here is an example of how to fill out the configuration:

* **Business Units**: Provider BU
* **Triggers**: create, update, send
* **Payload URL**: `https://example.com/webhook`
* **Content-Type**: `application/json`
* **Secret**: `your_secret_key`
* **SSL Verification**: Enable SSL verification
* **Data to Receive**: Send me everything

By following these steps and filling out the configuration settings as described, you can successfully set up the Reporting Webhook to meet your specific needs.

For further assistance or troubleshooting, please refer to the support documentation or contact our support team.

## Webhook Payload Envelope

Every webhook delivery wraps the contract payload in a standard envelope. The envelope contains the following fields:

| Field        | Description                                                        |
| ------------ | ------------------------------------------------------------------ |
| `eventId`    | Globally unique identifier for this event (use for idempotency)    |
| `eventType`  | Trigger name that caused this delivery (e.g. `contract.confirmed`) |
| `apiVersion` | API version that produced this payload                             |
| `timestamp`  | ISO-8601 UTC timestamp of the first dispatch attempt               |
| `data`       | Full contract payload (or status-only if configured)               |

> **`contract.confirmed` and `contract.paid` events** — when **"Send me everything"** is selected, the `data` object includes a `checkout` field with fully resolved customer, beneficiary, billing, payment, and card data (no raw IDs). See the payload examples below.

### Request Headers

Every delivery also includes the following HTTP headers:

| Header                | Description                                                      |
| --------------------- | ---------------------------------------------------------------- |
| `X-Webhook-Id`        | The `eventId` of the delivery (same as in the envelope body)     |
| `X-Webhook-Timestamp` | Unix epoch (seconds) — used in signature computation             |
| `X-Webhook-Signature` | HMAC-SHA256 signature — only present when a Secret is configured |

***

## HMAC Signature Verification

When you configure a **Secret**, every delivery includes an `X-Webhook-Signature` header. Verify it to confirm the request is genuine and has not been tampered with.

### How to verify

To verify a delivery, compute an HMAC-SHA256 hash over the string formed by concatenating the timestamp, a dot (`.`), and the raw request body — using your configured secret as the key. Compare the resulting hex string against the value in the `X-Webhook-Signature` header.

The `X-Webhook-Timestamp` header contains the Unix epoch (seconds) used in the signature. **Reject any delivery where the timestamp is more than 5 minutes old** to prevent replay attacks.

> **Important:** Always use a timing-safe string comparison when checking the signature to prevent timing attacks.

***

## Retry Behaviour

If your endpoint does not return a `2xx` response, or the connection times out, the delivery is retried automatically using exponential back-off with ±10% random jitter.

| Attempt         | Delay before retry |
| --------------- | ------------------ |
| 1st (immediate) | —                  |
| 2nd retry       | \~1 minute         |
| 3rd retry       | \~5 minutes        |
| 4th retry       | \~15 minutes       |
| 5th retry       | \~1 hour           |
| 6th retry       | \~6 hours          |
| 7th retry       | \~24 hours         |
| 8th retry       | \~24 hours         |

After 8 failed attempts the delivery is marked **ERROR** and no further automatic retries are made. You can trigger a manual retry from the **Dashboard → Contracts → \[Contract] → Reporting Events** panel.

### Delivery Statuses

| Status     | Meaning                                                 |
| ---------- | ------------------------------------------------------- |
| `PENDING`  | Delivery queued, first attempt not yet made             |
| `RETRYING` | One or more failed attempts; waiting for the next retry |
| `SEND`     | Endpoint returned 2xx                                   |
| `ERROR`    | All retry attempts exhausted                            |

***

## Idempotency

Each delivery carries a unique `eventId`. If you receive the same `eventId` more than once (e.g. after a network failure caused the delivery to be retried even though you already processed it), you should deduplicate on your side using this field.

***

## Payload Examples

### `contract.confirmed` — "Send me everything"

Sent when a contract is confirmed. Includes full checkout and payment schedule with resolved data.

```json
{
  "eventId": "evt_5f8d9c0b-9e2b-4b8d-b4e8-9b9d9d2a1f11",
  "eventType": "contract.confirmed",
  "apiVersion": "2025-04-01",
  "timestamp": "2025-04-22T10:33:51Z",
  "data": {
    "id": "C-2025-000123",
    "status": "CONFIRMED",
    "title": "Enterprise Travel Plan",
    "currency": "EUR",
    "externalIds": {
      "sap_id": "12345"
    },
    "items": [...],
    "checkout": {
      "reference": "ABC123",
      "payment": {
        "reference": "stripe-ch-001",
        "provider": "STRIPE"
      },
      "customer": {
        "firstName": "John",
        "lastName": "Doe",
        "email": "john.doe@example.com",
        "phone": "+49123456789",
        "address": {
          "street": "Main St",
          "house": "1",
          "city": "Berlin",
          "country": "DE",
          "zipCode": "10115"
        }
      },
      "beneficiary": {
        "firstName": "Jane",
        "lastName": "Smith",
        "email": "jane.smith@example.com"
      },
      "billing": {
        "name": "ACME Corp",
        "address": {
          "street": "Corp Ave",
          "house": "42",
          "city": "Munich",
          "country": "DE",
          "zipCode": "80331"
        },
        "externalIds": {
          "sap_id": "67890"
        },
        "identifiers": {
          "VATIN": "DE123456789"
        }
      },
      "card": {
        "masked": "411111****1111",
        "expiryMonth": "12",
        "expiryYear": "2027",
        "cardHolder": {
          "first": "John",
          "last": "Doe"
        }
      },
      "reporting": {
        "costCenter": "CC-001"
      }
    },
    "paymentSchedule": {
      "type": "ONCE",
      "dueDate": "2025-05-01",
      "paymentTerms": "NET30",
      "events": [
        {
          "id": "pe_001",
          "type": "PAYMENT",
          "paymentDate": "2025-05-01",
          "value": 1500.0,
          "unit": "AMOUNT",
          "status": "PENDING"
        }
      ]
    }
  }
}
```

### `contract.paid` — "Send me everything"

Sent when payment is successfully completed. Includes the same full checkout structure as `contract.confirmed`, with the payment event status updated to `PAID` and the payment reference resolved.

```json
{
  "eventId": "evt_7a1c3d22-b3f4-4c9e-a0d1-1c2b3d4e5f60",
  "eventType": "contract.paid",
  "apiVersion": "2025-04-01",
  "timestamp": "2025-05-01T14:22:09Z",
  "data": {
    "id": "C-2025-000123",
    "status": "PAID",
    "title": "Enterprise Travel Plan",
    "currency": "EUR",
    "externalIds": {
      "sap_id": "12345"
    },
    "items": [...],
    "checkout": {
      "reference": "ABC123",
      "payment": {
        "reference": "stripe-ch-001",
        "provider": "STRIPE"
      },
      "customer": {
        "firstName": "John",
        "lastName": "Doe",
        "email": "john.doe@example.com",
        "phone": "+49123456789",
        "address": {
          "street": "Main St",
          "house": "1",
          "city": "Berlin",
          "country": "DE",
          "zipCode": "10115"
        }
      },
      "beneficiary": {
        "firstName": "Jane",
        "lastName": "Smith",
        "email": "jane.smith@example.com"
      },
      "billing": {
        "name": "ACME Corp",
        "address": {
          "street": "Corp Ave",
          "house": "42",
          "city": "Munich",
          "country": "DE",
          "zipCode": "80331"
        },
        "externalIds": {
          "sap_id": "67890"
        },
        "identifiers": {
          "VATIN": "DE123456789"
        }
      },
      "card": {
        "masked": "411111****1111",
        "expiryMonth": "12",
        "expiryYear": "2027",
        "cardHolder": {
          "first": "John",
          "last": "Doe"
        }
      },
      "reporting": {
        "costCenter": "CC-001"
      }
    },
    "paymentSchedule": {
      "type": "ONCE",
      "dueDate": "2025-05-01",
      "paymentTerms": "NET30",
      "events": [
        {
          "id": "pe_001",
          "type": "PAYMENT",
          "paymentDate": "2025-05-01",
          "value": 1500.0,
          "unit": "AMOUNT",
          "status": "PAID"
        }
      ]
    }
  }
}
```

### `contract.paid` — "Only Update Status"

Sent when payment is completed. Only the envelope fields and contract status are included.

```json
{
  "eventId": "evt_7a1c3d22-b3f4-4c9e-a0d1-1c2b3d4e5f60",
  "eventType": "contract.paid",
  "apiVersion": "2025-04-01",
  "timestamp": "2025-05-01T14:22:09Z",
  "data": {
    "status": "PAID"
  }
}
```

### `contract.confirmed` — "Only Update Status"

```json
{
  "eventId": "evt_5f8d9c0b-9e2b-4b8d-b4e8-9b9d9d2a1f11",
  "eventType": "contract.confirmed",
  "apiVersion": "2025-04-01",
  "timestamp": "2025-04-22T10:33:51Z",
  "data": {
    "status": "CONFIRMED"
  }
}
```

***

## Signature Verification — Code Examples

### Node.js

```js
const crypto = require("crypto");

function verifyWebhookSignature(
  secret,
  timestampHeader,
  body,
  signatureHeader,
) {
  // Reject deliveries older than 5 minutes
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - Number(timestampHeader)) > 300) {
    return false;
  }

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestampHeader}.${body}`)
    .digest("hex");

  const received = signatureHeader.replace(/^sha256=/, "");

  // Use timing-safe comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(received, "hex"),
  );
}

// Express example
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const valid = verifyWebhookSignature(
    process.env.WEBHOOK_SECRET,
    req.headers["X-Webhook-Timestamp"],
    req.body.toString(), // raw bytes as string
    req.headers["X-Webhook-Signature"],
  );
  if (!valid) return res.status(401).send("Invalid signature");
  // process req.body ...
  res.sendStatus(200);
});
```

### Python

```python
import hashlib
import hmac
import time

def verify_webhook_signature(secret: str, timestamp: str, body: str, signature: str) -> bool:
    # Reject deliveries older than 5 minutes
    if abs(time.time() - int(timestamp)) > 300:
        return False

    expected = hmac.new(
        secret.encode(),
        f"{timestamp}.{body}".encode(),
        hashlib.sha256
    ).hexdigest()

    received = signature.removeprefix("sha256=")

    # Use constant-time comparison to prevent timing attacks
    return hmac.compare_digest(expected, received)

# Flask example
from flask import Flask, request, abort

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
    raw_body = request.get_data(as_text=True)
    valid = verify_webhook_signature(
        secret=os.environ['WEBHOOK_SECRET'],
        timestamp=request.headers.get('X-Webhook-Timestamp', ''),
        body=raw_body,
        signature=request.headers.get('X-Webhook-Signature', '')
    )
    if not valid:
        abort(401)
    # process request.json ...
    return '', 200
```

***

All data is access-controlled and only shows deliveries belonging to your accessible Business Units.
