7feastr
Get Rate & ETAAPITrack ShipmentContactBlog
LoginRegister
Documentation/Logistics API
WebhooksMenu
Overview

Get started

Getting startedAuthenticationSandbox and production

Plan your integration

Coverage & delivery timesPricing

API reference

Quotes & shipmentsTrackingWallet & disputes

Integrate

WebhooksErrors & rate limits

Resources

Changelog
K
Overview

Get started

Getting startedAuthenticationSandbox and production

Plan your integration

Coverage & delivery timesPricing

API reference

Quotes & shipmentsTrackingWallet & disputes

Integrate

WebhooksErrors & rate limits

Resources

Changelog
7feastr

Nigeria's food delivery and logistics infrastructure. Order from your favourite local restaurants, or ship anywhere in the country with 7feastr Logistics.

support@7feastr.com
+234 905 606 2230
Lagos, Nigeria

Company

  • About Us
  • Blog

Services

  • Nearby Restaurants
  • Logistics
  • Track Shipment
  • API Documentation

Food in Kaduna

  • Food Delivery in Kaduna
  • Shawarma
  • Jollof Rice
  • Chicken
  • Suya & Grills
  • Pizza
  • Swallow & Soup

Partners

  • Become an Agent
  • Become an Ambassador

Support

  • FAQ
  • Contact
  • System status
  • Privacy Policy
  • Terms of Service
  • Monetary Policy
  • Refund Policy

© 2026 7feastr Technologies Ltd. All rights reserved.

Integrate

Webhooks

Receive shipment and wallet events, verify their signatures, and handle retries.

Last updated 21 October 2026

Webhooks push events to your server the moment something changes, so you don't have to poll. Add an endpoint in the merchant dashboard under Webhooks, choose the events it receives, and copy its signing secret (whsec_…).

Endpoint requirements

RuleValue
MethodPOST with a JSON body
URLHTTPS in production. HTTP or HTTPS in sandbox
ResponseAny 2xx status within 8 seconds
Webhooks per environmentUp to 10

Sandbox and production webhooks are configured separately and each has its own secret.

Events

EventSent whendata fields
shipment.createdA shipment is created and chargedshipmentId, trackingCode, reference, status, price, pickupBooked
shipment.assignedA rider is assigned to collect the packageshipmentId, trackingCode, status
shipment.picked_upThe package is collected from the sendershipmentId, trackingCode, status
shipment.in_transitThe package is on its wayshipmentId, trackingCode, status
shipment.deliveredThe package is deliveredshipmentId, trackingCode, status, deliveredAt
shipment.failedThe delivery could not be completedshipmentId, trackingCode, status, reason
shipment.cancelledThe shipment is cancelledshipmentId, trackingCode, status, reason
shipment.refundedThe shipment's price is refunded to your walletshipmentId, trackingCode, refundAmount, refundReference
shipment.tracking_updateA new delivery event is reportedshipmentId, trackingCode, update, events
wallet.low_balanceA charge takes your balance to or below your low-balance thresholdbalance, threshold, currency

Subscribe an endpoint to * to receive every event, including any added later.

Payload

Every webhook has the same body:

FieldDescription
idUnique event id (evt_…). It stays the same on every retry of that event, so use it to ignore duplicates
eventThe event name
dataThe event details
createdAtWhen the event happened. Use it to ignore an update older than one you have already applied
sentAtWhen this delivery attempt was sent
JSON
{
  "id": "evt_8f3c2a1d9b7e4f6a8c0d1e2f3a4b5c6d",
  "event": "shipment.delivered",
  "data": {
    "shipmentId": "6716a3f2c9e77b0012a4d5e1",
    "trackingCode": "7F-20261021-4F91A2C8",
    "status": "delivered",
    "deliveredAt": "2026-10-22T08:52:37.000Z"
  },
  "createdAt": "2026-10-22T08:52:37.106Z",
  "sentAt": "2026-10-22T08:52:38.214Z"
}

Each request also carries these headers:

HeaderDescription
X-7feastr-SignatureHMAC-SHA256 signature of the raw body (see below)
X-7feastr-EventThe event name
X-7feastr-Event-IdSame as id in the body
X-7feastr-Delivery-Attempt1 for the first attempt, 2 for the first retry, and so on

shipment.created tells you whether the pickup was booked straight away:

JSON
{
  "event": "shipment.created",
  "data": {
    "shipmentId": "6716a3f2c9e77b0012a4d5e1",
    "trackingCode": "7F-20261021-4F91A2C8",
    "reference": "SHIP-9F3A1B2C4D5E6F70",
    "status": "pending",
    "price": 8492.5,
    "pickupBooked": true
  },
  "sentAt": "2026-10-21T09:32:11.530Z"
}

shipment.tracking_update

Sent each time our delivery network reports progress, such as reaching a sorting centre, even when status has not changed. data.update is the newest event and data.events is the full list, newest first:

JSON
{
  "event": "shipment.tracking_update",
  "data": {
    "shipmentId": "6716a3f2c9e77b0012a4d5e1",
    "trackingCode": "7F-20261021-4F91A2C8",
    "update": { "description": "Received at sorting centre", "at": "2026-10-21T13:47:19.000Z" },
    "events": [
      { "description": "Received at sorting centre", "at": "2026-10-21T13:47:19.000Z" },
      { "description": "Picked up", "at": "2026-10-21T11:20:03.000Z" },
      { "description": "Rider assigned for pickup", "at": "2026-10-21T10:05:42.000Z" },
      { "description": "Pickup booked", "at": "2026-10-21T09:32:11.000Z" }
    ]
  },
  "sentAt": "2026-10-21T13:47:21.904Z"
}

