Fawaterak Distributor — Admin
Sign in
Downstream integration · for humans

Product-Side Integration Guide

Everything your product needs to receive, verify, and act on events. You talk to the Distributor, never to Fawaterak — and only ever see your own data.

Node.jsPHPPythonC#
verified_user
Verify every webhook Constant-time HMAC over the raw body with your signing secret.
fingerprint
Dedupe on eventId Delivery is at-least-once — make handlers idempotent.
bolt
Acknowledge fast Return 2xx quickly, then process the event asynchronously.
shield
Don't trust payLoad Reconcile money-affecting fields against your own order.

Product-Side Integration Guide

How your product (a web app running behind the Fawaterak Webhook Distributor) integrates with it. This document covers only your side: what you receive, how to verify it, how to respond, and how to create payments so events route back to you.

You never talk to Fawaterak's webhook directly — the Distributor is the single registered webhook and forwards a normalized, signed copy of every event that belongs to you.


1. What the platform team gives you

When your product is registered with the Distributor, you receive these values out-of-band. Store them securely (secret manager / environment variables — never in source control):

Value Example Use
apiKey pk_9f2… (shown once) Your gateway key. Send it as X-Product-Key on every call you make to the Distributor.
signingSecret k7Yc… (shown once) Verifies the signature on every webhook the Distributor sends you.
productId prod_a1b2c3d4e5f6 Identifies your product (for reference; the gateway derives it from your key).
Distributor base URL https://hooks.yourcompany.com The single host you call for everything.

The apiKey and signingSecret are returned only at creation time. If you lose the key, ask the platform team to rotate it (POST /api/products/{id}/rotate-key) — you cannot read it back. Rotating invalidates the old key.

