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

# Authentication

MooniePay API endpoints are secured with API keys generated directly from your dashboard. Every request must include **four required headers** — a public key, a Unix timestamp, a computed HMAC-SHA256 signature, and an environment indicator.

This multi-header approach protects your integration against replay attacks and ensures your secret key cryptographically signs every request.

***

### Generating Your API Keys

Navigate to the **Developer** section of your MooniePay dashboard to create your API keys.

<figure><img src="/files/YpyX0TcDSXNiXXLmNcY0" alt=""><figcaption></figcaption></figure>

MooniePay issues two credentials per key pair:

| Credential     | Description                                                                                                    |
| -------------- | -------------------------------------------------------------------------------------------------------------- |
| **Public Key** | Identifies your application. Sent in the `Authorization` header of every request.                              |
| **Secret Key** | Used to sign requests via HMAC. Must never be exposed publicly, committed to Git, or used in client-side code. |

> ⚠️ **Security Notice:** If you suspect your secret key has been compromised, revoke it immediately from the dashboard and replace it. Always store keys as environment variables — never hardcode them.

***

### API Keys are Scoped by Type

Unlike a single universal key, **MooniePay issues separate key pairs for each operation type.** You must use the correct key pair for the endpoint you are calling — using the wrong key type will result in a `401 Invalid API Key` error.

| Key Type        | Used for            | Endpoints     |
| --------------- | ------------------- | ------------- |
| **Payment Key** | Collecting payments | `/payments/*` |
| **Payout Key**  | Sending payouts     | `/payouts/*`  |

### Required Headers

Every API request must include these four headers:

| Header               | Required | Description                                                   |
| -------------------- | -------- | ------------------------------------------------------------- |
| `Authorization`      | ✅        | Your **public key** (e.g. `pk_sandbox_payment_xxxxxxxxxxxx`)  |
| `X-TTUM-Timestamp`   | ✅        | Current Unix timestamp in **seconds** (integer)               |
| `X-TTUM-Signature`   | ✅        | HMAC-SHA256 hex signature of the canonical string (see below) |
| `X-TTUM-Environment` | ✅        | Either `sandbox` or `live`                                    |

If any header is missing, the API returns:

```json
{
  "success": false,
  "code": "AUTHENTICATION_HEADERS_REQUIRED",
  "message": "Missing authentication headers.",
  "errors": []
}
```

***

### Replay Attack Protection

The `X-TTUM-Timestamp` The header is validated against the server's current time. Requests with a timestamp outside **±5 minutes** are automatically rejected:

```json
{
  "success": false,
  "code": "TIMESTAMP_EXPIRED",
  "message": "Timestamp expired.",
  "errors": []
}
```

> ⚠️ Always generate a **fresh timestamp per request**. Never reuse a timestamp from a previous request.

***

### Building the HMAC Signature

The `X-TTUM-Signature` is a **HMAC-SHA256** signature computed over a canonical string. The server independently computes the same string and compares — if they don't match, the request is rejected.

#### The Canonical String Format

```
{publicKey}.{timestamp}.{method}.{path}.{payload}
```

| Part        | Description                                                                                               |
| ----------- | --------------------------------------------------------------------------------------------------------- |
| `publicKey` | Your public API key                                                                                       |
| `timestamp` | The same Unix timestamp sent in `X-TTUM-Timestamp`                                                        |
| `method`    | HTTP method in uppercase: `GET`, `POST`, etc.                                                             |
| `path`      | Request path only, no host (e.g. `/v1/payments`)                                                          |
| `payload`   | For `GET`: the raw query string. For `POST`/`PATCH`: the raw JSON body string. Empty string `""` if none. |

#### Example canonical string

```
pk_sandbox_payment_xxxxxxxxxxxx.1718000000.POST./v1/payments.{"amount":5000,"payment_method":"momo_cm"}
```

Then sign it:

```
HMAC-SHA256( canonicalString, secretKey )  →  hex string
```

***

### Code Examples

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

```shellscript
# Set your values
PUBLIC_KEY="sk_sandbox_payment_xxxxxxxxxxxx"
SECRET_KEY="your_secret_key_here"
ENVIRONMENT="sandbox"
TIMESTAMP=$(date +%s)
METHOD="POST"
PATH="/v1/payments"
PAYLOAD='{ "amount": 5000, "payment_method": "momo_cm", 
"phone_number": "+237XXXXXX", "description" : "School fees", 
"customer" : {"first_name" : "John", "last_name":"doe", "email" : "johndoe@test.com"]
 }'

# Build canonical string and sign (requires openssl)
STRING_TO_SIGN="${PUBLIC_KEY}.${TIMESTAMP}.${METHOD}.${PATH}.${PAYLOAD}"
SIGNATURE=$(echo -n "$STRING_TO_SIGN" | openssl dgst -sha256 -hmac "$SECRET_KEY" | awk '{print $2}')

# Send request
curl https://api.pay.movemoonie.com/v1/payments \
  -X POST \
  -H "Authorization: $PUBLIC_KEY" \
  -H "X-TTUM-Timestamp: $TIMESTAMP" \
  -H "X-TTUM-Signature: $SIGNATURE" \
  -H "X-TTUM-Environment: $ENVIRONMENT" \
  -H "Content-Type: application/json" \
  -d "$PAYLOAD"
```

