Before intents can settle against Paystack, a tenant admin connects a Bring-Your-Own (BYO) Paystack merchant and the paystack_nip rail. Paystack is self-serve: you sign up for your own Paystack merchant account, collect your own keys, and complete your own live KYB with Paystack. Paybond never brokers a partnership on your behalf — it resolves your secret key server-side from this configuration, and agents and SDK clients never supply Paystack credentials.
The rail funds through Dedicated Virtual Accounts (DVA): a payer sends a bank transfer into a tenant-owned virtual account (NUBAN), a verified webhook drives the funding gate, and Harbor executes a Transfer (NIP) payout (or refund/reversal) after evidence evaluation. This guide is analogous to Configure Flutterwave settlement and Configure Adyen settlement.
Console workspace
Paystack destinations are configured in Configuration → Settlement (/console/configuration/settlement) as a tenant admin.
From there you will be able to:
- Save a Paystack destination (environment
sandboxorlive, currencyNGN, label) - Enter a write-only secret key (never redisplayed after save)
- Copy the Gateway webhook URL into the Paystack dashboard
- Confirm readiness (secret key ready / environment and currency badges)
- Enable the
paystack_niprail once prerequisites are satisfied
Supported Paystack rail
| Rail | What it settles | Requirement |
|---|---|---|
paystack_nip | DVA credit funds the intent; Harbor pays out via Transfer (NIP) or refunds/reverses after evidence evaluation | Active Paystack destination with a NGN secret key for the environment |
Step 1 — Collect Paystack credentials (self-serve)
You own the Paystack merchant relationship. In your own Paystack dashboard:
- Create a Paystack account (or use your existing merchant), then open the dashboard in the environment you want. Start in test mode for sandbox.
- Copy the secret key from Settings → API Keys & Webhooks (
sk_test_…for sandbox,sk_live_…for live). This single secret is used both for outbound API calls and to verify inbound webhooks — there is no separate webhook secret. - Enable Dedicated Virtual Accounts (DVA) for NGN. DVA assignment is asynchronous (Paystack emits
dedicatedaccount.assign.successwhen a virtual account is ready). - Live keys require your own Paystack merchant KYB. Complete Paystack's merchant onboarding/KYB in your dashboard to unlock
sk_live_…. Sandbox test keys are available immediately without KYB.
Paybond v1 supports the NGN corridor only. The intent budget currency must match the destination currency (NGN).
Verify a webhook in Node.js
Use the raw bytes supplied by your HTTP framework. Do not parse and re-serialize JSON before calculating the digest. The secret must come from server-side configuration only; never send it to a browser, agent, or SDK client.
paybond-session.ts
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyPaystackWebhook(
rawBody: Buffer,
signatureHeader: string | undefined,
secretKey: string,
): boolean {
if (!signatureHeader || !secretKey) return false;
const expected = createHmac("sha512", secretKey).update(rawBody).digest();
let received: Buffer;
try {
received = Buffer.from(signatureHeader.trim(), "hex");
} catch {
return false;
}
return received.length === expected.length && timingSafeEqual(received, expected);
}Verify a webhook in Go
As above, calculate against the unmodified request body and compare decoded hexadecimal digests in constant time.
func verifyPaystackWebhook(rawBody []byte, signature, secretKey string) bool {
if strings.TrimSpace(signature) == "" || strings.TrimSpace(secretKey) == "" {
return false
}
mac := hmac.New(sha512.New, []byte(secretKey))
_, _ = mac.Write(rawBody)
expected := mac.Sum(nil)
received, err := hex.DecodeString(strings.TrimSpace(signature))
return err == nil &&
len(received) == len(expected) &&
subtle.ConstantTimeCompare(received, expected) == 1
}
Step 2 — Register the Gateway webhook URL
Register the Paybond Gateway webhook URLs in the Paystack dashboard so DVA credit and transfer lifecycle events reach Paybond:
- Live:
https://api.paybond.ai/webhooks/live/paystack(or the compatibility path/webhooks/paystack) - Sandbox tenant test traffic:
https://api.paybond.ai/webhooks/sandbox/paystack
Register the environment-matching URL — sandbox events on the sandbox path, live events on the live path. Gateway resolves the tenant using its vaulted destination inventory (account_number or va_id) and requires signed payload binding metadata; do not add tenant identifiers to the URL.
Step 3 — Save the destination in Console
In Configuration → Settlement:
- Choose sandbox or live environment. Sandbox tenants are restricted to sandbox.
- Confirm the currency is
NGN(the only supported v1 corridor). - Paste the secret key, add a label, then save.
- Register the returned environment-specific webhook URL in Paystack. Gateway's tenant-owned destination inventory resolves the DVA
account_numberor Paystackva_idfrom each signed delivery; this server-side binding is never taken from an agent request or an unauthenticated client tenant id.
The secret key is write-only. After save, the Console shows the destination environment, currency, label, and ready badges only — never the decrypted secret.
After you rotate the secret key in the Paystack dashboard, re-save the destination in Console Settlement immediately so outbound Transfer calls and inbound webhook verification (which both use the same key) stay aligned.
CLI readiness (paybond paystack)
After paybond login, use the Kit CLI to check settlement-config readiness without exposing credentials:
terminal
paybond paystack ready
paybond paystack doctor
paybond paystack doctor --format jsonpaystack readychecks thepaystack_nipallowed rail, destination, write-only secret key, and paid-plan gate for live destinations.paystack doctoradds the expected sandbox/live webhook URL, signed-in tenant environment mismatch hints, and a Console destination-upsert pointer.- Never pass
--secret-key,--account-number,--va-id, or--environmenton CLI argv. They can be exposed through process listings; use Console Settlement's authenticated write-only form.
Step 4 — Enable paystack_nip
Once readiness reports an active destination with the secret key configured:
- Enable Paystack NIP in allowed rails.
- Optionally set it as the default rail for
NGN. - Save routing.
Harbor rejects creates on this rail when no active Paystack destination exists for the tenant and destination currency.
Step 5 — Async terminal settlement
DVA credit and Transfer payout are asynchronous:
- A payer sends a bank transfer into the Dedicated Virtual Account. Paystack emits a
charge.successwebhook whereauthorization.channel == "dedicated_nuban". Paybond verifies the signature, bindsaccount_number/metadata(paybond_intent_id,paybond_tenant_id) to your tenant, and moves the intent tofunded. - Evidence evaluation moves the intent to
evidence_submitted. - Settlement confirm requests a Paystack Transfer (NIP) on evidence pass, or a refund/reversal on evidence fail, and waits for confirmation.
- The intent stays pending until Paybond receives the terminal webhook —
transfer.successfinalizes toreleased;transfer.failed/transfer.reversedand refund webhooks route torefundedor manual review per policy.
Treat create-transfer and refund API responses as request-accepted only. A 2xx from the Transfer API is not a terminal state — wait for the webhook.
Operational notes
Bad or mismatched signature
If the secret key in Console does not match the one in the Paystack dashboard, the X-Paystack-Signature HMAC-SHA512 verification fails and Paybond rejects the deliveries. Symptoms: DVA credits do not fund the intent, and Gateway logs record verification rejections. Re-copy the secret key from the dashboard into Console Settlement (remember it doubles as the webhook verification key) and re-save.
Unbound or unattributable webhooks
Money-state webhooks must carry binding metadata (metadata with paybond_intent_id and paybond_tenant_id) and a resolvable account_number or virtual-account id. Events that cannot be attributed to the authenticated tenant destination are held for manual review and never auto-fund an intent. If virtual accounts are provisioned outside Paybond, ensure the binding metadata is stamped at transaction create time.
Stuck or pending transfers
When a Transfer payout webhook lags, the intent stays in its pending terminal state rather than flipping to released / refunded on the API response alone. Paybond re-queries the provider transfer status to converge. Do not patch intent rows to fake a terminal state — preserve the stored provider ids (paystack_transaction_id, paystack_transfer_id) and let recovery reconcile.