Webhooks

Receive real-time HTTP POST notifications when transfers settle, wallets move money, mandates debit, bills vend, or a customer links a payment method — without polling the API.

Overview

When an event occurs on your merchant account, Aella sends a signed JSON payload to the webhook URL you configure per environment. Your endpoint should verify the signature, acknowledge with a 2xx response quickly, and process the event idempotently.

How to set up webhooks

  1. Open the merchant dashboard and go to Settings → API Keys and Webhooks.
  2. Set a publicly reachable HTTPS URL for Live and/or Sandbox.
  3. Keep your secret API key available — the same full key is used to sign outbound deliveries (see Signature verification).
  4. Return 2xx as soon as you have accepted the delivery. Do heavy work asynchronously.

Delivery envelope

Every outbound webhook uses the same top-level shape. Event-specific fields live inside data.

Envelope

{
  "data": {},
  "event": "transfer.completed",
  "environment": "LIVE",
  "timestamp": "2026-07-27T10:00:00.000Z"
}
  • Name
    data
    Type
    object
    Description

    Event payload. Shape depends on event.

  • Name
    event
    Type
    string
    Description

    Event type name (for example transfer.completed).

  • Name
    environment
    Type
    string
    Description

    LIVE or SANDBOX for the delivery.

  • Name
    timestamp
    Type
    string
    Description

    ISO-8601 time when the delivery was created.

Signature verification

Each request includes an HMAC-SHA512 hex digest of the exact JSON body string in the x-aella-signature header.

  • Header: x-aella-signature
  • Algorithm: HMAC-SHA512, hex digest
  • Signed bytes: JSON.stringify(body) as sent by Aella (verify against the raw request body, not a re-parsed object)
  • Secret: your full secret API key (prefix + value), for example ae_sk_live_... or ae_sk_test_...

Reject any request whose signature does not match before acting on the event.

Verify signature

import crypto from 'crypto'
import express from 'express'

const SECRET = process.env.AELLA_SECRET_KEY // e.g. ae_sk_live_...

const app = express()

app.post(
  '/webhooks/aella',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.get('x-aella-signature')
    const rawBody = req.body // Buffer of the exact JSON bytes

    const expected = crypto
      .createHmac('sha512', SECRET)
      .update(rawBody)
      .digest('hex')

    const valid =
      typeof signature === 'string' &&
      signature.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))

    if (!valid) {
      return res.status(401).send('Invalid signature')
    }

    const event = JSON.parse(rawBody.toString('utf8'))
    // handle event.event / event.data idempotently
    res.status(200).send('ok')
  },
)

Retries

If your endpoint does not return a successful 2xx response, Aella retries delivery:

  • Attempts: 10
  • Backoff: exponential, starting at 30 seconds
  • Handlers must be idempotent — the same event + resource id may arrive more than once

Prefer acknowledging quickly (2xx) and processing asynchronously. Deduplicate using stable ids in data (for example transfer id, bill transaction_reference, or payment-method id).

Event types

  • Name
    transfer.completed
    Description

    Outward transfer settled successfully.

  • Name
    transfer.failed
    Description

    Outward transfer failed.

  • Name
    subaccount.credited
    Description

    Sub-account balance credited.

  • Name
    subaccount.debited
    Description

    Sub-account balance debited.

  • Name
    merchant.credited
    Description

    Merchant main wallet credited.

  • Name
    merchant.debited
    Description

    Merchant main wallet debited.

  • Name
    subaccount.created
    Description

    Sub-account created.

  • Name
    inwards.completed
    Description

    Inward transfer (collection) completed.

  • Name
    inwards.failed
    Description

    Inward transfer failed.

  • Name
    p2p.completed
    Description

    Peer-to-peer wallet transfer completed.

  • Name
    mandate.created
    Description

    Direct-debit mandate created.

  • Name
    mandate.confirmed
    Description

    Mandate confirmed by the customer.

  • Name
    mandate.debit.initiated
    Description

    Mandate debit initiated.

  • Name
    mandate.debit.successful
    Description

    Mandate debit succeeded.

  • Name
    mandate.debit.failed
    Description

    Mandate debit failed.

  • Name
    bill.successful
    Description

    Bill payment vend succeeded.

  • Name
    bill.failed
    Description

    Bill payment vend failed.

  • Name
    customer.card.link.successful
    Description

    Customer card link completed.

  • Name
    customer.card.link.failed
    Description

    Customer card link failed.

  • Name
    customer.direct_debit.link.successful
    Description

    Customer direct-debit link completed.

  • Name
    customer.direct_debit.link.failed
    Description

    Customer direct-debit link failed.

  • Name
    customer.direct_debit.link.deleted
    Description

    Customer direct-debit link deleted.

