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.
Checkout webhook events (checkout.completed, checkout.failed, checkout.expired) are deferred and are not documented here yet.
How to set up webhooks
- Open the merchant dashboard and go to Settings → API Keys and Webhooks.
- Set a publicly reachable HTTPS URL for Live and/or Sandbox.
- Keep your secret API key available — the same full key is used to sign outbound deliveries (see Signature verification).
- Return
2xxas 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
LIVEorSANDBOXfor 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 exampleae_sk_live_...orae_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"
}