> For the complete documentation index, see [llms.txt](https://developer.fraudcheck.co.za/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developer.fraudcheck.co.za/authentication.md).

# Authentication

Every API request must include a valid bearer token in the `Authorization` header. Tokens are obtained by exchanging your account email and password at the `/auth/login` endpoint and are valid for **1 hour**.

{% stepper %}
{% step %}

#### Request a token

Submit your credentials as `application/x-www-form-urlencoded`. Note that the field name is `username`, even though the value is your email address.

{% tabs %}
{% tab title="cURL" %}

```bash
curl -X POST "https://consumer-service-api.fraudcheckonline.co.za/auth/login" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "email=your-email@example.com&password=your-password"
```

{% endtab %}

{% tab title="Python" %}

```python
import requests

response = requests.post(
    "https://consumer-service-api.fraudcheckonline.co.za/auth/login",
    data={
        "email": "your-email@example.com",
        "password": "your-password",
    },
)
token = response.json()["access_token"]
```

{% endtab %}

{% tab title="JavaScript (Node.js)" %}

```javascript
const response = await fetch("https://consumer-service-api.fraudcheckonline.co.za/auth/login", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: "email=your-email@example.com&password=your-password",
});
const { access_token } = await response.json();
```

{% endtab %}
{% endtabs %}

**Response**

```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer"
}
```

{% endstep %}

{% step %}

#### Use the token

Include the token in the `Authorization` header on every API call:

```http
Authorization: Bearer {your_access_token}
```

**Example call**

```bash
curl -X POST "https://consumer-service-api.fraudcheckonline.co.za/consumer-service/idv/verify/onfile" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "IdentityNo": "8601015800086",
    "FirstName": "John",
    "LastName": "Smith",
    "ConsentObtainedByDataSubject": true
  }'
```

{% endstep %}

{% step %}

#### Token lifetime and refresh

* **Lifetime:** 1 hour from issue
* **Expiry behaviour:** expired tokens return `HTTP 401 Unauthorized` on every request
* **Refresh:** there is no refresh token - request a new token from `/auth/login` using the same credentials

A common pattern is to cache the token for 50 minutes, then proactively refresh before it expires. Alternatively, catch the first `401` from a request, fetch a new token, and retry the original request once.

**Python - refresh on 401**

```python
import requests

class TokenClient:
    def __init__(self, base_url, username, password):
        self.base_url = base_url
        self.username = username
        self.password = password
        self.token = None

    def _refresh(self):
        r = requests.post(
            f"{self.base_url}/auth/login",
            data={"username": self.username, "password": self.password},
        )
        r.raise_for_status()
        self.token = r.json()["access_token"]

    def post(self, path, json):
        if not self.token:
            self._refresh()
        headers = {"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}
        r = requests.post(f"{self.base_url}{path}", headers=headers, json=json)
        if r.status_code == 401:
            self._refresh()
            headers["Authorization"] = f"Bearer {self.token}"
            r = requests.post(f"{self.base_url}{path}", headers=headers, json=json)
        return r
```

{% endstep %}
{% endstepper %}

## Errors

| HTTP | Meaning                                                            | Action                                    |
| ---- | ------------------------------------------------------------------ | ----------------------------------------- |
| 400  | Missing `username` or `password` field                             | Include both fields in the form body      |
| 401  | Invalid credentials, or expired/invalid token on a subsequent call | Check credentials, or request a new token |
| 403  | Account exists but is disabled or not provisioned                  | Contact your account manager              |

## Storing tokens securely

* **Never** commit tokens to source control or log them.
* Store tokens in environment variables, a secrets manager, or an encrypted credential store - not in plain config files.
* Treat tokens like passwords: anyone with the token can call the API as you for the next hour.
* Rotate the password used to issue tokens periodically.

## Troubleshooting

<details>

<summary>I'm getting `401 Unauthorized` on every call.</summary>

The token has expired or was never set. Request a new token and confirm the `Authorization` header is being sent in the format `Bearer {token}` (with a space after `Bearer`).

</details>

<details>

<summary>My new token still returns `401`.</summary>

Check that you're using the correct base URL and that your account is active. If the credentials are right and the account is enabled, contact support.

</details>

<details>

<summary>I'm getting `403 Forbidden` on a service call.</summary>

The token is valid but your account doesn't have access to that service. Contact your account manager to enable the product.

</details>

<details>

<summary>Can I have multiple tokens active at once?</summary>

Yes - issuing a new token does not invalidate previously issued tokens. Each token is independently valid until it expires.

</details>
