# ZenithPay Merchant Integration Guide
## One master collection for Pay With Transfer and Dedicated Virtual Accounts

Version 1.0 • Reviewed 19 October 2026

This guide covers the currently reviewed merchant PWT and DVA collection APIs. It preserves existing merchant endpoints and callback formats. It does not enable new providers, sandbox access, payouts or settlement permissions. All account numbers, payment references and identities in the distributed examples are placeholders.

## 1. Choose the right collection method

| Topic | Dedicated Virtual Account (DVA) | Pay With Transfer (PWT) |
|---|---|---|
| Purpose | Assign a customer a reusable receiving account | Initialize one payment-specific transfer account and checkout link |
| Customer BVN | Required by the current assignment API | Not required by the initialization request |
| Required setup | Active merchant account, approved business, business name, valid Developer API token and configured merchant webhook | Valid approved merchant access and Developer API token; callback URL supplied with initialization |
| Identity to store | Returned accountReference, accountNumber and accountName, linked to your customer | Returned orderNo, order ID when present, checkout link, status URL and your business reference |
| Duplicate-payment protection | Use each callback transaction_id as the payment reference | Retain the payment reference plus the delivery event identity |
| Callback receiver | Merchant's configured webhook URL | Initialization callback_url |
| Success meaning | A verified successful transfer into the DVA | A verified successful collection for the PWT order |
| Settlement | Separate pending D+1 settlement | Separate pending D+1 settlement |

Account assignment and payment initialization do not mean a customer has paid. Do not credit a customer merely because an account number was created.

## 2. Import and configure Postman

1. Import ZenithPay-Master-PWT-DVA.postman_collection.json.
2. Import ZenithPay-Private-Template.postman_environment.json and select that environment.
3. Enter your existing merchant Developer token in apiToken privately. Keep baseUrl as https://zenithpay.ng.
4. Fill the DVA or PWT variables listed below, then choose the individual request you intend to send.
5. Read the Tests results and the JSON response. A legacy HTTP 200 alone does not confirm business success.

The five requests are DVA assignment, PWT initialization, PWT status, PWT callback diagnostics and deliberate PWT callback retry. The webhook-reference folder contains documentation only; it does not send a callback.

Use current Postman with pm.execution.skipRequest support. Missing configuration skips the request and explains the missing field in the Postman Console. Scripts do not send hidden requests or automatically run other requests. Do not run the full collection blindly against production: account creation and manual resend have real effects. A Test/Live toggle or placeholder credentials do not establish an isolated provider sandbox.

Keep a filled environment private: it can contain API credentials, BVN, customer details and saved request bodies. The distributed template is empty of these values. Do not put merchant tokens in browser/mobile frontend code or share screenshots of populated environments.

Web login is a separate session flow and does not issue the Developer token used by these APIs. A login cookie or a successful login response is not a substitute for the approved merchant Developer bearer token. This master collection deliberately avoids the old token-extraction login example.

## 3. Authentication and base URL

```http
Authorization: Bearer YOUR_MERCHANT_DEVELOPER_TOKEN
Accept: application/json
Content-Type: application/json
```

The request scripts read the selected private environment directly and bind the reviewed destination and bearer header. They do not send the token to returned checkout links. The legacy base URL in this collection is https://zenithpay.ng; do not replace it with an unverified API hostname or assume that /api/v1 collection endpoints exist.

Ownership is resolved from authentication. Do not send user_id or authenticated_user_id to select a different merchant.

## 4. DVA — account assignment

POST /api/dedicated_account/assign

| Field | Postman variable | Current rule |
|---|---|---|
| bvn | dvaBVN | Required 11-digit string; the current validator also rejects a BVN present in users.bvn |
| account_name | dvaAccountName | Required string, maximum 255 characters |
| first_name | dvaFirstName | Required string, maximum 255 characters |
| last_name | dvaLastName | Required string, maximum 255 characters |
| email | dvaEmail | Required valid email, maximum 255 characters |

```json
{
  "bvn": "YOUR_CUSTOMER_BVN",
  "account_name": "Abba",
  "first_name": "Abba",
  "last_name": "Example",
  "email": "customer@example.invalid"
}
```

Replace the placeholders privately with the customer's legitimate details. The collection uses JSON; the existing Laravel request also accepts form-urlencoded input. No callback_url, amount or PWT idempotency key is part of this DVA assignment body.

The merchant account must be active, the business approved and its business name present. Some legacy rejection branches return an error object even under HTTP 200. Confirm status is the boolean true, response_code is "00", and returned account fields are present. Empty, HTML or non-JSON responses are failures to confirm creation; they are not a reason to assume success or retry automatically.