transfer.completed

{
  "data": {
    "id": "xfer-uuid",
    "narration": "Payout to John Doe",
    "status": "Success",
    "amount": 10000,
    "fee": 0,
    "externalReference": "order-1001",
    "currency": "NGN",
    "sessionId": null,
    "senderName": "Acme Merchants",
    "senderBank": "Aella Microfinance Bank",
    "senderBankCode": "090416",
    "senderAccountNumber": "0242438865",
    "receiverName": "JOHN DOE",
    "receiverBank": "GTBANK PLC",
    "receiverAccountNumber": "0123456789",
    "receiverBankCode": "000013",
    "environment": "LIVE",
    "source": "API",
    "sourceWallet": "wallet-uuid",
    "createdAt": "2026-07-27T10:00:00.000Z",
    "updatedAt": "2026-07-27T10:00:00.000Z"
  },
  "event": "transfer.completed",
  "environment": "LIVE",
  "timestamp": "2026-07-27T10:00:00.000Z"
}

inwards.completed

{
  "data": {
    "id": "inwards-uuid",
    "narration": "",
    "status": "Success",
    "amount": 5000,
    "fee": 25,
    "externalReference": "invoice-1001",
    "currency": "NGN",
    "sessionId": "090614447739340718791499120552",
    "senderName": "JOHN DOE",
    "senderBank": "GTBANK PLC",
    "senderBankCode": "000013",
    "senderAccountNumber": "0123456789",
    "receiverName": "Acme Merchants",
    "receiverBank": "Aella Microfinance Bank",
    "receiverAccountNumber": "0376236159",
    "receiverBankCode": "090416",
    "environment": "LIVE",
    "source": "DYNAMIC_VIRTUAL",
    "sourceWallet": "vaccount-uuid",
    "createdAt": "2026-07-27T10:00:00.000Z",
    "updatedAt": "2026-07-27T10:00:00.000Z"
  },
  "event": "inwards.completed",
  "environment": "LIVE",
  "timestamp": "2026-07-27T10:00:00.000Z"
}

customer.card.link.successful

{
  "data": {
    "id": "pm-card-uuid",
    "customer_id": "cust-uuid",
    "type": "card",
    "status": "active",
    "provider_reference": "card-uuid",
    "reference": "card-link-ref-1001",
    "email": "john.doe@example.com",
    "card_id": "card-uuid"
  },
  "event": "customer.card.link.successful",
  "environment": "LIVE",
  "timestamp": "2026-07-27T10:00:00.000Z"
}

customer.direct_debit.link.successful

{
  "data": {
    "id": "pm-dd-uuid",
    "customer_id": "cust-uuid",
    "type": "direct_debit",
    "status": "active",
    "provider_reference": "mandate-uuid",
    "reference": "dd-link-ref-1001",
    "email": "john.doe@example.com",
    "mandate_reference": "mandate-uuid"
  },
  "event": "customer.direct_debit.link.successful",
  "environment": "LIVE",
  "timestamp": "2026-07-27T10:00:00.000Z"
}

bill.successful

{
  "data": {
    "transaction_reference": "bill-tx-uuid",
    "status": "Success",
    "amount": 2000,
    "external_reference": "bill-order-1001"
  },
  "event": "bill.successful",
  "environment": "LIVE",
  "timestamp": "2026-07-27T10:00:00.000Z"
}

Was this page helpful?