# NPD Partner Credit API v1

The Partner Credit API lets commerce platforms submit seller stock-credit and buyer BNPL applications without depending on NPD's internal retail-loan implementation.

## Authentication

An NPD administrator creates a partner in **Admin → Partner Credit**. The API key is displayed once and must be sent as a bearer token:

```http
Authorization: Bearer npd_test_xxxxxxxxxx.yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
Accept: application/json
```

Keys are tenant-scoped. A partner cannot read another partner's applications. Use sandbox credentials until NPD changes the integration status to `active` and issues live credentials.

## Submit an application

```http
POST /api/v1/partner-credit/applications
```

Money is expressed in the currency's minor unit. For NGN, `50000000` means ₦500,000.00.

```json
{
  "partner_reference": "PRE-CREDIT-1001",
  "product": "seller_stock_credit",
  "customer_reference": "vendor-123",
  "requested_amount_minor": 50000000,
  "currency": "NGN",
  "customer": {
    "first_name": "Ada",
    "last_name": "Okafor",
    "email": "ada@example.com",
    "phone": "08012345678"
  },
  "business": {
    "name": "Ada Stores",
    "presency_store_id": "store-123"
  },
  "transaction": null,
  "consent": {
    "accepted": true,
    "accepted_at": "2026-08-28T10:00:00+01:00",
    "version": "credit-application-v1"
  },
  "metadata": {}
}
```

Supported products are `seller_stock_credit` and `buyer_bnpl`. The partner must be enabled for the submitted product.

`partner_reference` is the idempotency key. Repeating the same request returns the existing application. Reusing it with a different product, customer, or amount returns HTTP `409`.

## Retrieve an application

```http
GET /api/v1/partner-credit/applications/{npd_reference}
```

The response contains the current status and approved amount when available. Customer documents and private underwriting data are intentionally excluded.

## Automated affordability assessment

After application submission, upload the customer statement to NPD:

```http
POST /api/v1/partner-credit/applications/{npd_reference}/assessments
Content-Type: multipart/form-data
```

Common fields are `file`, `statement_source`, and `statement_reference`. Buyer BNPL also requires `tenure_months`. Seller stock credit also requires `product_price_min_minor`, `product_price_max_minor`, `repayment_frequency`, and `duration_days`.

Trusted buyer statement sources are `bank_provider` and `open_banking`. Seller assessments may additionally use `partner_sales_export`. A manual upload can be analysed but cannot pass the automatic-approval policy without human validation.

NPD calls its AI analyser to extract evidence, then a deterministic, versioned policy decides the result. The model itself does not approve credit. The policy checks:

- partner-attested identity verification;
- trusted statement provenance;
- buyer income coverage, stability, existing debt and affordability;
- seller statement coverage, qualifying transaction history, active sales days, sales concentration and qualifying-sales ratio;
- product caps, minimum offer and existing NPD exposure.

The response includes `recommended_amount_minor`, `requested_amount_eligible`, `decision`, and machine-readable `reason_codes`. When automation is enabled, a strong case can be approved immediately. If the requested amount exceeds the recommendation, NPD creates the lower eligible offer. Borderline or conflicting evidence is routed to review; insufficient affordability is declined.

Automated state changes are disabled by default with `PARTNER_CREDIT_AUTOMATED_DECISIONS_ENABLED=false`.

## Statuses

- `submitted`
- `under_review`
- `awaiting_information`
- `approved`
- `declined`
- `cancelled`

When an application is approved, its response includes `facility_reference`. The credit facility is the permanent container for mandates, disbursement, and repayments.

## Repayment mandates

Check provider availability:

```http
GET /api/v1/partner-credit/mandate-providers
```

Retrieve the facility and all safe mandate states:

```http
GET /api/v1/partner-credit/facilities/{facility_reference}
```

Initialize each mandate after collecting explicit repayment consent. Every
facility requires separate active `mono` and `paystack` mandates:

```http
POST /api/v1/partner-credit/facilities/{facility_reference}/mandates
```

```json
{
  "provider": "mono",
  "return_url": "https://partner.example/credit/mandate-return",
  "account_number": "0123456789",
  "bank_code": "058",
  "consent": {
    "accepted": true,
    "accepted_at": "2026-08-28T10:30:00+01:00",
    "version": "repayment-mandate-v1"
  }
}
```

`provider` accepts `auto`, `mono`, or `paystack`, but partners should explicitly
initialize both providers so the authorization checklist is deterministic. A
provider failure does not silently authorize the other provider—the customer
must explicitly complete each authorization.

Before initiating a mandate, the partner must show the configured `terms`, `terms_version`, and repayment schedule to the customer and record acceptance:

```http
POST /api/v1/partner-credit/facilities/{facility_reference}/accept-offer
```

```json
{
  "terms_version": "offer-v1-npd-fac-example-20260828120000",
  "consent": {
    "accepted": true,
    "accepted_at": "2026-08-28T12:05:00+01:00"
  }
}
```

NPD rejects stale offer versions. Each returned authorization URL should then
be opened for the customer. `pending` does not permit disbursement. The facility
becomes `ready_to_disburse` only after Mono reports ready-to-debit **and**
Paystack sends `direct_debit.authorization.active`.

