# How to Configure Webhooks
Set up a webhook endpoint, register it in the Forage dashboard, and verify signatures
To start receiving webhooks from Forage, complete the following steps. (See [Webhook Events](./webhooks-event-reference.md) for available events and payload schemas.)
**To configure a webhook:**
1. Set up your receiving endpoint
2. Add the webhook to the Forage dashboard
3. Verify the webhook signature
4. Verify the webhook timestamp
## Step 1: Set up your receiving endpoint
Your endpoint should be publicly available and accept a POST request with a JSON payload. Confirm that your endpoint returns a `200` when you receive the webhook. If the `200` isn't received, then Forage retries the webhook with an exponential backoff.
### Example endpoints
```python
# Python Django Example
@api_view(http_method_names=["POST"])
def handle_webhook(request):
event = request.data
received_signature = request.headers.get("Webhook-Signature")
# Validate signature (more instructions in Step 3)
event_type = event["type"]
# Handle the event
if event_type == "ORDER_STATUS_UPDATED":
# do something with order
order_ref = event["data"]["order_ref"]
pass
elif event_type == "PAYMENT_STATUS_UPDATED":
payment_ref = event["data"]["payment_ref"]
pass
elif event_type == "REFUND_STATUS_UPDATED":
refund_ref = event["data"]["refund_ref"]
pass
# Return a 200 response to acknowledge receipt of the event
return HttpResponse(status=200)
```
```java
// Java Spring Example
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class WebhookController {
@PostMapping("/mywebhookendpoint")
public ResponseEntity handleRequest(@RequestBody MyRequestData requestData, HttpServletRequest request) {
String received_signature = request.getHeader("Webhook-Signature");
// Validate signature (more instructions in Step 3)
String eventType = requestData.getType();
// Handle the event
switch (eventType) {
case "ORDER_STATUS_UPDATED": {
break;
}
case "PAYMENT_STATUS_UPDATED": {
break;
}
case "REFUND_STATUS_UPDATED": {
break;
}
default:
System.out.println("Webhook Event is unhandled");
}
// Return a 200 response to acknowledge receipt of the event
String responseBody = "Processed Webhook";
return new ResponseEntity<>(responseBody, HttpStatus.OK);
}
}
```
```javascript Node
// Node Example
const express = require('express');
const app = express();
app.post('/mywebhookendpoint', express.raw({type: 'application/json'}), (request, response) => {
const signature = request.headers['Webhook-Signature'];
// Validate signature (more instructions in Step 3)
let event;
event = request.body
// Handle the event
switch (event.type) {
case 'ORDER_STATUS_UPDATED':
const orderRef = event.data.order_ref;
// Handle order ref
break;
case 'PAYMENT_STATUS_UPDATED':
const paymentRef = event.data.payment_ref
// Handle payment ref
break;
case 'REFUND_STATUS_UPDATED':
const refundRef = event.data.refund_ref
// Handle refund ref
break;
default:
console.log(`Event type ${event.type} not handled`);
}
// Return a 200 response to acknowledge receipt of the event
response.send();
});
```
```ruby
require 'json'
require 'sinatra'
post '/webhook' do
event = JSON.parse(request.body.read)
received_signature = request.env['Webhook-Signature']
# Validate signature (more instructions in Step 3)
event_type = event['type']
# Handle the event
case event_type
when 'ORDER_STATUS_UPDATED'
# do something with order
order_ref = event['data']['order_ref']
when 'PAYMENT_STATUS_UPDATED'
payment_ref = event['data']['payment_ref']
when 'REFUND_STATUS_UPDATED'
refund_ref = event['data']['refund_ref']
end
# Return a 200 response to acknowledge receipt of the event
status 200
end
```
### Always return `2XX`
Your endpoint should return `2XX` for any well-formed payload, whether or not you act on the data.
## Step 2: Add the webhook to the Forage dashboard
Navigate to "Webhooks" in the Forage dashboard. From the Webhooks page, click "+Add webhook" and provide the requested information:
* **Webhook URL**: The endpoint that receives the webhook (you set this up in [Step 1](#step-1-set-up-your-receiving-endpoint))
* **Status**: Whether the webhook is "Active", currently being listened for, or "Inactive"
* **Subscribed Events**: The Forage webhook events to listen for
* **Notification Email**: An email address that Forage can use to contact you in case of webhook errors
## Step 3: Verify the webhook signature
Forage uses an HMAC with SHA-256 to sign its webhooks. The signature is passed in the webhook payload `Webhook-Signature` header, for example:
`Webhook-Signature: be521964c21a8eb7f5ddd0f45b5bf83d8904a4dd4238b7b14f3eee73fa9c21f2`
As a security measure, Forage requires that you validate the `Webhook-Signature` value against a signature that you calculate using the webhook's secret from the Forage dashboard. This verifies that the webhook message came from Forage.
To calculate the signature, retrieve the webhook's secret from the Forage dashboard and convert the webhook payload to a JSON string. Pass the `secret` as the key and the stringified payload as the `message` to a hash function to compute the HMAC with SHA-256. Finally, compare the signature that you computed with the `Webhook-Signature`.
```mermaid
flowchart TD
A[Receive webhook POST] --> B[Extract Webhook-Signature header]
B --> C[Get webhook secret from Forage dashboard]
C --> D[Compute HMAC-SHA256\nsecret + raw request body]
D --> E{Constant-time compare:\ncomputed == received?}
E -->|Match| F[Process webhook payload]
E -->|No match| G[Reject: discard payload]
```
### Example comparison
> ⚠️ **Use the raw request body**
>
> Forage calculates the signature using the webhook's raw request body. To avoid miscalculating the signature, make sure that any framework that you're using doesn't manipulate the request body. For example, you might need to skip parsing if the framework removes whitespaces (e.g. Express.js).
> ⚠️ **Use constant-time comparison**
>
> Use a constant-time string comparison to compare the two signatures to avoid timing attacks.
```python
import hashlib
import hmac
def get_hmac_string(secret, message):
return hmac.new(
secret.encode("utf-8"),
message.encode("utf-8"),
hashlib.sha256,
).hexdigest()
# Django example
@api_view(http_method_names=["POST"])
def webhook_view(request):
webhook_payload = request.body # ie. "{\"ref\": \"6ce5bdb204\", \"created\": \"2023-06-23T18:48:13.791077+00:00\", \"type\": \"ORDER_STATUS_UPDATED\", \"data\": {\"order_ref\": \"3b96a5312a\", \"status\": \"canceled\", \"snap_total\": \"20.00\", \"ebt_cash_total\": \"20.00\", \"remaining_total\": \"0.00\"}}"
my_secret = "wh_secretabc123"
# Compute expected signature
expected_signature = get_hmac_string(my_secret, webhook_payload)
received_signature = request.headers.get("Webhook-Signature")
# Assert that computed signature above equals Webhook-Signature header using constant-time string comparison
if not hmac.compare_digest(expected_signature, received_signature):
raise SignatureError()
```
```java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.MessageDigest;
public class WebhookHandler {
public static String getHmacString(String secret, String message) throws NoSuchAlgorithmException, InvalidKeyException {
Mac sha256Hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
sha256Hmac.init(secretKey);
byte[] hmacBytes = sha256Hmac.doFinal(message.getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte hmacByte : hmacBytes) {
String hex = Integer.toHexString(0xff & hmacByte);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
}
public static boolean constantTimeEquals(String str1, String str2) throws NoSuchAlgorithmException {
byte[] digest1 = getDigest(str1);
byte[] digest2 = getDigest(str2);
// Compare the digests using MessageDigest.isEqual()
return MessageDigest.isEqual(digest1, digest2);
}
// Spring Boot example
@PostMapping("/webhook")
public void webhookHandler(@RequestBody String webhookPayload, @RequestHeader("Webhook-Signature") String receivedSignature) {
String mySecret = "wh_secretabc123";
try {
// Compute expected signature
String expectedSignature = getHmacString(mySecret, webhookPayload);
// Assert that computed signature above equals Webhook-Signature header using constant-time string comparison
if (!constantTimeEquals(expectedSignature, receivedSignature)) {
throw new SignatureError();
}
// Process the webhook payload
// ...
} catch (Exception e) {
// Handle the exception
e.printStackTrace();
}
}
}
```
```javascript Node
const crypto = require('crypto');
function getHmacString(secret, message) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(message, 'utf8');
return hmac.digest('hex');
}
// Express example
app.post('/webhook', (req, res) => {
const webhookPayload = req.body.toString(); // assuming the request body contains the JSON payload
const mySecret = 'wh_secretabc123';
try {
// Compute expected signature
const expectedSignature = getHmacString(mySecret, webhookPayload);
const receivedSignature = req.headers['webhook-signature'];
// Assert that computed signature above equals Webhook-Signature header using constant-time string comparison
if (!crypto.timingSafeEqual(Buffer.from(expectedSignature, 'hex'), Buffer.from(receivedSignature, 'hex'))) {
throw new Error('SignatureError');
}
// Process the webhook payload
// ...
res.sendStatus(200);
} catch (error) {
// Handle the error
res.status(500).send(error.message);
}
});
```
```ruby
require 'openssl'
require 'base64'
require 'sinatra'
def get_hmac_string(secret, message)
digest = OpenSSL::Digest.new('sha256')
hmac = OpenSSL::HMAC.hexdigest(digest, secret, message)
hmac
end
# Sinatra example
post '/webhook' do
webhook_payload = request.body.read
my_secret = 'wh_secretabc123'
# Compute expected signature
expected_signature = get_hmac_string(my_secret, webhook_payload)
received_signature = request.env['HTTP_WEBHOOK_SIGNATURE']
# Assert that computed signature above equals Webhook-Signature header using constant-time string comparison
unless Rack::Utils.secure_compare(expected_signature, received_signature)
raise 'SignatureError'
end
# Process the webhook payload
# ...
status 200
end
```
## Step 4: Verify timestamp
To further validate the webhook, compare the `created` value in the payload against your system's timestamp for the corresponding resource. For example, confirm that the `created` value of a `PAYMENT_STATUS_UPDATED` webhook and the timestamp for when the payment was created in your system are within a few seconds, or whatever your risk tolerance is.
## Related documentation
* [How Forage Webhooks Work](./webhooks-concepts.md). What webhooks are, when events fire, and how Forage delivers them.
* [Reference: Webhook Events](./webhooks-event-reference.md). Specs for all event types, payload schemas, field definitions, and IP addresses.
* [Errors (API Reference)](https://docs.joinforage.app/reference/errors). Full error response schema and general API error codes.