Webhook
When you create an integration with Sibill, you may be interested in receiving events as they occur on the entities in your Sibill accounts, so that your backend systems can take action accordingly.
How to use it
To start receiving events in your app:
- Create a webhook endpoint to receive events via
HTTPSPOSTrequests - Request webhook registration with Sibill
- Secure the endpoint
A webhook endpoint is a destination on your server that receives requests from Sibill.
Add a new endpoint to your server and make sure it is publicly accessible, so we can send unauthenticated POST requests to it.
Also make sure there are no redirects from the destination on your server to other destinations. If a redirect occurs at any point, the destination will be identified as invalid.
1 Create an endpoint to respond to the webhook
Create an HTTPS service that can receive requests using the POST method.
The service must return a 2xx status code response as quickly as possible and then execute business logic to avoid potential timeouts.
Example
- PHP
- Elixir
// Set the content type to application/json
header('Content-Type: application/json');
// Get the raw POST data
$json = file_get_contents('php://input');
// Decode the JSON data
$data = json_decode($json, true);
// Prepare the response array
$response = [];
// Check for JSON decoding errors
if (json_last_error() !== JSON_ERROR_NONE) {
// If there's an error, include it in the response
$response['error'] = 'Invalid JSON';
} else {
// If the JSON is valid, include the parsed data
$response['received'] = $data;
}
// Always return a 200 status code
http_response_code(200);
echo json_encode($response);
defmodule MyApp.PostHandler do
@moduledoc"""
Make sure to add the `jason` dependency to `mix.exs`.
"""
import Plug.Conn
@behaviour Plug
def init(default), do: default
def call(conn, _opts) do
# Check if the request method is POST
if conn.method == "POST" do
# Read the request body
{:ok, body, conn} = read_body(conn)
# Process the body (for example, parse JSON)
case Jason.decode(body) do
{:ok, params} ->
# Send a response
conn
|> put_resp_content_type("application/json")
|> send_resp(200, "")
{:error, _reason} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(400, Jason.encode!(%{error: "Invalid JSON"}))
end
else
conn |> send_resp(404, Jason.encode!(%{error: "Not found"}))
end
end
end
# Add the plug to the router
# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :api do
plug :accepts, ["json"]
plug MyApp.PostHandler # Add your custom Plug here
end
scope "/api", MyAppWeb do
pipe_through :api
end
end
2 Register your endpoint
At the moment, registration must be requested by sending an email to api@sibill.it. In this request you will need to provide the HTTPS address of the endpoint that will respond to the calls.
3 Secure the endpoint
It is essential to ensure that the integration takes place in complete security, so that you can be certain that the requests received actually originate from Sibill. For this reason it is necessary to verify the signatures of the received requests.
Some examples are provided below.
Manually verify the webhook signature
You can create a custom solution by following this procedure or follow the examples below.
The X-Sibill-Signature header included in every signed event contains a timestamp and a signature that you must verify.
The timestamp has a t= prefix and each signature has a scheme prefix. Currently, the only valid scheme for signatures is v1.
X-Sibill-Signature: t=1492774577, v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Sibill generates signatures using a hash-based message authentication code (HMAC) with SHA-256. To prevent downgrade attacks, ignore all schemes other than v1.
To create a manual solution for signature verification, you need to complete the following steps:
Step 1: extract the timestamp and signature from the header
To get a list of elements, split the header using the , character as a separator. Then split each element using the = character as a separator to get a prefix and value pair. The value of the t prefix corresponds to the timestamp and v1 corresponds to the signature. You can ignore all other elements.
Step 2: prepare the signed_payload string
The signed_payload string is created by concatenating:
- The timestamp (as a string)
- The
.character - The actual JSON payload, i.e. the request body
Step 3: determine the expected signature
Compute an HMAC with the SHA256 hash function. Use the endpoint's private signing key as the key and the signed_payload string as the message.
Step 4: compare the signatures
Compare the signature in the header with the expected signature. For an equality match, calculate the difference between the current timestamp and the received one, then decide whether the difference falls within the tolerance. To protect against timing attacks, use a constant-time string comparison to compare the expected signature with each received signature.
Signature validation example
- PHP
- Elixir
abstract class WebhookSignature
{
public static function verifyHeader($payload, $header, $secret)
{
// Extract timestamp and signatures from header
$timestamp = self::getTimestamp($header);
$signatures = self::getSignatures($header, 'v1');
if (-1 === $timestamp || empty($signatures)) {
return false;
}
// Check if expected signature is found in list of signatures from header
$signedPayload = "{$timestamp}.{$payload}";
$expectedSignature = self::computeSignature($signedPayload, $secret);
$signatureFound = false;
foreach ($signatures as $signature) {
if (\hash_equals($expectedSignature, $signature)) {
$signatureFound = true;
break;
}
}
return $signatureFound;
}
/**
* Extracts from the headers a particular header given a key name.
*
* @param string $headers the headers of the request
* @param string $header the header to be extracted
*
* @return the string value of the header
*/
private static function getHeader($headers, $header)
{
$items = \explode(',', $headers);
$headerValues = [];
foreach ($items as $item) {
$itemParts = \explode('=', $item, 2);
if (\trim($itemParts[0]) === $header) {
$headerValues[] = $itemParts[1];
}
}
return $headerValues;
}
private static function getTimestamp($header)
{
$res = self::getHeader($header, 't');
if (\count($res) != 1) {
return -1;
}
if (!\is_numeric($res[0])) {
return -1;
}
return (int) $res[0];
}
private static function getSignatures($header, $scheme)
{
return self::getHeader($headers, $scheme);
}
private static function computeSignature($payload, $secret)
{
return \hash_hmac('sha256', $payload, $secret);
}
}
defmodule SibillWebhookVerifier do
@doc """
Verifies the Sibill signature for a webhook call.
Args:
- `payload`: The payload of the webhook call as a string.
- `sig_header`: The value of the `X-Sibill-Signature` header.
- `secret`: Your Sibill secret key.
Returns:
- `:ok` if the signature is valid.
- `:error` if the signature is invalid.
"""
def verify_signature(payload, sig_header, secret) do
# Split the signature header into its components
components = String.split(sig_header, ",")
# Extract the timestamp and signature
{timestamp, signature} =
Enum.find(components, fn component ->
String.starts_with?(component, "t=")
end)
|> String.split("=")
|> then(fn [_, timestamp] -> timestamp end)
{signature, _} =
Enum.find(components, fn component ->
String.starts_with?(component, "v1=")
end)
|> String.split("=")
|> then(fn [_, signature] -> signature end)
# Generate the expected signature
message = "#{timestamp}.#{payload}"
expected_signature =
:crypto.mac(:hmac, :sha256, secret, message)
|> Base.encode16()
|> String.downcase()
# Compare the expected signature with the actual signature
if signature == expected_signature do
:ok
else
:error
end
end
end
Event delivery behaviours
This section helps you understand the various behaviours to expect regarding how Sibill sends events to the webhook endpoint.
Automatic retries
Sibill attempts to deliver events to your destination for a maximum of two days with a three-hour delay between each retry.
Event ordering
Sibill does not guarantee delivery of events in the order in which they were generated in the system. Be prepared to handle delivery appropriately. You can also use the API to retrieve any missing objects or to reconstruct a state that appears inconsistent.
Webhook best practices
This section lists some best practices for protecting webhook endpoints and making sure they work well with your Sibill integration.
Handle duplicate events
Webhook endpoints might occasionally receive the same event more than once. To protect yourself from receiving duplicate events, handle the event and then do not process already-recorded events; you can rely on various fields including updated_at or infer the validity of the event from the entity's state.
Listen only for the event types required by your integration
Configure your webhook endpoints to receive only the event types required by your integration and ignore others. Listening to all events unnecessarily burdens your server and is therefore not recommended.
Handle events asynchronously
Configure your system to process incoming events with an asynchronous queue. If you choose to process events synchronously, scalability issues may arise. A sharp increase in webhook deliveries (for example, at the end of the month, depending on the frequency of your business) can overload endpoint hosts.
Receive events with an HTTPS server
To receive events from the Sibill webhook, your server must be correctly configured to support the HTTPS protocol and must have a valid certificate.
Prevent replay attacks
A replay attack occurs when an attacker intercepts a valid payload and its signature and then retransmits them. To mitigate these attacks, Sibill includes a timestamp in the X-Sibill-Signature header. Since the timestamp is part of the signed payload, it is also verified by the signature: an attacker therefore cannot modify the timestamp without invalidating the signature. If the signature is valid but the timestamp is too old, you can configure your application to reject the payload.
Return a 2xx response quickly
The endpoint must quickly return a successful status code (2xx) before the execution of any complex logic could cause a timeout. For example, you must return a 200 response before the receiving system performs any operation.