## Disbursement requests

Once the offer is accepted, the repayment schedule exists, and both required
mandates are active, the partner can request disbursement:

```http
POST /api/v1/partner-credit/facilities/{facility_reference}/disbursements
```

```json
{
  "partner_reference": "PRE-DIS-1001",
  "account_number": "0123456789",
  "account_name": "ADA OKAFOR",
  "bank_code": "058",
  "bank_name": "Guaranty Trust Bank"
}
```

The endpoint only creates a request. It cannot move money. An authenticated NPD administrator must review and explicitly release the disbursement. API keys belonging to partners can never call the release endpoint.

Sandbox release marks the request successful without moving funds. Live release uses Paystack Transfers and is disabled unless `PARTNER_CREDIT_LIVE_DISBURSEMENTS_ENABLED=true`. Successful, failed, and reversed transfers are confirmed through Paystack's signed webhook before facility state is updated.

## Automated repayment collection

Partner-credit collections share NPD's scheduler and queue infrastructure but use a separate facility-aware collector and accounting ledger. The existing NPD salary and retail collector is not reused as a data model and is not modified by partner collections.

Collections are disabled by default. Production must set:

```dotenv
PARTNER_CREDIT_COLLECTIONS_ENABLED=true
PARTNER_CREDIT_COLLECTION_MAX_ATTEMPTS=5
PARTNER_CREDIT_COLLECTION_RETRY_MINUTES=360
PARTNER_CREDIT_COLLECTION_PROCESSING_TIMEOUT_MINUTES=15
```

The scheduler dispatches `partner-credit:collect-repayments` using the same configured interval as NPD's existing collector. Each due installment is locked, assigned a unique idempotency key, and charged for its exact remaining amount through the active facility mandate. A failed charge does not trigger percentage-based partial deductions or silently switch providers. A fallback mandate is used only when it is active and no active primary mandate remains.

Sandbox partner collections are simulated as successful and never call Mono or Paystack. No funds move in sandbox mode.

Collection outcomes create encrypted `credit_collection_attempts` records and emit:

- `credit.repayment.successful`
- `credit.repayment.failed`
- `credit.repayment.reconciliation_required`

The repayment webhook includes the facility reference, schedule sequence, collection reference, provider, exact minor-unit amount, attempt number, status, and safe failure details. When every installment is paid, NPD closes the facility.

An API timeout or non-final provider response is treated as an unknown outcome. NPD marks that attempt `reconciliation_required` and blocks automatic and manual retries until operations confirms the provider result. This prevents a second debit after a provider accepted the first request but NPD did not receive the response.

NPD administrators can queue an audited retry for an eligible failed installment from the facility screen. Manual retries may bypass the normal retry waiting period, but they do not bypass the maximum-attempt limit, the global collection feature flag, or a reconciliation hold.

Partial collection is disabled in every offer unless `partial_collection.permitted=true` is included in the versioned terms and the customer accepts that exact `terms_version`. The agreement also records the permitted percentage steps and minimum partial debit. NPD first attempts the full remaining installment; after an insufficient-funds result, later scheduler runs may attempt one permitted partial amount at a time. Every provider debit receives its own attempt and idempotency reference.

## Borrower-initiated manual repayment

An active facility can open a one-time Paystack checkout for exactly one of two scopes:

```http
POST /api/v1/partner-credit/facilities/{facility_reference}/manual-repayments
```

```json
{
  "scope": "next_installment",
  "return_url": "https://partner.example/credit/status"
}
```

`scope` is either `next_installment` or `full_balance`. NPD calculates the amount from the locked repayment schedule; partners and borrowers cannot submit a custom amount. The response contains the exact minor-unit amount and a short-lived `authorization_url` to open in the partner UI or app WebView.

Paystack's signed `charge.success` webhook is the authoritative confirmation. For local development or a delayed webhook, the partner can ask NPD to verify the returned Paystack reference directly:

```http
POST /api/v1/partner-credit/facilities/{facility_reference}/manual-repayments/{repayment_reference}/verify
```

Allocation is idempotent. While checkout is open, mandate collection is blocked for the same schedule. If a mandate outcome or schedule balance changes while checkout is open, the manual payment is marked `reconciliation_required` and no automatic allocation occurs. Successful payments emit `credit.manual_repayment.successful`; ambiguous payments emit `credit.manual_repayment.reconciliation_required`.

## Webhooks

NPD sends JSON events to the URL configured for the partner. Relevant headers are:

```http
X-NPD-Event-Id: <uuid>
X-NPD-Event: credit.application.approved
X-NPD-Timestamp: <unix timestamp>
X-NPD-Signature: sha256=<hex digest>
```

Verify the signature by calculating HMAC-SHA256 over:

```text
{timestamp}.{raw_request_body}
```

using the webhook signing secret shown when the partner is created. Reject stale timestamps and store `X-NPD-Event-Id` so repeated deliveries are handled idempotently.

Webhook delivery is disabled by default. Production must set `PARTNER_CREDIT_WEBHOOKS_ENABLED=true` and run the Laravel queue worker.
