Fortis webhooks on your own server
This guide explains how to set up direct reception of Fortis webhooks on your server, without using third-party platforms (Zapier, Make, etc.). You will create a public HTTPS endpoint, verify incoming events using digital signatures, and process them as you wish.
For the full technical reference, see: Fortis API Documentation
1. Server Requirements
- Public HTTPS address – Fortis sends requests only to TLS-protected URLs (self-signed certificates are not accepted).
- Programming language – any (e.g., Python, Node.js, PHP, Ruby). You only need to accept POST requests, read headers and body, and compute HMAC-SHA256.
- Access to Fortis merchant portal with Owner or Administrator role.
- Tariff that includes webhooks (General Full or F&B Full).
2. Setting Up an Endpoint in Fortis merchant portal
- Log in to your Fortis merchant portal (test or production environment).
- Go to Settings → Developer tools → Webhooks.
- Click Add endpoint and fill in the fields:
| Field | Value |
|---|---|
| Endpoint URL | The public HTTPS address of your endpoint (e.g., https://your-server.com/webhook). |
| Event types | Select the events you want to be notified about (e.g., payment, transaction, kyc). |
- After saving the endpoint, your secret key will be displayed. Copy and store it securely, as it cannot be viewed again; you can only generate a new one. This key is used to verify signatures.
- Click the "Test" button. Fortis will immediately send a test event to your endpoint. This helps verify your setup early.
Note If you see a "Verification failed" status, don't worry. It means that Fortis did not receive a special echo response from your server (see section 5). However, the endpoint is active, and events will be delivered.
3. Incoming Webhook Structure
Fortis sends a POST request to your URL with a JSON body and a set of required headers.
Headers
| Header | Description |
|---|---|
| X-Fortis-Signature | Event's digital signature (string like sha256=...). |
| X-Fortis-Timestamp | Sending time as Unix timestamp (seconds since epoch). |
| X-Fortis-Event-Type | Event type (e.g., sale.completed). |
Body
Example body (may differ depending on the event):
{
"event": "sale.completed",
"event_id": "05c88a0c-...",
"occurred_at": "2026-07-07T09:53:46.445Z",
"version": "2026-06-01",
"data": {
"cursor": "aQvigECEB7y9rBL9ObjlA0z0KRmGgHmUPNkQ",
"items": [
{
"id": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
"number": "0006-0973-152658",
"ticket_number": "T1-4",
"status": "completed",
"initiated_at": "2026-10-06T10:33:07.155Z",
"comment": "Deliver to the address",
"point_of_sale_id": "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
"initiated_by_id": "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
"external_data": {
"id": "external-system-id-001"
},
"positions": [
{
"id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
"name": "Grande Latte",
"quantity": {
"value": 500,
"exponent": 3,
"uom_id": "33333333-3333-4333-8333-333333333333",
"uom_name": "Kilogram",
"uom_symbol": "kg"
},
"codes": [
{
"type": "ean_8",
"value": "string"
}
],
"catalog_good_id": "11111111-1111-4111-8111-111111111111",
"catalog_good_variation_id": "22222222-2222-4222-8222-222222222222",
"source": "catalog",
"vat": {
"fee_item_id": "66666666-6666-4666-8666-666666666666",
"fee_item_name": "5%",
"fee_item_percentage": 5
},
"unit_price_including_tax": {
"value": 50000,
"currency_code": "AED"
},
"total_price": {
"value": 50000,
"currency_code": "AED"
},
"discount_amount": null,
"taxable_amount": {
"value": 47619,
"currency_code": "AED"
},
"tax_percentage": 5
}
],
"totals": {
"discount": null,
"taxable_amount": {
"value": 50000,
"currency_code": "AED"
},
"tax": {
"value": 2500,
"currency_code": "AED"
},
"total": {
"value": 52500,
"currency_code": "AED"
}
},
"created_at": "2026-10-06T10:33:07.155Z",
"updated_at": "2026-10-06T10:33:07.155Z"
}
]
}
}4. Signature Verification (Protection Against Forgeries)
The signature ensures that the request truly came from Fortis and was not altered in transit. Verification algorithm:
- Extract the values of X-Fortis-Signature (signature) and X-Fortis-Timestamp (timestamp) from headers.
- Get the raw request body as a string.
- Build the signed payload – concatenation of timestamp + '.' + raw_body (dot as separator).
- Compute HMAC-SHA256 of this payload using your secret key (obtained when creating the endpoint).
- Convert the resulting hash to hexadecimal and prepend 'sha256='.
- Compare this value with the X-Fortis-Signature header (use a time-constant comparison function).
- Optionally – check that the timestamp is not older than 5 minutes (protection against replay attacks).
Example Code in Python (Flask)
Below is a complete handler. Copy it into your project, replacing SECRET with your actual key (store it in environment variables).
import hmac
import hashlib
import time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = b'your_secret_key' # replace with the real one
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Fortis-Signature')
timestamp = request.headers.get('X-Fortis-Timestamp')
raw_body = request.data.decode('utf-8')
if not signature or not timestamp:
abort(400, 'Missing headers')
# Check timestamp is not older than 5 minutes
try:
ts = int(timestamp)
if abs(time.time() - ts) > 300:
abort(400, 'Timestamp too old')
except ValueError:
abort(400, 'Invalid timestamp')
# Compute expected signature
payload = (timestamp + '.' + raw_body).encode('utf-8')
expected = 'sha256=' + hmac.new(SECRET, payload, hashlib.sha256).hexdigest()
# Constant-time comparison
if not hmac.compare_digest(expected, signature):
abort(403, 'Invalid signature')
# Signature is valid – process the event
print(f"Received event: {raw_body}")
# Your business logic here
return '', 2005. Handling Verification Requests (Optional)
When you create an endpoint, Fortis may send a verification request to ensure your endpoint exists and responds correctly. This request usually includes the header X-Fortis-Verification. Your server should return the same header or echo the request body. The exact mechanism depends on your Fortis version. If you do not implement this, the merchant portal may show a "verification failed" status, but it does not affect the event delivery.
6. Testing and Debugging
- Use the "Test" button in the Fortis panel to send a sample event.
- Check your server logs – confirm that the request is received, signature passes.
- If your server does not respond with 200, Fortis will retry with exponential backoff (up to several days). You can check delivery history in your merchant portal.
7. Security Recommendations
- Store the secret key in environment variables, never hard-code it.
- Always verify the signature – never trust unsigned requests.
- Use HTTPS with a valid certificate.
- Limit request body size and processing timeouts.