{% endtab %}

{% tab title="PHP" %}

```php
<?php

$publicKey   = 'sk_sandbox_payment_xxxxxxxxxxxx';
$secretKey   = 'your_secret_key_here';
$environment = 'live';
$timestamp   = time();

$method  = 'POST';
$path    = '/v1/payments';
$payload = json_encode([ 'amount' => 5000, 'payment_method' => 'mooniepay_demo', 
'phone_number' => '+237XXXXXX', 'description' => 'School fees', 
'customer' => ['first_name' => "John", "last_name"=>"doe", "email" => "johndoe@test.com"]
]);

// Build canonical string
$stringToSign = "{$publicKey}.{$timestamp}.{$method}.{$path}.{$payload}";

// Sign
$signature = hash_hmac('sha256', $stringToSign, $secretKey);

// Send request
$ch = curl_init('https://api.pay.movemoonie.com/v1/payments');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $payload,
    CURLOPT_HTTPHEADER     => [
        "Authorization: {$publicKey}",
        "X-TTUM-Timestamp: {$timestamp}",
        "X-TTUM-Signature: {$signature}",
        "X-TTUM-Environment: {$environment}",
        "Content-Type: application/json",
    ],
]);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
```

{% endtab %}

{% tab title="Node.Js" %}

```javascript
const crypto = require('crypto');
const https  = require('https');

const publicKey   = 'sk_sandbox_payment_xxxxxxxxxxxx';
const secretKey   = 'your_secret_key_here';
const environment = 'sandbox';
const timestamp   = Math.floor(Date.now() / 1000);

const method  = 'POST';
const path    = '/v1/payments';
const payload = JSON.stringify({ 'amount': 5000, 'payment_method': 'momo_cm', 
'phone_number': '+237XXXXXX', 'description' : 'School fees', 
'customer' : {'first_name' : "John", "last_name":"doe", "email" : "johndoe@test.com"]
 });

// Build canonical string
const stringToSign = `${publicKey}.${timestamp}.${method}.${path}.${payload}`;

// Sign
const signature = crypto
  .createHmac('sha256', secretKey)
  .update(stringToSign)
  .digest('hex');

// Send request
const options = {
  hostname: 'https://api.pay.movemoonie.com/v1/payments',
  path,
  method,
  headers: {
    'Authorization':      publicKey,
    'X-TTUM-Timestamp':   timestamp.toString(),
    'X-TTUM-Signature':   signature,
    'X-TTUM-Environment': environment,
    'Content-Type':       'application/json',
    'Content-Length':     Buffer.byteLength(payload),
  },
};

const req = https.request(options, (res) => {
  let data = '';
  res.on('data', (chunk) => (data += chunk));
  res.on('end', () => console.log(JSON.parse(data)));
});

req.write(payload);
req.end();
```

{% endtab %}

{% tab title="Python" %}

```python
import hmac
import hashlib
import time
import json
import requests

public_key   = 'sk_sandbox_payment_xxxxxxxxxxxx'
secret_key   = 'your_secret_key_here'
environment  = 'sandbox'
timestamp    = int(time.time())

method   = 'POST'
path     = '/v1/payments'
payload  = json.dumps({ "amount": 5000, "payment_method": "momo_cm", 
"phone_number": "+237XXXXXX", "description" : "School fees", 
"customer" : {"first_name" : "John", "last_name":"doe", "email" : "johndoe@test.com"]
 }, separators=(',', ':'))

# Build canonical string
string_to_sign = f"{public_key}.{timestamp}.{method}.{path}.{payload}"

# Sign
signature = hmac.new(
    secret_key.encode('utf-8'),
    string_to_sign.encode('utf-8'),
    hashlib.sha256
).hexdigest()

# Send request
response = requests.post(
    'https://api.pay.movemoonie.com/v1/payments',
    data=payload,
    headers={
        'Authorization':      public_key,
        'X-TTUM-Timestamp':   str(timestamp),
        'X-TTUM-Signature':   signature,
        'X-TTUM-Environment': environment,
        'Content-Type':       'application/json',
    }
)

print(response.json())
```

{% endtab %}
{% endtabs %}

***

#### Postman — Pre-request Script

If you are testing with **Postman**, add the following script to your collection's **Pre-request Script** tab. It automatically detects the endpoint type, selects the right key pair, computes the HMAC signature, and injects all four authentication headers before every request — no manual setup needed.

