# Webhook configuration

This topic describes how to use webhooks with the Rebilly API.
For information on how to configure webhooks in the Rebilly UI, see [Webhooks](/docs/automations/webhooks).

Use webhooks to notify your systems when certain events occur and to collect event information.
Rebilly sends notifications through HTTP and HTTPS methods to a URL of your choice.
To view webhooks operations, see the [Webhooks API docs](/catalog/all/webhooks).

Each website that you add to Rebilly can have its own webhook configuration.
This includes the URI to which you want to send the webhook, along with optional HTTP basic access authentication details.

One organization can have up to 25 active webhooks.
Disable or remove one or more webhooks to create new ones when the limit is reached.

Webhooks may be delayed or delivered multiple times.
Keep this in mind if you intend to use webhooks to make actionable decisions.
For more information, see [Data validity](#data-validity).

## Define events

To restrict a webhook to a specific event or event types, use the `eventsFilter` field.
To add more than one event type, set an array of system event types as follows:`["gateway-account-requested", "subscription-canceled"]`.
If you leave the `eventsFilter` field empty blank all events are active for the webhook.

For a complete list of event types, see [Global event types](/catalog/all/webhooks/postwebhook#!t=request&path=eventsFilter).

## Security

[Create webhook service credentials](/catalog/all/service-credentials/postservicecredential) for your webhook host.

HTTP basic access authentication through HTTPS is highly recommended.
This adds an extra layer of security for your webhook endpoint that can prevent undesired data injection by third parties.

Rebilly webhooks may be sent from [these IP addresses](/docs/dev-docs/ip-addresses).
If you use HTTP basic access authentication, Rebilly also sends an authorization header along with the `GET`, `POST`, `PUT`, `PATCH`, or `DELETE` request.

The following is an example of a header with HTTP basic access authentication enabled that uses `username = rebilly, password = superman`:

```text
POST /odt4xsod HTTP/1.1
User-Agent: Rebilly/20e49c5
Host: example.com
Content-Type: application/json; charset=utf-8
Content-Length: 478
Connection: close
Authorization: Basic cmViaWxseTpzdXBlcm1hbgo=
Accept-Encoding: gzip, deflate
```

If you use base64 to decode the value `decode(cmViaWxseTpzdXBlcm1hbgo=)` you receive `rebilly:superman`.

## Responses

Rebilly expects a `HTTP 200 OK` response after a successful webhook transmission to your systems.
If no response is received within 20 seconds, or the HTTP response code is not 200, the request is considered not sent and be marked for retry.
The webhook is sent again automatically after 30 minutes.
If no response is received, the webhook is attempted by an exponential back-off, at 1, 2, 4, 8, and 16 hours for a total of 6 retries attempts until successful.
If still unsuccessful, it is not attempted again.

## User agent

In Rebilly, all outgoing webhooks contain a `User-Agent` header.
The header value changes frequently.
The value format is `Rebilly/%release%`, where `%release%` is a 7 character long alphanumeric string which matches the `/^[a-z0-9]{7}$/` regular expression pattern.

## Data validity

When a notification is triggered, Rebilly sends all available information to your endpoint at the time of the event.
The data contained in the webhook reflects the state of Rebilly records at the time of the event.
If there is a delay, your records might be more up to date than the data that is sent by the webhook.

The following example describes a scenario where a delay occurs in recording of data between systems.

1. A transaction triggers a webhook, and some relating customer information was not supplied at the time when the transaction occurred.
2. The notification transmission is delayed for a short period.
3. Your system modifies or adds this information before a notification is received.
4. The notification for the transaction is received by your systems, but the customer information does not match your records.


## Data authenticity

In Rebilly, all outgoing webhooks contain a `Security-Signature` header.
Use this header to ensure data authenticity.
Signature verification is optional and is not required for your integration to work, but it is recommended.

The authenticity check uses JSON Web Tokens (JWTs) and JSON Web Keys (JWKs) to verify webhooks.

To authenticate webhook data:

1. Locate, load, and cache Rebilly JWKs from the [live](https://api.rebilly.com/.well-known/keys) or [sandbox](https://api-sandbox.rebilly.com/.well-known/keys) environments. 
To ensure proper key rotation, Rebilly recommends a cache Time to Live (TTL) of no greater than 1 hour.
2. Extract the JWT from the `Security-Signature` header of the webhook.
3. Validate the JWT against your preferred JWT and JWK library for your chosen programming language.
4. Decode the payload (second) segment of the JWT and verify the following checks:


If a webhook fails any of the following checks, consider the webhook invalid and discard it.

- The `iss` value is equal to the Rebilly API URL where you expect to receive webhooks.
For example, `https://api.rebilly.com/` or `https://api-sandbox.rebilly.com/`.
- The `aud` value is equal to your organization ID within Rebilly.
- The `iat` value is greater or equal to the current Unix timestamp.
Rebilly recommends a leeway of no greater than 10 seconds.
- The `exp` value is less than the current Unix timestamp.
This is an optional check to prevent replay attacks.
Rebilly accepts webhooks that are received within a 5-minute timeframe between the `iat` and `exp` values.
- The `requestBodyHash` is equal to the SHA256 hash of the raw webhook body.


Authenticity check example
The following example is of a webhook that is received at `2023-02-12 19:45:00 UTC`.
Formatting and spaces are preserved in the webhook body.

```text
POST /odt4xsod HTTP/1.1
User-Agent: Rebilly/20e49c5
Host: example.com
Content-Type: application/json; charset=utf-8
Content-Length: 478
Connection: close
Security-Signature: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImI2MDNmMWY4Y2U0Y2RlMzc2NjExNzlkNGRmODNlYjZlODc0ZTRjZjRjNWNmNWRhMGIxYTY0NDUyMDJmNmJlNGIifQ.eyJpc3MiOiJodHRwczovL2FwaS1zYW5kYm94LnJlYmlsbHkuY29tLyIsImF1ZCI6ImE5ZjY4ZTZlLWUyZTYtNDNkZC1hMjdmLTgxMjAxMjFkNzQyOCIsImlhdCI6MTY3NjIzMDk3MiwiZXhwIjoxNjc2MjMxMjcyLCJyZXF1ZXN0Qm9keUhhc2giOiIyMjA1YThkMjU0M2M5N2E5YThkYzI5YTRmOWZkZjcxN2ZkOTQ3OGI5ODkxYWRmNTJhYmQ5YzA5ZjdhZmRlMDk0In0.ChVc_NAWxfZVEl4g7QhFFO8c5ZtUuTWlP60qQ7gEKtUQdWY70WyzYOREbGPodTbraSNggh1M98gaYSqcQM6vAk3op_9HQpeMtQEIGUOjNDMe2_BmwyeGtcc3_c3Ft0gsARUYatZcVCeeYi7i8Xwekj2xQKkxbkLYEkHHj3f_o2dHXdXaFVvgU2fNN9pmfq1iI2ROM_UXrpGDfsm1nfvZsFLk9cdCum3bBMpTeqelm02ojk6yHES9otoMTYqwGJ5ovyOKaPzMAUJT1tNv1xzf5YxxyBMj6CRqTMPkItrgU3nX6pPMdS5cbEWzhTUcm29WeWrXf90PqLUJTvSxIuMhUA
Accept-Encoding: gzip, deflate

{"dataExportId":"d9dd134f-0bee-46b1-bec0-992da3173336","fileId":"d348968b-41ed-4d7d-b534-02e5265c8912","eventType":"data-export-completed","_embedded":{"dataExport":{"id":"d9dd134f-0bee-46b1-bec0-992da3173336","name":"Preview","resource":"customers","format":"csv","arguments":null,"dateRange":null,"emailNotification":[],"fields":[],"recurring":null,"status":"pending","userId":"3d08e3f3-fa38-4731-9761-8a04400e05d3","recordCount":null,"scheduledTime":"2023-02-12T19:42:52+00:00","expiredTime":"2023-03-12T19:42:52+00:00","purgedTime":"2024-06-12T19:42:52+00:00","createdTime":"2023-02-12T19:42:52+00:00","updatedTime":"2023-02-12T19:42:52+00:00","_links":[{"rel":"self","href":"https:\/\/api-sandbox.rebilly.com\/organizations\/a9f68e6e-e2e6-43dd-a27f-8120121d7428\/experimental\/data-exports\/d9dd134f-0bee-46b1-bec0-992da3173336"},{"rel":"user","href":"https:\/\/api-sandbox.rebilly.com\/users\/3d08e3f3-fa38-4731-9761-8a04400e05d3"}]}},"_links":[{"rel":"dataExport","href":"https:\/\/api-sandbox.rebilly.com\/organizations\/a9f68e6e-e2e6-43dd-a27f-8120121d7428\/experimental\/data-exports\/d9dd134f-0bee-46b1-bec0-992da3173336"},{"rel":"download","href":"https:\/\/api-sandbox.rebilly.com\/organizations\/a9f68e6e-e2e6-43dd-a27f-8120121d7428\/files\/d348968b-41ed-4d7d-b534-02e5265c8912\/download"}]}
```

At `2023-02-12 19:45:00 UTC` the JWKs located at [https://api-sandbox.rebilly.com/.well-known/keys](https://api-sandbox.rebilly.com/.well-known/keys) contain the following key:

```json
[
  {
    "kty": "RSA",
    "n": "sT66o-HJLpVt5qifPS6MsFpu8dj-NZJWWJnoJMohCBHhGNL8v6EJm_kCNY43Czi0Bt5dbrFovPgcjdR1yq938hQ9Hi2SjNoddqcsvOUtThztsXitTKSutBKjVcInN_At_DxCpO8pardiR3aLR-bCClz8iSutmRaTenee6cMUkkZZRs0XRqI_gDPfAoN_f5HWZBXnu-VSUrBpT-PnzA4ICvR9ZaeU3EK3T64QhLnxuGa_o561__CR5oD79sEXILqJoD47uE22FydAvHvrgWqk0RMkBFtSdgmFkhylpAVgnGWAJqB2FLy-yLh5QMS02SxKtz89KRVRe0V4BVx5x3bTbw",
    "e": "AQAB",
    "alg": "RS256",
    "kid": "b603f1f8ce4cde37661179d4df83eb6e874e4cf4c5cf5da0b1a6445202f6be4b",
    "use": "sig"
  }
]
```

The JWT signature could be verified with the key whose `kid` value is `b603f1f8ce4cde37661179d4df83eb6e874e4cf4c5cf5da0b1a6445202f6be4b` as expected.

The decoded JWT payload segment provides:

```json
{
  "iss": "https://api-sandbox.rebilly.com/",
  "aud": "a9f68e6e-e2e6-43dd-a27f-8120121d7428",
  "iat": 1676230972,
  "exp": 1676231272,
  "requestBodyHash": "2205a8d2543c97a9a8dc29a4f9fdf717fd9478b9891adf52abd9c09f7afde094"
}
```

Results of the verification checks:

- The `iss` value is equal to the sandbox API URL.
- The `aud` value is equal to the organization ID `a9f68e6e-e2e6-43dd-a27f-8120121d7428`.
- The `iat` value is equal to `2023-02-12 19:42:52 UTC`, which is less than the reception time of `2023-02-12 19:45:00 UTC`.
- The `exp` value is equal to `2023-02-12 19:47:52 UTC`, which is greater than the reception time of `2023-02-12 19:45:00 UTC`.
- The `requestBodyHash` value is equal to the SHA256 hash of the raw webhook body:


```javascript
sha256('{"dataExportId":"d9dd134f-0bee-46b1-bec0-992da3173336","fileId":"d348968b-41ed-4d7d-b534-02e5265c8912","eventType":"data-export-completed","_embedded":{"dataExport":{"id":"d9dd134f-0bee-46b1-bec0-992da3173336","name":"Preview","resource":"customers","format":"csv","arguments":null,"dateRange":null,"emailNotification":[],"fields":[],"recurring":null,"status":"pending","userId":"3d08e3f3-fa38-4731-9761-8a04400e05d3","recordCount":null,"scheduledTime":"2023-02-12T19:42:52+00:00","expiredTime":"2023-03-12T19:42:52+00:00","purgedTime":"2024-06-12T19:42:52+00:00","createdTime":"2023-02-12T19:42:52+00:00","updatedTime":"2023-02-12T19:42:52+00:00","_links":[{"rel":"self","href":"https://api-sandbox.rebilly.com/organizations/a9f68e6e-e2e6-43dd-a27f-8120121d7428/experimental/data-exports/d9dd134f-0bee-46b1-bec0-992da3173336"},{"rel":"user","href":"https://api-sandbox.rebilly.com/users/3d08e3f3-fa38-4731-9761-8a04400e05d3"}]}},"_links":[{"rel":"dataExport","href":"https://api-sandbox.rebilly.com/organizations/a9f68e6e-e2e6-43dd-a27f-8120121d7428/experimental/data-exports/d9dd134f-0bee-46b1-bec0-992da3173336"},{"rel":"download","href":"https://api-sandbox.rebilly.com/organizations/a9f68e6e-e2e6-43dd-a27f-8120121d7428/files/d348968b-41ed-4d7d-b534-02e5265c8912/download"}]}');
```

## Standard Webhooks signature

Rebilly also signs each webhook in the [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md) format.
The `Security-Signature` header described in [Data authenticity](#data-authenticity) does not change.
Use either check.
Rebilly recommends that new integrations use the Standard Webhooks check, because it verifies against a fixed public key and does not require fetching or caching signing keys.

Each webhook carries these headers:

| Header | Value |
|  --- | --- |
| `webhook-id` | The unique ID of the webhook. Rebilly sends the same ID on every retry of the same webhook. Use it as an idempotency key. |
| `webhook-timestamp` | The Unix timestamp, in seconds, of the delivery attempt. |
| `webhook-signature` | One or more signatures, separated by spaces. Each value is a version prefix, a comma, and a base64 signature. Rebilly sends `v1a` signatures. |


The `v1a` scheme is an asymmetric Ed25519 signature.
The signed content is the `webhook-id` value, a period, the `webhook-timestamp` value, a period, and the raw webhook body.

Public keys, in the `whpk_` format the specification defines:

| Environment | Public key |
|  --- | --- |
| Live, `https://api.rebilly.com/` | `whpk_lr2xkGO1EEdyZxVx4DKQ5XfmSjxOmwv9UcwNPbn9fFI=` |
| Sandbox, `https://api-sandbox.rebilly.com/` | `whpk_qism49rHt4dOh6txzL3dfnMTmhQiITY1A86Y9BJGEZM=` |


To verify a webhook:

1. Read the three headers.
2. Reject the webhook if `webhook-timestamp` is more than 5 minutes from the current time.
3. Build the signed content from `webhook-id`, `webhook-timestamp`, and the raw body.
4. Verify one of the `v1a` signatures against the public key of the environment that sent the webhook.
5. Reject the webhook if no signature verifies.


The Standard Webhooks reference libraries verify only symmetric `v1` signatures, so they do not verify the `v1a` signatures that Rebilly sends.
The following examples verify a webhook without a library.
Each one takes the three request headers, the raw request body exactly as received, and the `whpk_` public key of the environment.
Each one returns `false` for a missing header, a malformed value, or a signature that does not verify.

```js Node.js
import { createPublicKey, verify } from 'node:crypto';

// headers: the request headers, lower-case names. rawBody: the request body as a string or Buffer.
export function verifyWebhook(headers, rawBody, publicKey) {
  const raw = Buffer.from(publicKey.replace(/^whpk_/, ''), 'base64');
  const key = createPublicKey({
    key: { kty: 'OKP', crv: 'Ed25519', x: raw.toString('base64url') },
    format: 'jwk',
  });
  const timestamp = Number(headers['webhook-timestamp']);
  if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) {
    return false;
  }
  const signed = Buffer.concat([
    Buffer.from(`${headers['webhook-id']}.${headers['webhook-timestamp']}.`),
    Buffer.from(rawBody),
  ]);
  return (headers['webhook-signature'] ?? '')
    .split(' ')
    .filter((value) => value.startsWith('v1a,'))
    .some((value) => verify(null, signed, key, Buffer.from(value.slice(4), 'base64')));
}
```

```php PHP
<?php // PHP 8.0 or later, with the sodium extension.

// $headers: the request headers, lower-case names. $rawBody: the request body as received.
function verifyWebhook(array $headers, string $rawBody, string $publicKey): bool
{
    $raw = base64_decode(substr($publicKey, strlen('whpk_')), true);
    if ($raw === false || strlen($raw) !== SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES) {
        return false;
    }
    if (!isset($headers['webhook-id'], $headers['webhook-timestamp'], $headers['webhook-signature'])) {
        return false;
    }
    if (!ctype_digit($headers['webhook-timestamp']) || abs(time() - (int) $headers['webhook-timestamp']) > 300) {
        return false;
    }
    $signed = $headers['webhook-id'] . '.' . $headers['webhook-timestamp'] . '.' . $rawBody;
    foreach (explode(' ', $headers['webhook-signature']) as $value) {
        if (!str_starts_with($value, 'v1a,')) {
            continue;
        }
        $signature = base64_decode(substr($value, 4), true);
        if ($signature === false || strlen($signature) !== SODIUM_CRYPTO_SIGN_BYTES) {
            continue;
        }
        if (sodium_crypto_sign_verify_detached($signature, $signed, $raw)) {
            return true;
        }
    }

    return false;
}
```

```ruby Ruby
require "ed25519" # gem install ed25519 (Ruby 3.0 or later)

# headers: the request headers, lower-case names. raw_body: the request body as received.
def verify_webhook(headers, raw_body, public_key)
  key = Ed25519::VerifyKey.new(public_key.delete_prefix("whpk_").unpack1("m0"))
  timestamp = headers["webhook-timestamp"].to_s
  return false unless timestamp.match?(/\A\d+\z/) && (Time.now.to_i - timestamp.to_i).abs <= 300

  signed = "#{headers["webhook-id"]}.#{timestamp}.#{raw_body}"
  headers["webhook-signature"].to_s.split(" ").any? do |value|
    next false unless value.start_with?("v1a,")

    key.verify(value.delete_prefix("v1a,").unpack1("m0"), signed)
  rescue ArgumentError, Ed25519::VerifyError
    false
  end
end
```

```csharp C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NSec.Cryptography; // dotnet add package NSec.Cryptography

public static class WebhookVerifier
{
    // headers: the request headers, lower-case names. rawBody: the request body bytes as received.
    public static bool Verify(IDictionary<string, string> headers, byte[] rawBody, string publicKey)
    {
        var raw = Convert.FromBase64String(publicKey["whpk_".Length..]);
        var key = PublicKey.Import(SignatureAlgorithm.Ed25519, raw, KeyBlobFormat.RawPublicKey);
        if (!headers.TryGetValue("webhook-id", out var id)
            || !headers.TryGetValue("webhook-timestamp", out var timestampText)
            || !headers.TryGetValue("webhook-signature", out var signatures)
            || !long.TryParse(timestampText, out var timestamp)
            || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - timestamp) > 300)
        {
            return false;
        }
        var prefix = Encoding.UTF8.GetBytes($"{id}.{timestampText}.");
        var signed = prefix.Concat(rawBody).ToArray();
        return signatures
            .Split(' ')
            .Where(value => value.StartsWith("v1a,"))
            .Any(value =>
            {
                try
                {
                    return SignatureAlgorithm.Ed25519.Verify(key, signed, Convert.FromBase64String(value[4..]));
                }
                catch (FormatException)
                {
                    return false;
                }
            });
    }
}
```

```go Go
package webhooks

import (
    "crypto/ed25519"
    "encoding/base64"
    "math"
    "strconv"
    "strings"
    "time"
)

// headers: the request headers, lower-case names. rawBody: the request body bytes as received.
func VerifyWebhook(headers map[string]string, rawBody []byte, publicKey string) bool {
    raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(publicKey, "whpk_"))
    if err != nil || len(raw) != ed25519.PublicKeySize {
        return false
    }
    timestamp, err := strconv.ParseInt(headers["webhook-timestamp"], 10, 64)
    if err != nil || math.Abs(float64(time.Now().Unix()-timestamp)) > 300 {
        return false
    }
    signed := []byte(headers["webhook-id"] + "." + headers["webhook-timestamp"] + "." + string(rawBody))
    for _, value := range strings.Split(headers["webhook-signature"], " ") {
        if !strings.HasPrefix(value, "v1a,") {
            continue
        }
        signature, err := base64.StdEncoding.DecodeString(value[4:])
        if err == nil && ed25519.Verify(ed25519.PublicKey(raw), signed, signature) {
            return true
        }
    }
    return false
}
```

## Test webhooks

This process describes how to test webhooks in the sandbox environment using the Rebilly API.

1. This step describes how to obtain your website ID, organization ID, and secret API key from your Rebilly sandbox account.
2. This step describes uses an interactive component to create webhook credentials in the sandbox environment.
For more information, see [Create a service credential](/catalog/all/service-credentials/postservicecredential).
  1. Enter your organization ID:
    1. Locate and press `server /`, then in the **Server variables** section, press **Edit s**.
    2. In the **Value** field, enter your organization ID and press **Save**.
  2. Specify that you want to create a service credential of type `webhook`:
    1. Locate and press `{type}`, then press **Edit in path**.
    2. In the **Value** field, enter `webhook`.
  3. Enter your secret API key:
    1. Press **Security**.
    2. In the **API key** field, enter your secret Rebilly sandbox API key. 
For more information, see [Obtain IDs and API keys](/docs/dev-docs/set-up-environment#obtain-ids-and-api-keys).
  4. Press **Send**. 
In the response, take note of the `hash` value.
You will use this value in the [next step](/docs/dev-docs/webhooks-configuration#trigger-a-webhook).
3. This step describes uses an interactive component to trigger a test webhook in the sandbox environment.
For more information, see [Trigger a test webhook](/catalog/all/webhooks/postpreviewwebhook).
  1. Enter your organization ID:
    1. Locate and press `server /`, then in the **Server variables** section, press **Edit**.
    2. In the **Value** field, enter your organization ID and press **Save**.
  2. Enter your secret API key:
    1. Press **Security**.
    2. In the **API key** field, enter your secret Rebilly sandbox API key. 
For more information, see [Obtain IDs and API keys](/docs/dev-docs/set-up-environment#obtain-ids-and-api-keys).
    3. In the **Value** field, enter your secret Rebilly sandbox API key and press **Save**.
  3. Enter your webhook credentials hash:
    1. Press **Body**.
    2. In the **credentialHash** field, enter the hash value you obtained when you created webhook credentials, as described in [Step 2](/docs/dev-docs/webhooks-configuration#create-webhook-credentials).
  4. Press **Send**.


Alternatively, use the Rebilly UI to [test a webhook](/docs/automations/webhooks#test-a-webhook).

If you leave the `eventsFilter` field empty all events are active for the webhook.

## Example event notification

All events are described in the [Rebilly API documentation](https://www.rebilly.com/docs/dev-docs/api/). for an example, see the [invoice paid event](/docs/dev-docs/api/invoices/invoice-paid/).

For more information on Rebilly resources, see [Concepts](https://www.rebilly.com/docs/dev-docs/concepts/).

## Retry policy for webhooks

When a webhook is triggered and an HTTP 200 OK response is not received, the webhook is marked for retry.

In both the sandbox and live environments, the webhook is automatically resent after 30 minutes, and then at exponentially increasing intervals of: 1, 2, 4, 8, and 16 hours, for a total of 6 retry attempts until successful.
If still unsuccessful, it is not attempted again.

In the sandbox environment, if the webhook is unsuccessful after 48 hours and 70 consecutive attempts, the corresponding webhook subscriber is disabled.