### DVA account naming

For the current PalmPay adapter, ZenithPay combines the merchant-supplied account name with the merchant business name. PalmPay returns the issued display name with its ZenithPay suffix. Example: account name Abba, business Databuy → input ABBA DATABUY → issued name such as ABBA DATABUY (ZenithPay). Do not manually append the suffix to account_name. Save and display the actual returned accountName; other providers have their own native naming conventions.

### DVA assignment response and customer mapping

The current response contains top-level camelCase fields. An illustrative subset is:

```json
{
  "status": true,
  "response_code": "00",
  "accountReference": "EXAMPLE_DVA_ACCOUNT_REFERENCE",
  "accountName": "ABBA DATABUY (ZenithPay)",
  "bankName": "PALMPAY",
  "accountNumber": "EXAMPLE_ACCOUNT_NUMBER"
}
```

Store the accountReference and accountNumber against the intended customer in your own database, scoped to your merchant account. The response may also contain customer/identity fields, including BVN; protect them and do not copy real values into support messages. The Postman script only saves the account reference, number and returned name in the private environment.

### DVA reuse, timeouts and errors

Reuse the assigned account for that customer rather than calling assignment before every deposit. Each genuine deposit has its own transaction_id, even when accountReference and account number remain the same.

The reviewed DVA assignment API does not offer the PWT stable-key initialization guarantee. Sending an Idempotency-Key header does not establish DVA idempotency. After a timeout, empty response or uncertain provider result, check existing account mapping with ZenithPay support/dashboard before creating another account. Do not change a BVN to bypass validation. A BVN validation conflict is not proof that a usable DVA was returned.

## 5. DVA — incoming payments and callbacks

Configure your merchant webhook URL through the existing ZenithPay merchant settings/support flow before relying on automated receipt. ZenithPay posts a successful DVA payment notification to that configured URL. This URL is separate from provider-to-ZenithPay notification endpoints.

```http
Content-Type: application/json
X-ZenithPay-Timestamp: UNIX_TIMESTAMP
X-ZenithPay-Signature: HEX_HMAC_SHA256
X-ZenithPay-Server-IP: 195.110.59.12
```

The legacy DVA sender does not require PWT X-Zenithpay-Event or X-Zenithpay-Event-Id headers. Do not reject a genuine DVA payment because those PWT headers are missing. Header names are case-insensitive. No bearer Authorization signing token is sent with an outbound callback.

The included examples/dva-callback.anonymized.json preserves the supplied callback field layout, including status, response_code, transaction_id, type, paymentDetails, payerDetails, orderId, accountNumber, amountPaid, settlementAmount, Payment_type and processed_by. Optional extra fields can be present. Treat string field names and their casing as part of the existing wire format.

### DVA processing rules

1. Read the exact raw request body and verify the timestamp/signature as described in section 8.
2. Require a recognized successful DVA notification and validate transaction_id as a nonempty payment reference.
3. Resolve the intended customer using your stored merchant/account mapping. Do not credit a customer solely by the email or payer name in the notification.
4. Check the account and amount fields for consistency with the expected contract. The supplied example had different nested and top-level account-number values; that may be masking. This bundle uses one consistent placeholder. Do not silently accept unexplained discrepancies in real receiver data.
5. In one durable database transaction, reserve a unique payment-reference record and perform your single business credit. Commit before acknowledging.
6. Return HTTP 200 for a durably accepted payment or an already accepted duplicate. A duplicate must not credit again. The legacy DVA sender specifically checks HTTP 200, so do not rely on 201 or 204 as equivalent acknowledgements.

Two deposits into the same DVA with different transaction_id values are two payments. The same transaction_id arriving twice is one payment. Keep deduplication records when your own transaction history is archived. Do not use accountReference or the account number as a once-ever payment deduplication key: that would suppress legitimate later deposits.

There is no DVA payment-status, per-event diagnostics or DVA replay request documented in this collection. The PWT endpoints below are not DVA endpoints. For missing DVA notifications, review your receiver and use support with the genuine payment reference; do not fabricate a provider callback or repeatedly assign new accounts.

## 6. PWT — initialize one payment

POST /api/transaction/initialize

| Body field/header | Postman variable | Meaning |
|---|---|---|
| amount | pwtAmount | Positive amount in legacy naira units, minimum 0.01 |
| email | pwtEmail | Customer email |
| callback_url | pwtCallbackUrl | Merchant's public HTTPS POST receiver |
| fee_bearer | pwtFeeBearer | customer or merchant |
| meta_data.customer_id | pwtCustomerId | Optional merchant customer reference |
| meta_data.order_id | pwtMerchantOrderId | Optional merchant business-order reference |
| Idempotency-Key | pwtIdempotencyKey | Stable key for one logical initialization |

