Webhooks

Webhooks

Century consumes signed webhooks from its payment provider to settle money movement. Webhooks are inbound only — Century does not currently deliver events to third-party endpoints.

Endpoint

POST https://centuryvtu.com/api/public/payments/webhook/paystack
  • Public route — it must be reachable without a session, so it authenticates the caller by signature.
  • Always responds 200 once the signature is valid, even for events Century ignores, so the provider stops retrying.
  • Processing is idempotent: replaying an event for a reference already settled changes nothing.

Security

Verify before you parse
Century computes an HMAC SHA-512 of the raw request body using the provider secret and compares it in constant time against the signature header. Only then is the JSON parsed. Never trust an unverified body, and never authenticate a webhook by IP alone.
Verification (Node.js)
import crypto from "node:crypto";

const raw = await request.text();               // raw body, not parsed JSON
const expected = crypto
  .createHmac("sha512", process.env.PAYSTACK_SECRET_KEY)
  .update(raw)
  .digest("hex");

const signature = request.headers.get("x-paystack-signature") ?? "";
const ok =
  signature.length === expected.length &&
  crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));

if (!ok) return new Response("Invalid signature", { status: 401 });

const event = JSON.parse(raw);

Events

EventProviderFires when
charge.successPaystackFunds are confirmed by Paystack for a Century reference.
transfer.successPaystackPaystack confirms the payout has been received by the destination bank.
transfer.failedPaystackThe destination bank or Paystack rejects the payout.
transfer.reversedPaystackPaystack reverses a settled payout.

charge.success

A collection succeeded — checkout payment or a transfer into a dedicated virtual account.

Example payload
{
  "event": "charge.success",
  "data": {
    "reference": "CENT_TOPUP_XXXXXXXX",
    "amount": 500000,
    "status": "success",
    "customer": { "email": "customer@example.com" }
  }
}

transfer.success

An outbound transfer (provider just-in-time funding or payout) completed.

Example payload
{
  "event": "transfer.success",
  "data": {
    "reference": "CENTJIT_XXXXXXXX",
    "amount": 10000,
    "status": "success",
    "recipient": { "name": "PROVIDER RESELLER ACCOUNT" }
  }
}

transfer.failed

An outbound transfer failed. Century marks the funding event FAILED and alerts the operations team.

Example payload
{
  "event": "transfer.failed",
  "data": { "reference": "CENTJIT_XXXXXXXX", "reason": "Account resolution failed" }
}

transfer.reversed

A previously successful transfer was reversed and the funds returned to the pool.

Example payload
{
  "event": "transfer.reversed",
  "data": { "reference": "CENTJIT_XXXXXXXX", "status": "reversed" }
}

Retries & ordering

  • The provider retries with backoff until it receives a 2xx. Century therefore never returns 5xx for a duplicate.
  • Events can arrive out of order or more than once — key your own handling on the reference, not on arrival order.
  • If a webhook is missed entirely, reconciliation re-verifies the reference directly with the provider and credits once.

Not implemented

Century does not emit outbound events
These commonly requested events do not exist. Poll the relevant read endpoint instead.
wallet.creditedwallet.debitedvtu.successvtu.failedkyc.approvedkyc.rejectedsubscription.createdsubscription.cancelledloan.approvedloan.repaid