**Step 1 — Configure your Postman Environment variables:**

| Variable                | Example value                     | Description                      |
| ----------------------- | --------------------------------- | -------------------------------- |
| `X-TTUM-PAYMENT-KEY`    | `pk_sandbox_payment_xxxxxxxxxxxx` | Public key for payment endpoints |
| `X-TTUM-PAYMENT-SECRET` | `your_payment_secret`             | Secret key for payment endpoints |
| `X-TTUM-PAYOUT-KEY`     | `pk_sandbox_payout_xxxxxxxxxxxx`  | Public key for payout endpoints  |
| `X-TTUM-PAYOUT-SECRET`  | `your_payout_secret`              | Secret key for payout endpoints  |
| `X-TTUM-ENVIRONMENT`    | `sandbox`                         | `sandbox` or `live`              |

**Step 2 — Paste this into your collection Pre-request Script:**

```javascript
const CryptoJS = require('crypto-js');

// 1. Request path (no host)
const path = pm.request.url.getPath();

// 2. Select the correct key pair based on the endpoint type
let publicKey;
let secretKey;

if (path.includes('payouts')) {
    publicKey = pm.environment.get("X-TTUM-PAYOUT-KEY");
    secretKey = pm.environment.get("X-TTUM-PAYOUT-SECRET");
} else {
    publicKey = pm.environment.get("X-TTUM-PAYMENT-KEY");
    secretKey = pm.environment.get("X-TTUM-PAYMENT-SECRET");
}

const environment = pm.environment.get("X-TTUM-ENVIRONMENT") || "sandbox";

if (!publicKey || !secretKey) {
    throw new Error("Missing MooniePay API credentials in Postman environment variables.");
}

// 3. Timestamp (Unix seconds)
const timestamp = Math.floor(Date.now() / 1000);

// 4. HTTP method
const method = pm.request.method.toUpperCase();

// 5. Payload — query string for GET, raw JSON body for POST/PATCH
let payload = "";
if (method === "GET") {
    payload = pm.request.url.getQueryString() || "";
} else if (pm.request.body && pm.request.body.raw) {
    payload = pm.variables.replaceIn(pm.request.body.raw).trim();
    pm.request.body.raw = payload;
}

// 6. Build canonical string
// Format: {publicKey}.{timestamp}.{method}.{path}.{payload}
const stringToSign = `${publicKey}.${timestamp}.${method}.${path}.${payload}`;

// 7. Sign with HMAC-SHA256
const signature = CryptoJS
    .HmacSHA256(stringToSign, secretKey)
    .toString(CryptoJS.enc.Hex);

// 8. Inject authentication headers
pm.request.headers.upsert({ key: "Authorization",       value: publicKey });
pm.request.headers.upsert({ key: "X-TTUM-Timestamp",    value: timestamp.toString() });
pm.request.headers.upsert({ key: "X-TTUM-Signature",    value: signature });
pm.request.headers.upsert({ key: "X-TTUM-Environment",  value: environment });

// Debug output (visible in Postman Console)
console.log("MooniePay Auth Debug", {
    publicKey,
    timestamp,
    method,
    path,
    payload,
    stringToSign,
    signature,
    environment
});
```

> 💡 **How it works:** The script inspects the request path. If it contains `payouts`, it uses your payout key pair. Otherwise it uses your payment key pair. This means the same collection handles both endpoint types automatically.

***

### IP Whitelisting (Optional)

For additional security, you can restrict any API key to a list of trusted IP addresses from the dashboard. Requests from unlisted IPs are blocked with:

```json
{
  "success": false,
  "code": "FORBIDDEN",
  "message": "Forbidden.",
  "errors": []
}
```

***

### Authentication Error Reference

| HTTP Status | Code                              | Cause                                                              |
| ----------- | --------------------------------- | ------------------------------------------------------------------ |
| `401`       | `AUTHENTICATION_HEADERS_REQUIRED` | One or more of the 4 required headers is missing                   |
| `401`       | `INVALID_TIMESTAMP`               | `X-TTUM-Timestamp` is not a valid positive integer                 |
| `400`       | `INVALID_ENVIRONMENT`             | `X-TTUM-Environment` is not `sandbox` or `live`                    |
| `401`       | `TIMESTAMP_EXPIRED`               | Timestamp is outside the ±5 minute window                          |
| `401`       | `INVALID_API_KEY`                 | Key not found, disabled, inactive, or wrong type for this endpoint |
| `401`       | `INVALID_DIGITAL_SIGNATURE`       | HMAC signature does not match                                      |
| `403`       | `FORBIDDEN`                       | Request IP is not in the whitelisted IPs                           |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://moonie.gitbook.io/mooniepay/introduction/authentication.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