No customer BVN is required in this request. Use returned payable, fee and merchant-receivable amounts, account/bank details, currency and expiry. Do not hardcode a universal fee percentage or assume that every profile uses the same body shape.

Save the successful initialization's orderNo, checkout_link and status_url, and order ID when present. The Postman script saves these in the private environment after a valid success response. Initialization is not proof of payment. Returned checkout/status links are informational customer-facing links; use the authenticated status request for the merchant API flow.

The callback destination must use public HTTPS with a valid certificate on port 443. URL userinfo, fragments, private/reserved destinations and redirects are rejected by the current transport policy. Keep query strings exactly as configured when your endpoint requires them.

### PWT initialization retries and uncertain results

The collection generates a key only when pwtIdempotencyKey is empty, then retains it across sends. It stores a private key/body binding and refuses to send a changed body under that same key. An intentionally different key starts a different logical initialization; it is not a recovery mechanism.

For a genuine new business payment, intentionally clear the key and enter the new payment details. Do not rotate or clear keys to bypass an in-flight/uncertain result. Retain the original key/body/reference and obtain support. A non-200/conflict/uncertain response must not be reported as successful initialization, and automatic provider resend is not enabled by this guide.

## 7. PWT — status, diagnostics and deliberate retry

| Method/path | Input | Purpose |
|---|---|---|
| POST /api/transaction/status | order_no, or order_id | Query your merchant-owned PWT order |
| POST /api/transaction/callback-diagnostics | order_no, or order_id | Read existing order-level callback diagnostics |
| POST /api/transaction/callback-retry | order_no, or order_id | Deliberately send an existing notification after receiver repair |

Set pwtOrderNo or pwtOrderId. When both are populated, the collection sends order_no only to avoid accidentally combining references from different orders. The reviewed route limits are 30/minute for status, 60/minute for diagnostics and 20/minute for manual callback retry; honor any additional platform limits.

A status query can contact the provider and update local collection status. Common collection states are pending, success, failed and expired. The deployed policy preserves confirmed success against stale downgrades and retains supported verified late-success handling. A successful status response is not a settlement-status or available-wallet-balance response.

Callback diagnostics include callback_attempts, callback_last_error, callback_last_attempt_at and callback_notified_at. These order-level fields are not a complete per-event delivery ledger and do not prove merchant-side single credit.

Manual callback retry is included for completeness but disabled by default. Use it only after read-only review and receiver repair. Set allowManualPwtCallbackRetry to true for a deliberate send; the script resets it to false before sending. A retry sends a real notification and does not create another entitlement to money. Inspect a failed result before considering another resend.

### PWT incoming callback

```http
Content-Type: application/json
X-Zenithpay-Event: pwt.intent.succeeded
X-Zenithpay-Event-Id: YOUR_EVENT_ID
X-Zenithpay-Timestamp: UNIX_TIMESTAMP
X-Zenithpay-Signature: HEX_HMAC_SHA256
X-Zenithpay-Source-IP: CONFIGURED_SOURCE_IP
```

The source-IP header is included when configured. The reference PWT body in examples/pwt-callback.illustrative.json is one compatible shape; existing queued/direct profiles can retain other body details. Recognize the appropriate profile, validate the authenticated body and retain payment-reference uniqueness in addition to delivery-event identity. Do not credit pending, failed or expired notices.

PWT retries/resends retain their durable logical event identity. A timeout can occur after a receiver committed its work; an acknowledgement can be lost. Do not promise exactly-once HTTP delivery. Return HTTP 200 after durable acceptance; PWT also accepts other 2xx responses.

## 8. Shared callback authentication

```text
HMAC-SHA256(timestamp + "." + exact_raw_request_body, merchant_Developer_signing_token)
```

Use the existing Developer signing token used by the ZenithPay sender. If you name your private receiver environment variable ZENITHPAY_WEBHOOK_SECRET, its value must be that exact existing signing token; the variable name does not create a separate secret. An empty, invented or unrelated value will reject authentic callbacks. Never take the signing token from the incoming request.

Verify the raw bytes before JSON decoding. Compare signatures in constant time and validate a bounded timestamp window. The included verify-zenithpay-callback.php provides authentication-only verification using a 300-second example window; keep the server clock synchronized. It performs no HTTP acknowledgement, wallet credit or database write.