You also tell the platform team your webhookUrl — the public HTTPS endpoint on your side that will receive events (e.g. https://myshop.example/fawaterak-hook).


2. You talk to us, not to Fawaterak

You never call Fawaterak directly and you never hold Fawaterak credentials. The Distributor is your single gateway: it holds the shared merchant account, attaches the right credential to each call, and isolates your data — you can only ever see or act on your own transactions, refunds, and saved cards. Authenticate every call with your X-Product-Key; the gateway figures out your productId from the key, so you never pass it and it can't be spoofed.

Create a payment

POST {DistributorBaseUrl}/api/gateway/transactions
X-Product-Key: pk_9f2…
Content-Type: application/json

{
  "cartTotal": 150.00,
  "cartItems": [
    { "name": "Pro plan", "price": 150.00, "quantity": 1 }
  ],
  "customer": { "first_name": "Sara", "email": "sara@example.com" }
  // add any pay_load fields you want echoed back; productId is added for you
}
// 200 OK
{
  "url":      "https://app.fawaterk.com/checkout/…",  // redirect the buyer here
  "shortUrl": "https://…/c/abc",
  "intentKey": "Asbv2zmnFfdUOOe"                       // = transaction_key
}

Redirect the customer to url (or shortUrl). You'll receive the result later as a webhook (section 3). Routing is automatic — the gateway tagged the payment with your product, so you do not need to pre-declare anything or set pay_load yourself.

Everything else you can do through the gateway

All of these take X-Product-Key and are scoped to you — a request for a transaction, refund, or token that isn't yours returns 403, and lists return only your rows:

Call Purpose
GET /api/gateway/payment-methods List the enabled payment methods.
POST /api/gateway/transactions/data Get one transaction's status ({ "transaction_key": "…" } or transaction_id).
GET /api/gateway/transactions List your transactions.
GET /api/gateway/refunds/types · …/reasons?lang=ar Refund lookups.
POST /api/gateway/refunds Submit a refund (refund_type, refund_id, refundable_amount, reason).
GET /api/gateway/refunds · GET /api/gateway/refunds/{id} List / inspect your refunds.
DELETE /api/gateway/refunds/{id} Cancel a pending refund.
POST /api/gateway/tokenization/card-screen Start saving a card (pass a customerUniqueId).
POST /api/gateway/tokenization/pay · …/pay/recurring Charge a saved-card token.
DELETE /api/gateway/tokenization/tokens Delete a saved-card token.

Each response relays Fawaterak's own status code and JSON body unchanged. A 403 means the resource isn't yours; a 502 means Fawaterak returned an error (its body is echoed under fawaterak).

Saved cards. Call card-screen with a customerUniqueId you choose; open the returned URL for the customer. When the card is saved, the Distributor records the resulting token against your product, so later tokenization/pay calls with that token are authorized only for you.


3. The webhook you will receive

The Distributor sends an HTTP POST to your webhookUrl with Content-Type: application/json and this stable envelope — identical shape for every event type, so you never branch on Fawaterak's raw payloads:

{
  "eventId": 42,                         // unique per event — dedupe on this
  "eventType": "paid",                   // paid | failed | cancel | refund
  "productId": "prod_a1b2c3d4e5f6",
  "transactionId": "28180",              // present for paid/failed/refund
  "transactionKey": "Asbv2zmnFfdUOOe",   // present for paid/failed
  "referenceId": "778586510",            // present for cancel
  "paymentMethod": "Fawry",
  "status": "paid",                      // see status table below
  "amount": 150.00,                      // present for refund
  "currency": "EGP",                     // present for refund
  "payLoad": { "order_id": "ORD-1001" }, // your original pay_load, parsed
  "occurredAt": "2026-06-26T12:00:00+00:00"
}

Null fields are omitted. Only fields relevant to the event type are present (e.g. referenceId appears on cancel; amount/currency on refund). Always null-check optional fields.

Headers

Header Meaning
X-Distributor-Signature sha256=<hex> — HMAC of the body (verify this).
X-Distributor-Timestamp Unix time (seconds) when the delivery was signed.
X-Distributor-Event-Id Same as eventId — convenient for dedupe/logging.

eventType and status values

eventType Typical status What it means
paid paid Payment captured — fulfil the order.
paid pending Async method (Fawry/Aman/Masary) — reference issued, not yet paid. Do not fulfil.
failed failed Payment attempt failed (declined, gateway error).
cancel canceled Async reference expired or was canceled.
refund refunded A refund was processed (amount/currency set).

4. Verify every webhook

Never act on a webhook before verifying its signature. The signature proves it came from the Distributor and (with the timestamp) blocks replays.

Formula:

expected = "sha256=" + hex( HMAC_SHA256( signingSecret, timestamp + "." + rawBody ) )

Rules:

  • Compute over the raw request body bytes — not a re-serialized object (re-serializing changes whitespace/key order and breaks the HMAC).
  • Compare in constant time.
  • Reject if |now - timestamp| exceeds your tolerance (e.g. 5 minutes) to stop replays.

Node.js / Express

import crypto from "node:crypto";
import express from "express";

const SECRET = process.env.DISTRIBUTOR_SIGNING_SECRET;
const app = express();

// IMPORTANT: capture the raw body for HMAC
app.post("/fawaterak-hook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const raw = req.body.toString("utf8");
    const sig = req.get("X-Distributor-Signature") || "";
    const ts  = Number(req.get("X-Distributor-Timestamp") || 0);

    if (Math.abs(Date.now() / 1000 - ts) > 300) return res.sendStatus(401);

    const expected = "sha256=" +
      crypto.createHmac("sha256", SECRET).update(`${ts}.${raw}`).digest("hex");

    const ok = sig.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
    if (!ok) return res.sendStatus(401);

    const event = JSON.parse(raw);
    if (alreadyProcessed(event.eventId)) return res.sendStatus(200); // idempotent
    handleEvent(event);                                              // your logic
    return res.sendStatus(200);                                      // ack fast
  });

PHP (Laravel controller)

public function handle(Request $request)
{
    $raw = $request->getContent();
    $sig = $request->header('X-Distributor-Signature', '');
    $ts  = (int) $request->header('X-Distributor-Timestamp', 0);

    if (abs(time() - $ts) > 300) abort(401);

    $expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $raw, config('services.distributor.secret'));
    if (! hash_equals($expected, $sig)) abort(401);

    $event = json_decode($raw, true);
    if ($this->alreadyProcessed($event['eventId'])) return response('', 200);

    $this->handleEvent($event);
    return response('', 200);
}

Python (Flask)

import hmac, hashlib, time
from flask import request, abort

SECRET = os.environ["DISTRIBUTOR_SIGNING_SECRET"].encode()

