# Wallet credit

import { ApiExample, ApiResponse } from "../../../src/ApiExample";

You can **declare** that one of your clients has paid. You never credit directly.

<Callout type="danger" title="Why a request and not a credit">
  You are a third party. Only Instafuel observes the payment landing on its bank account.

  If your call credited immediately, calling the API would be enough to create purchase credit with nothing collected behind it. Every request therefore goes through an Instafuel approval — no exception, and no threshold below which it would be automatic.
</Callout>

## Three locks

A credit is only possible if all three are open:

1. the key carries the `CREDIT_REQUEST_WRITE` scope — otherwise `403 PAPI_SCOPE_MISSING`;
2. `walletCreditEnabled` is true for your account — otherwise `403 RESELLER_CREDIT_DISABLED`;
3. the amount respects your per-operation and daily caps — otherwise `422`.

Your current caps are on `GET /v1/papi/me`:

<ApiResponse
  status={200}
  title="Excerpt from GET /v1/papi/me"
  body={{
    walletCreditEnabled: true,
    caps: {
      perOperationFcfa: 5000000,
      dailyFcfa: 20000000,
      dailyRemainingFcfa: 14500000,
    },
  }}
/>

## The flow

<Stepper>

1. **Upload the proof of payment**

   <ApiExample
     method="POST"
     path="/v1/papi/companies/4832/wallet/credit/proof"
     file="virement-kouassi-aout.pdf"
   />

   <ApiResponse
     status={201}
     body={{
       data: [
         {
           id: "11111111-1111-4111-8111-111111111111",
           fileName: "virement-kouassi-aout.pdf",
           mimeType: "application/pdf",
           sizeBytes: 184320,
           companyId: 4832,
           uploadedAt: "2026-08-31T09:02:44.000Z",
         },
       ],
     }}
   />

   Up to **3 files, 10 MB each**. Keep the returned `id`s: they are required in the next step.

   :::note[Proof is mandatory, whatever the source]
   Mobile money included. It is the document Instafuel's bank reconciliation will require before approving.
   :::

2. **Declare the payment**

   <ApiExample
     method="POST"
     path="/v1/papi/companies/4832/wallet/credit"
     idempotency
     body={{
       amountFcfa: 500000,
       source: "BANK_TRANSFER",
       clientReference: "VIR-2026-0814",
       proofIds: ["11111111-1111-4111-8111-111111111111"],
       note: "Versement mensuel Transports Kouassi",
     }}
   />

   The response is a **`202`, never a `200`** — and that difference is the whole point:

   <ApiResponse
     status={202}
     body={{
       status: "APPROVAL_PENDING",
       approvalId: "0193a1f2-7c44-7c1e-9b0a-5f2d1c8e4b60",
       companyId: 4832,
       amountFcfa: 500000,
       source: "BANK_TRANSFER",
       clientReference: "IFP7-VIR-2026-0814",
       walletBalanceFcfa: 1450000,
       requestedAt: "2026-08-31T09:04:12.000Z",
       expiresAt: "2026-09-07T09:04:12.000Z",
     }}
   />

   `202` means "request recorded", not "operation done". Three things to read in that response:

   - `walletBalanceFcfa` is **still 1,450,000**: the balance has not moved. Do not display it as credited.
   - `clientReference` came back **prefixed** with your introducer identifier (`IFP7-`). Two partners using "VIR-2026-0814" on the same wallet therefore do not collide.
   - `expiresAt` is 7 days away: without approval by then, the request expires and has to be filed again.

   A cap overrun is refused on the spot, with the figures you need to split the payment:

   <ApiResponse
     status={422}
     body={{
       error: {
         code: "RESELLER_CREDIT_CAP_EXCEEDED",
         message: "Montant supérieur au plafond par opération",
         details: [
           {
             field: "amountFcfa",
             messages: [
               "Plafond par opération : 5 000 000 FCFA",
               "Montant demandé : 7 500 000 FCFA",
             ],
           },
         ],
       },
     }}
   />