Show descriptions to your customer as they are, and branch your logic on the lifecycle events (shipment.picked_up, shipment.delivered and so on), not on the wording.

shipment.refunded

JSON
{
  "event": "shipment.refunded",
  "data": {
    "shipmentId": "6716a3f2c9e77b0012a4d5e1",
    "trackingCode": "7F-20261021-4F91A2C8",
    "refundAmount": 8492.5,
    "refundReference": "shipment_refund_6716a3f2c9e77b0012a4d5e1"
  },
  "sentAt": "2026-10-21T09:41:28.077Z"
}

wallet.low_balance

JSON
{
  "event": "wallet.low_balance",
  "data": { "balance": 9500, "threshold": 10000, "currency": "NGN" },
  "sentAt": "2026-10-21T16:03:12.640Z"
}

Verify the signature

Every webhook carries an X-7feastr-Signature header: the hex-encoded HMAC-SHA256 of the raw request body, keyed with your webhook's signing secret (whsec_…).

Warning

Always verify the signature before you trust a webhook. Anyone can send a request to your URL. Compute the HMAC over the raw bytes you received, before any JSON parsing, and compare it in constant time.

// Node.js 18+ with Express
import crypto from "node:crypto";
import express from "express";

const app = express();
const WEBHOOK_SECRET = process.env.SEVENFEASTR_WEBHOOK_SECRET; // whsec_...

function isValidSignature(rawBody, signature, secret) {
  if (typeof signature !== "string") return false;
  const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(signature, "utf8");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// express.raw keeps the exact bytes that were signed
app.post("/webhooks/7feastr", express.raw({ type: "application/json" }), (req, res) => {
  if (!isValidSignature(req.body, req.get("X-7feastr-Signature"), WEBHOOK_SECRET)) {
    return res.status(401).send("Invalid signature");
  }

  const { event, data } = JSON.parse(req.body.toString("utf8"));
  if (event === "shipment.delivered") {
    console.log(`Shipment ${data.trackingCode} delivered at ${data.deliveredAt}`);
  }

  // Acknowledge quickly; do slow work in a background job
  return res.sendStatus(200);
});

app.listen(3000);
# Python 3 with Flask
import hashlib
import hmac
import json
import os

from flask import Flask, abort, request

app = Flask(__name__)
WEBHOOK_SECRET = os.environ["SEVENFEASTR_WEBHOOK_SECRET"]  # whsec_...


def is_valid_signature(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature or "")


@app.post("/webhooks/7feastr")
def sevenfeastr_webhook():
    raw_body = request.get_data()  # the exact bytes that were signed
    if not is_valid_signature(raw_body, request.headers.get("X-7feastr-Signature", ""), WEBHOOK_SECRET):
        abort(401)

    payload = json.loads(raw_body)
    if payload["event"] == "shipment.delivered":
        print(f"Shipment {payload['data']['trackingCode']} delivered")

    # Acknowledge quickly; do slow work in a background job
    return "", 200

To test your verification code, compute the signature yourself with OpenSSL:

Shell
printf '%s' '{"event":"shipment.delivered","data":{"trackingCode":"7F-20261021-4F91A2C8"},"sentAt":"2026-10-22T08:52:38.214Z"}' \
  | openssl dgst -sha256 -hmac "$SEVENFEASTR_WEBHOOK_SECRET" -hex

Retries and auto-disable

Every event is saved before it is sent, so deliveries survive restarts and deploys on our side and are never dropped. Delivery is at least once: an event can occasionally arrive more than once, always with the same id.

A delivery fails when your endpoint doesn't answer with a 2xx status within 8 seconds. Failed deliveries are retried, for 6 attempts over about 8.5 hours:

AttemptWhen
1Immediately
230 seconds later
35 minutes later
430 minutes later
52 hours later
66 hours later

After 10 consecutive failures the webhook is disabled so a broken endpoint does not pile up retries. Fix the endpoint, then re-enable the webhook in the dashboard. The dashboard shows each webhook's latest delivery result, and you can send a test event at any time.

Best practices

  • Respond fast. Return 200 as soon as the signature checks out, then process the event in a background job.
  • Handle duplicates. Store each id you have processed and skip any event whose id you have already seen.
  • Don't rely on arrival order. A retried event can arrive after a newer one. Compare createdAt with the last update you applied for that shipment and ignore older ones, or fetch GET /shipments/:id for the current state.
  • Statuses only move forward. pending → assigned → picked_up → in_transit → delivered (or failed / cancelled). Each status change is sent once, and shipment.tracking_update carries the finer progress steps in between.
  • Keep the secret secret. Store whsec_… like a password. If it leaks, delete the webhook and create a new one.
PreviousWallet & disputesNext Errors & rate limits

On this page

  • Endpoint requirements
  • Events
  • Payload
  • shipment.tracking_update
  • shipment.refunded
  • wallet.low_balance
  • Verify the signature
  • Retries and auto-disable
  • Best practices