paybondpaybond
Sign in

Integration · Paystack

Configure Paystack settlement

Tenant-admin guide: save your BYO Paystack secret key in Console Settlement, register the Paybond Gateway webhook URL, and enable the paystack_nip rail for NGN Dedicated Virtual Accounts and Transfers (NIP).

  • BYO secret key
  • DVA + Transfers (NIP)
  • NGN
  1. 01Connect
  2. 02Configure
  3. 03Verify
  4. 04Go live

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 sandbox or live, currency NGN, 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_nip rail once prerequisites are satisfied

Supported Paystack rail

RailWhat it settlesRequirement
paystack_nipDVA credit funds the intent; Harbor pays out via Transfer (NIP) or refunds/reverses after evidence evaluationActive 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:

  1. Create a Paystack account (or use your existing merchant), then open the dashboard in the environment you want. Start in test mode for sandbox.
  2. 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.
  3. Enable Dedicated Virtual Accounts (DVA) for NGN. DVA assignment is asynchronous (Paystack emits dedicatedaccount.assign.success when a virtual account is ready).
  4. 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

TS
TypeScript code sampleSwipe to inspect long lines
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:

  1. Choose sandbox or live environment. Sandbox tenants are restricted to sandbox.
  2. Confirm the currency is NGN (the only supported v1 corridor).
  3. Paste the secret key, add a label, then save.
  4. Register the returned environment-specific webhook URL in Paystack. Gateway's tenant-owned destination inventory resolves the DVA account_number or Paystack va_id from 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
Terminal commandSwipe to inspect long lines
paybond paystack ready
paybond paystack doctor
paybond paystack doctor --format json
  • paystack ready checks the paystack_nip allowed rail, destination, write-only secret key, and paid-plan gate for live destinations.
  • paystack doctor adds 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 --environment on 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:

  1. Enable Paystack NIP in allowed rails.
  2. Optionally set it as the default rail for NGN.
  3. 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:

  1. A payer sends a bank transfer into the Dedicated Virtual Account. Paystack emits a charge.success webhook where authorization.channel == "dedicated_nuban". Paybond verifies the signature, binds account_number / metadata (paybond_intent_id, paybond_tenant_id) to your tenant, and moves the intent to funded.
  2. Evidence evaluation moves the intent to evidence_submitted.
  3. Settlement confirm requests a Paystack Transfer (NIP) on evidence pass, or a refund/reversal on evidence fail, and waits for confirmation.
  4. The intent stays pending until Paybond receives the terminal webhook — transfer.success finalizes to released; transfer.failed / transfer.reversed and refund webhooks route to refunded or 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.

Where to go next