3. **Track the request**

   <ApiExample
     path="/v1/papi/credit-requests"
     query={{ status: "PENDING_APPROVAL", perPage: 20 }}
   />

   <ApiResponse
     status={200}
     body={{
       data: [
         {
           approvalId: "0193a1f2-7c44-7c1e-9b0a-5f2d1c8e4b60",
           companyId: 4832,
           amountFcfa: 500000,
           source: "BANK_TRANSFER",
           clientReference: "IFP7-VIR-2026-0814",
           status: "PENDING_APPROVAL",
           proofIds: ["11111111-1111-4111-8111-111111111111"],
           requestedAt: "2026-08-31T09:04:12.000Z",
           expiresAt: "2026-09-07T09:04:12.000Z",
         },
       ],
       pagination: {
         page: 1,
         perPage: 20,
         total: 1,
         totalPages: 1,
       },
     }}
   />

   | `status` | Meaning |
   |---|---|
   | `PENDING_APPROVAL` | waiting for Instafuel validation |
   | `APPROVED` | wallet credited, entry posted to the ledger |
   | `REJECTED` | refused — the reason is in `rejectionReason` |
   | `EXPIRED` | 7 days without approval |

4. **Instafuel approves**

   The wallet is credited, the entry is posted to the ledger and the proofs are attached to it. The request then reflects the entry produced:

   <ApiResponse
     status={200}
     body={{
       approvalId: "0193a1f2-7c44-7c1e-9b0a-5f2d1c8e4b60",
       status: "APPROVED",
       amountFcfa: 500000,
       approvedAt: "2026-09-01T08:12:55.000Z",
       ledgerEntry: {
         ref: "LED-8C40D21A",
         type: "CREDIT",
         amountFcfa: 500000,
         balanceAfterFcfa: 1950000,
         createdAt: "2026-09-01T08:12:55.000Z",
         confirmedAt: "2026-09-01T08:12:55.000Z",
       },
     }}
   />

   `balanceAfterFcfa` is the balance after the entry: that is the moment, and not before, when the credit becomes usable for fill-ups.

</Stepper>

## Fields

| Field | Required | Detail |
|---|---|---|
| `amountFcfa` | yes | strictly positive whole FCFA |
| `source` | yes | `BANK_TRANSFER`, `MOBILE_MONEY`, `CASH_DEPOSIT`, `CHEQUE` — the **actual** nature of the payment |
| `clientReference` | yes | your own reference: transfer number, mobile money transaction id… |
| `proofIds` | yes | 1 to 3 proof identifiers uploaded in step 1 |
| `note` | no | free comment, 500 characters |

## Caps

The daily cap counts credits **already approved** *and* requests **still pending**. Stacking requests under the cap does not get around it.

| `error.code` | HTTP | Cause |
|---|---|---|
| `RESELLER_CREDIT_DISABLED` | 403 | credit not enabled for your account |
| `RESELLER_CREDIT_CAP_EXCEEDED` | 422 | per-operation cap exceeded |
| `RESELLER_DAILY_CAP_EXCEEDED` | 422 | daily cap exceeded |
| `PROOF_REQUIRED` | 400 | no proof supplied |
| `PROOF_INVALID` | 422 | proof missing, deleted, already attached, or belonging to another company |
| `PROOF_TOO_MANY` | 400 | more than 3 proofs |
| `WALLET_BLOCKED` | 400 | the company's wallet is blocked |

A cap `422` is not retried as-is: split the payment or ask for a higher cap.

## Re-checked at approval time

Days can pass between your request and the approval. Everything is **re-checked** at execution time: is your account still active, is credit still allowed, is the company still in your portfolio, are the proofs still valid, is the wallet unblocked.

If any of those changed, execution fails and the request **stays pending** rather than being marked approved by mistake.

## What you cannot do

No refund, no debit, no wallet block, no cancellation of a posted entry. The ledger is immutable: a correction is a new entry, posted by Instafuel.
