# Authentication

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

The partner API authenticates with an **API key**, sent as a header on every request. There is no login, no password, no token to refresh: the key is the only secret.

```http title="Header"
Authorization: Bearer ifp_live_9f2c4a7b1e8d3406af5b2c9d1e0f7a83
```

<Callout type="note" title="No user login here">
  `/v1/papi/*` is a machine API, meant for your server. Your clients' human accounts (fleet manager, finance director) sign in to the Instafuel dashboard with their own password — that journey does not go through this API and is not documented here.
</Callout>

## An authenticated call

<ApiExample path="/v1/papi/me" />

<ApiResponse
  status={200}
  body={{
    ref: "APP-7C3D91B0",
    name: "Ivoire Fleet Partners",
    email: "contact@ivoirefleet.ci",
    environment: "test",
    companiesCount: 12,
    walletCreditEnabled: true,
    scopes: ["COMPANIES_READ", "TRANSACTIONS_READ", "WALLET_READ", "COMPANIES_WRITE", "CREDIT_REQUEST_WRITE", "REPORTS_READ"],
    caps: {
      perOperationFcfa: 5000000,
      dailyFcfa: 20000000,
      dailyRemainingFcfa: 14500000,
    },
  }}
/>

`GET /v1/papi/me` is the right health check: it changes nothing, costs little, and tells you exactly what the key allows.

## The two prefixes

| Prefix | Environment | Base URL | Effect |
|---|---|---|---|
| `ifp_test_*` | staging | <StagingUrl /> | test data, no real money |
| `ifp_live_*` | production | provided with the key | real money |

The prefix is part of the key: you read it, you do not guess it. A test key presented against production is rejected with `401`, and vice versa — the two environments share no data.

That separation gives you a control which is easy to automate: if your test code holds a key that does not start with `ifp_test_`, stop before the call.

```javascript title="JavaScript"
if (
  process.env.NODE_ENV !== "production" &&
  !process.env.INSTAFUEL_API_KEY.startsWith("ifp_test_")
) {
  throw new Error("Production key detected outside production — aborting.");
}
```

```python title="Python"
import os

api_key = os.environ["INSTAFUEL_API_KEY"]

if os.environ.get("APP_ENV") != "production" and not api_key.startswith("ifp_test_"):
    raise SystemExit("Production key detected outside production — aborting.")
```

## Where the key belongs

<Callout type="danger" title="An API key never lives on the client side">
  It grants access to your entire portfolio. It belongs in an environment variable or a secret manager, on your server. Never in a Git repository, never in a browser JavaScript bundle, never in a mobile app — even "obfuscated", it is extractable in minutes.
</Callout>

If your web interface must display portfolio data, route it through your own backend: that backend holds the key and applies your own access rules.

## Authentication errors

| HTTP | `error.code` | Cause | What to do |
|---|---|---|---|
| 401 | `PAPI_KEY_MISSING` | `Authorization` header absent | add the header |
| 401 | `PAPI_KEY_INVALID` | unknown key, revoked, or wrong environment | check the prefix and the base URL |
| 401 | `PAPI_KEY_EXPIRED` | key past its expiry | issue a new one, see [API keys](/en/guides/cles-api) |
| 403 | `PAPI_IP_FORBIDDEN` | calling address not in the allow list | add your server's outbound IP |
| 403 | `PAPI_SCOPE_MISSING` | the key lacks the scope this operation needs | use a key carrying it |
| 403 | `RESELLER_INACTIVE` | introducer account disabled | contact Instafuel |

A key revoked after a botched rotation gives this — that is the trace to look for when an integration goes dark at once:

<ApiResponse
  status={401}
  body={{
    error: {
      code: "PAPI_KEY_INVALID",
      message: "Clé d'API inconnue ou révoquée",
    },
  }}
/>

A valid key missing the required scope:

<ApiResponse
  status={403}
  body={{
    error: {
      code: "PAPI_SCOPE_MISSING",
      message: "Cette clé ne porte pas le droit CREDIT_REQUEST_WRITE",
      requiredScope: "CREDIT_REQUEST_WRITE",
    },
  }}
/>

Branch on `error.code`, never on `message` — see [Errors and idempotency](/en/guides/erreurs).

## What the key determines

The key carries your identity **and** your scope. You do not pick which company you call for: you call, and the server narrows the answer to your portfolio. No parameter widens that view — see [Scope](/en/guides/perimetre).

It also carries its scopes: a read-only key returns `403 PAPI_SCOPE_MISSING` on any write, with no side effect.