@app.post("/fawaterak-hook")
def hook():
    raw = request.get_data()                         # raw bytes
    sig = request.headers.get("X-Distributor-Signature", "")
    ts  = int(request.headers.get("X-Distributor-Timestamp", "0"))

    if abs(time.time() - ts) > 300:
        abort(401)

    mac = hmac.new(SECRET, f"{ts}.".encode() + raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(f"sha256={mac}", sig):
        abort(401)

    event = request.get_json()
    if already_processed(event["eventId"]):
        return "", 200
    handle_event(event)
    return "", 200

C# / ASP.NET Core (minimal API)

app.MapPost("/fawaterak-hook", async (HttpRequest req) =>
{
    using var reader = new StreamReader(req.Body);
    var raw = await reader.ReadToEndAsync();
    var sig = req.Headers["X-Distributor-Signature"].ToString();
    var ts  = long.Parse(req.Headers["X-Distributor-Timestamp"].ToString());

    if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ts) > 300)
        return Results.Unauthorized();

    var mac = Convert.ToHexStringLower(System.Security.Cryptography.HMACSHA256.HashData(
        Encoding.UTF8.GetBytes(secret), Encoding.UTF8.GetBytes($"{ts}.{raw}")));
    if (!CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(sig), Encoding.UTF8.GetBytes($"sha256={mac}")))
        return Results.Unauthorized();

    var ev = JsonSerializer.Deserialize<JsonElement>(raw);
    var eventId = ev.GetProperty("eventId").GetInt64();
    if (AlreadyProcessed(eventId)) return Results.Ok();
    HandleEvent(ev);
    return Results.Ok();
});

5. Respond correctly

  • Return 2xx quickly (ideally after you've persisted the event, before heavy work). Treat your handler as "accept & enqueue", then process asynchronously.
  • Any non-2xx (or a timeout) triggers redelivery. Deliveries are at-least-once: the same event may arrive more than once, so deduplicate on eventId and make your side-effects idempotent.
  • Redelivery schedule (so you know what to expect): retries at roughly 1 min → 5 min → 15 min → 1 h → 3 h → 6 h → 12 h, up to 8 attempts. After that the delivery is dead-lettered and the platform team can replay it once your endpoint is healthy.

6. Handle each event type

paid + paid       → mark the order paid, fulfil, send receipt
paid + pending    → record "awaiting payment"; DO NOT fulfil yet (a paid event follows)
failed            → mark the attempt failed; let the customer retry
cancel            → release any hold / expire the pending order
refund            → reverse fulfilment / update accounting (amount, currency provided)

Reconcile against your own records. Before granting value, re-check the amount and status against the order you created. Treat payLoad contents as untrusted input — it is not covered by Fawaterak's signature; use it only as a correlation hint (e.g. your order_id), never as proof of payment.


7. Security checklist (your side)

  • Serve webhookUrl over HTTPS only.
  • Verify the signature on every request before any side effect.
  • Enforce a timestamp window (≈5 min) to reject replays.
  • Compute the HMAC over the raw body and compare in constant time.
  • Deduplicate on eventId; make handlers idempotent (at-least-once delivery).
  • Keep signingSecret and your API key in a secret store, never in code.
  • Don't trust payLoad; reconcile money-affecting fields against your own order.
  • Return 2xx fast; do heavy work asynchronously.
  • Rotate the signingSecret periodically (coordinate with the platform team).

8. Test your integration locally

Simulate a delivery with a correctly-signed request. This bash snippet signs the body exactly like the Distributor does:

SECRET="your-signing-secret"
TS=$(date +%s)
BODY='{"eventId":1,"eventType":"paid","productId":"prod_test","transactionId":"28180","paymentMethod":"Fawry","status":"paid","payLoad":{"order_id":"ORD-1001"},"occurredAt":"2026-06-26T12:00:00+00:00"}'
SIG="sha256=$(printf '%s' "$TS.$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')"

curl -i -X POST http://localhost:3000/fawaterak-hook \
  -H "Content-Type: application/json" \
  -H "X-Distributor-Signature: $SIG" \
  -H "X-Distributor-Timestamp: $TS" \
  -H "X-Distributor-Event-Id: 1" \
  --data "$BODY"

Expect your endpoint to return 200. Flip a character in SECRET or BODY and confirm it returns 401 — that proves your verification works.


9. Troubleshooting

Symptom Likely cause
You never receive events Payment wasn't created through POST /api/gateway/transactions (so it wasn't tagged with your product), or webhookUrl is wrong/unreachable.
Signature check always fails You're hashing a re-serialized body instead of the raw bytes, or the timestamp isn't included as "{ts}.{body}", or wrong signingSecret.
401 on valid requests Timestamp window too tight, or clock skew between servers — widen the window or sync NTP.
Same order processed twice Not deduping on eventId — delivery is at-least-once.
pending treated as paid Don't fulfil on status: pending; wait for the paid event.
Events stopped after downstream outage Deliveries dead-lettered after 8 attempts — ask the platform team to replay them.

For credentials, replays, or production support, contact the platform team that operates the Distributor.