Skip to content
GLORESEND

Guides

Webhooks

GLORESEND sends signed HTTPS POST requests to your endpoints when events happen in an environment.

Event types

  • transfer.succeeded — A transfer settled successfully.
  • transfer.failed — A transfer failed. Reserved funds were released back to the available balance.
  • card.created — A card was issued.
  • card.funded — Funds were loaded onto a card.
  • card.withdrawn — Funds were moved from a card back to your wallet.
  • card.status_changed — A card was frozen, unfrozen or terminated.
  • test.ping — Sent when you press “Send test event” in the dashboard.

Payload

POST to your endpoint
{
  "id": "evt_9b8c7d6e5f4a3b2c1d0e",
  "object": "event",
  "type": "transfer.succeeded",
  "environment": "sandbox",
  "test_data": true,
  "created_at": "2026-10-15T09:30:00.130Z",
  "data": {
    "id": "tr_4k2m9q8w7e6r5t3y",
    "object": "transfer",
    "status": "succeeded",
    "amount": 125000,
    "currency": "XOF",
    "…": "…"
  }
}

Each request also carries these headers:

  • Gloresend-Signature — t=<unix seconds>,v1=<hex HMAC-SHA256>
  • Gloresend-Event-Id and Gloresend-Delivery-Id

Verifying signatures

The signature is an HMAC-SHA256 of <t>.<raw request body> using the endpoint’s signing secret. Compare in constant time and reject timestamps more than 300 seconds old to prevent replays.

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

const app = express();
const secret = process.env.GLORESEND_WEBHOOK_SECRET; // whsec_…

// Use the raw body: re-serialized JSON will not match the signature.
app.post("/webhooks/gloresend", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("Gloresend-Signature") ?? "";
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const timestamp = Number(parts.t);
  const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${req.body}`).digest("hex");

  const fresh = Math.abs(Date.now() / 1000 - timestamp) <= 300;
  const valid =
    fresh &&
    typeof parts.v1 === "string" &&
    parts.v1.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
  if (!valid) return res.status(400).send("invalid signature");

  const event = JSON.parse(req.body.toString("utf8"));
  // Deduplicate on event.id — the same event can be delivered more than once.
  res.sendStatus(200);
  queueForProcessing(event);
});

Delivery and retries

  • Respond with any 2xx status within 10 seconds. Redirects are not followed.
  • Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours — six attempts in total — then marked failed. You can retry any failed delivery from the dashboard.
  • Deliveries may arrive out of order or more than once. Make your handler idempotent using the event id.
  • Sandbox and Live endpoints are configured separately and only receive events from their own environment.

Endpoint requirements

Endpoint URLs must use HTTPS and resolve to public IP addresses. Private, loopback, link-local (including cloud metadata) and internal hostnames are rejected when the endpoint is saved and again at delivery time.

The signing secret is shown once when the endpoint is created and can be rotated at any time; GLORESEND stores it encrypted.