Read php://input, the timestamp and signature headers, and your private signing token. Call the function inside your own exception handling. Malformed JSON or authentication failure must not produce a success acknowledgement. After authentication, validate the product, merchant/customer/order mapping, successful payment state, amount and currency where supplied, then perform durable business processing.

The signature covers timestamp and raw body, not every header. Event IDs and source-IP headers are metadata, not substitutes for HMAC or independent proof of the network sender. Scope deduplication to your merchant and the authenticated payment reference. If using an IP restriction, validate the real peer/trusted-proxy chain; a claimed header alone is insufficient.

## 9. Durable merchant credit and acknowledgement

Keep the received-payment decision and the business-credit ledger entry atomic, with a durable unique constraint on the merchant/payment reference. Record delivery-event identity where supplied. An already committed duplicate should return the accepted response without another credit. Preserve these reference records when moving old history to backup/archive tables.

DVA amountPaid is the gross payment and settlementAmount is the net value carried by the current callback. The supplied 11.00/10.89 values are one example, not a universal fee quotation. Apply your documented business-credit policy and validate the supplied amounts; never credit both fields separately. Handle money as exact decimals/integer minor units internally rather than binary floating-point.

Test duplicate processing in an isolated receiver database: two copies of the same payment reference should produce one credit; a distinct reference on the same DVA should produce another legitimate credit. Observe genuine live payments read-only when validating production; do not replay live financial callbacks for testing. HTTP 200 at the sender does not alone prove one merchant ledger credit.

## 10. D+1 settlement — both PWT and DVA

ZenithPay settlement follows D+1: transactions received today are processed for settlement on the next working day by 06:00 AM Africa/Lagos. Collection success and pending settlement are different states. A successful callback does not make funds immediately available for withdrawal.

| Transaction day | Usual settlement day, when no intervening configured holiday applies |
|---|---|
| Monday | Tuesday, 06:00 AM |
| Tuesday | Wednesday, 06:00 AM |
| Wednesday | Thursday, 06:00 AM |
| Thursday | Friday, 06:00 AM |
| Friday, Saturday and Sunday | Together on Monday, 06:00 AM |

If the next settlement day is a weekend or public holiday, settlement moves to the next eligible working day at 06:00 AM. In the supplied holiday example, payments received Wednesday 30 September 2026 would normally settle Thursday 1 October; with 1 October configured as a holiday, settlement moves to Friday 2 October at 06:00 AM.

Collections continue during weekends and holidays. Settlement processing follows working-day rules and configured holiday deferrals. These are policy examples, not a calculation or certification of an individual transaction's actual settlement eligibility. This bundle changes no live settlement schedule, fee configuration, balances or payout permission, and contains no settlement-execution request.

## 11. Troubleshooting

| Symptom | What to check |
|---|---|
| Request skipped/no response in Postman | Select the private environment, fill the required variables, and read the Console's non-secret configuration message |
| API HTTP 401 | Correct Developer token and merchant access; a web login cookie is insufficient |
| DVA HTTP 200 but status is error/false | Business approval, active-account state, business name and response message; do not cache as a successful assignment |
| DVA HTTP 422 | Exact required field names, BVN string/length/current uniqueness validation, email and customer data |
| DVA timeout/empty/non-JSON response | Outcome is not confirmed; inspect existing account mapping with support before resending |
| Callback signature rejected | Existing signing token, exact raw bytes, timestamp header, case-insensitive header lookup and synchronized clock |
| DVA callback lacks PWT event headers | Expected legacy difference; verify DVA signature/timestamp and use transaction_id |
| Merchant receiver HTTP 405 | Receiving route may not accept POST; repair it before any deliberate resend |
| PWT order HTTP 404 | Authenticated owner and the correct order_no/order_id |
| PWT conflict/uncertain initialization | Preserve original key/body/reference; do not create a new key to force another account |
| Repeated callback acknowledgements | Inspect the receiver's durable deduplication and ledger; repeated attempts are not proof of duplicate credit |

## 12. Coverage and verification boundaries

The master contains all five reviewed merchant PWT/DVA collection operations, common authentication setup, request examples, response handling, separate callback contracts and settlement guidance. It does not invent DVA lookup/retry APIs, add provider-notify simulation requests, register merchants, enable payouts or create a sandbox. No live API call is needed to import or inspect these files.

The scripts and examples were checked offline. A filled environment and actual end-to-end merchant processing still require the merchant's own legitimate setup and acceptance checks. The public website and separately published remote Postman reference are not automatically changed by downloading this bundle.
