> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ceypay.io/llms.txt
> Use this file to discover all available pages before exploring further.

# API Authentication

> Learn how CeyPay's HMAC-SHA256 signature authentication secures API communication between your application and our servers.

## Overview

Every API request must include three headers:

<CardGroup cols={3}>
  <Card title="x-api-key" icon="key">
    Your API key ID only (format: `ak_live_xxx`)
  </Card>

  <Card title="x-timestamp" icon="clock">
    Current Unix timestamp in milliseconds
  </Card>

  <Card title="x-signature" icon="signature">
    HMAC-SHA256 signature of the request
  </Card>
</CardGroup>

<Warning>
  The `x-api-key` header should contain ONLY the key ID, NOT the full API key. The secret key is never transmitted over the wire.
</Warning>

## Getting Your API Key

<Steps>
  <Step title="Log in to your CeyPay dashboard" />

  <Step title="Navigate to Settings > API Keys" />

  <Step title="Click Generate API Key" />

  <Step title="Copy the complete API key (you'll only see it once!)" />

  <Step title="Store it securely (treat it like a password)" />
</Steps>

<Info>
  **API Key Format**: `ak_live_abc123.sk_live_xyz789`

  * First part (`ak_live_abc123`): Public key ID - sent in `x-api-key` header
  * Second part (`sk_live_xyz789`): Secret key - used locally to derive signing key, NEVER transmitted
</Info>

## Signature Calculation

The signature is calculated using a **derived signing key** (hash of your secret):

```
signingKey = SHA256(secret_key)
message = timestamp + method + path + body
signature = HMAC-SHA256(message, signingKey)
```

### Why Use a Derived Key?

This design ensures your secret key is **never transmitted** over the network:

* You send only the key ID in `x-api-key`
* The signature proves you possess the secret without revealing it
* Even if an attacker intercepts the request, they cannot forge new requests

### Components:

1. **signingKey**: SHA256 hash of your secret key (computed locally)
2. **timestamp**: Unix timestamp in milliseconds (same value as `x-timestamp` header)
3. **method**: HTTP method in UPPERCASE (`GET`, `POST`, `PATCH`, `DELETE`)
4. **path**: Full request path including query parameters (e.g., `/v1/payments?page=1`)
5. **body**: Request body as JSON string (empty string for GET/DELETE requests)

## Implementation Examples

<CodeGroup>
  ```javascript Node.js theme={null}
  const crypto = require('crypto');

  // Your full API key (store securely, e.g., in environment variables)
  const API_KEY = 'ak_live_abc123.sk_live_xyz789';
  const [keyId, secret] = API_KEY.split('.');

  // Derive signing key from secret (do this once, reuse for all requests)
  const signingKey = crypto.createHash('sha256').update(secret).digest('hex');

  function generateSignature(timestamp, method, path, body) {
    const message = timestamp + method + path + body;
    const signature = crypto
      .createHmac('sha256', signingKey)
      .update(message)
      .digest('hex');
    return signature;
  }

  // Example: POST /v1/payment
  const timestamp = Date.now().toString();
  const method = 'POST';
  const path = '/v1/payment';
  const body = JSON.stringify({
    amount: 100,
    currency: 'USDT',
    goods: [{ name: 'Product', description: 'Test product' }]
  });

  const signature = generateSignature(timestamp, method, path, body);

  // Make the request
  const axios = require('axios');
  const response = await axios.post('https://api.ceypay.io/v1/payment', body, {
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': keyId,  // Only the key ID, NOT the full key!
      'x-timestamp': timestamp,
      'x-signature': signature
    }
  });
  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  import time
  import json
  import requests

  # Your full API key (store securely)
  API_KEY = 'ak_live_abc123.sk_live_xyz789'
  key_id, secret = API_KEY.split('.')

  # Derive signing key from secret
  signing_key = hashlib.sha256(secret.encode()).hexdigest()

  def generate_signature(timestamp, method, path, body):
      message = f"{timestamp}{method}{path}{body}"
      signature = hmac.new(
          signing_key.encode(),
          message.encode(),
          hashlib.sha256
      ).hexdigest()
      return signature

  # Example: POST /v1/payment
  timestamp = str(int(time.time() * 1000))
  method = 'POST'
  path = '/v1/payment'
  body_dict = {
      'amount': 100,
      'currency': 'USDT',
      'goods': [{'name': 'Product', 'description': 'Test product'}]
  }
  body = json.dumps(body_dict)

  signature = generate_signature(timestamp, method, path, body)

  # Make the request
  response = requests.post(
      'https://api.ceypay.io/v1/payment',
      json=body_dict,
      headers={
          'x-api-key': key_id,  # Only the key ID!
          'x-timestamp': timestamp,
          'x-signature': signature
      }
  )
  ```

  ```php PHP theme={null}
  <?php

  // Your full API key (store securely)
  $apiKey = 'ak_live_abc123.sk_live_xyz789';
  [$keyId, $secret] = explode('.', $apiKey);

  // Derive signing key from secret
  $signingKey = hash('sha256', $secret);

  function generateSignature($timestamp, $method, $path, $body, $signingKey) {
      $message = $timestamp . $method . $path . $body;
      $signature = hash_hmac('sha256', $message, $signingKey);
      return $signature;
  }

  // Example: POST /v1/payment
  $timestamp = (string)(time() * 1000);
  $method = 'POST';
  $path = '/v1/payment';
  $bodyArray = [
      'amount' => 100,
      'currency' => 'USDT',
      'goods' => [
          ['name' => 'Product', 'description' => 'Test product']
      ]
  ];
  $body = json_encode($bodyArray);

  $signature = generateSignature($timestamp, $method, $path, $body, $signingKey);

  // Make the request
  $ch = curl_init('https://api.ceypay.io/v1/payment');
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_POST, true);
  curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
  curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'Content-Type: application/json',
      'x-api-key: ' . $keyId,  // Only the key ID!
      'x-timestamp: ' . $timestamp,
      'x-signature: ' . $signature
  ]);

  $response = curl_exec($ch);
  curl_close($ch);
  ?>
  ```

  ```ruby Ruby theme={null}
  require 'openssl'
  require 'json'
  require 'net/http'
  require 'uri'

  # Your full API key (store securely)
  api_key = 'ak_live_abc123.sk_live_xyz789'
  key_id, secret = api_key.split('.')

  # Derive signing key from secret
  signing_key = Digest::SHA256.hexdigest(secret)

  def generate_signature(timestamp, method, path, body, signing_key)
    message = "#{timestamp}#{method}#{path}#{body}"
    OpenSSL::HMAC.hexdigest('SHA256', signing_key, message)
  end

  # Example: POST /v1/payment
  timestamp = (Time.now.to_f * 1000).to_i.to_s
  method = 'POST'
  path = '/v1/payment'
  body_hash = {
    amount: 100,
    currency: 'USDT',
    goods: [{ name: 'Product', description: 'Test product' }]
  }
  body = body_hash.to_json

  signature = generate_signature(timestamp, method, path, body, signing_key)

  # Make the request
  uri = URI('https://api.ceypay.io/v1/payment')
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true

  request = Net::HTTP::Post.new(uri.path)
  request['Content-Type'] = 'application/json'
  request['x-api-key'] = key_id  # Only the key ID!
  request['x-timestamp'] = timestamp
  request['x-signature'] = signature
  request.body = body

  response = http.request(request)
  ```
</CodeGroup>

## Common Mistakes & Troubleshooting

### 1. Sending Full API Key in Header

❌ **Wrong**: Sending the full key including secret

```javascript theme={null}
headers: {
  'x-api-key': 'ak_live_abc123.sk_live_xyz789'  // Wrong!
}
```

✅ **Correct**: Send only the key ID

```javascript theme={null}
headers: {
  'x-api-key': 'ak_live_abc123'  // Correct!
}
```

### 2. Using Raw Secret Instead of Derived Key

❌ **Wrong**: Signing with raw secret

```javascript theme={null}
const signature = crypto.createHmac('sha256', secret).update(message).digest('hex');
```

✅ **Correct**: Sign with derived key (SHA256 hash of secret)

```javascript theme={null}
const signingKey = crypto.createHash('sha256').update(secret).digest('hex');
const signature = crypto.createHmac('sha256', signingKey).update(message).digest('hex');
```

### 3. Incorrect Timestamp Format

❌ **Wrong**: Using seconds instead of milliseconds

```javascript theme={null}
const timestamp = Math.floor(Date.now() / 1000); // Wrong!
```

✅ **Correct**: Use milliseconds

```javascript theme={null}
const timestamp = Date.now().toString(); // Correct!
```

### 4. Incorrect Message Concatenation

❌ **Wrong**: Adding spaces or separators

```javascript theme={null}
const message = `${timestamp} ${method} ${path} ${body}`; // Wrong!
```

✅ **Correct**: Direct concatenation with no separators

```javascript theme={null}
const message = timestamp + method + path + body; // Correct!
```

### 5. Query Parameters in Path

For GET requests with query parameters, include them in the path:

✅ **Correct**:

```javascript theme={null}
const path = '/v1/payment/list?page=2&pageSize=50';
const body = ''; // Empty for GET requests
const message = timestamp + 'GET' + path + body;
```

### 6. JSON Body Formatting

Ensure the body is stringified exactly as sent in the request:

✅ **Correct**:

```javascript theme={null}
const bodyObject = { amount: 100, currency: 'USDT', goods: [...] };
const bodyString = JSON.stringify(bodyObject);

// Use bodyString for both signature and request body
const signature = generateSignature(timestamp, method, path, bodyString);
```

### 7. Timestamp Expiration

Timestamps are valid for **5 minutes**. If you get a timestamp error:

* Ensure your server's clock is synchronized (use NTP)
* Generate the timestamp immediately before making the request
* Don't reuse old timestamps

## Security Best Practices

1. **Never expose your secret key**
   * Don't commit it to version control
   * Use environment variables
   * Rotate keys if compromised

2. **Use HTTPS only**
   * Never send API requests over HTTP
   * Validate SSL certificates

3. **Implement timestamp validation**
   * Reject requests with timestamps older than 5 minutes
   * Prevents replay attacks

4. **Log signature failures**
   * Monitor for unusual patterns
   * Could indicate attempted attacks

5. **Rotate API keys periodically**
   * Recommended: Every 90 days
   * Immediately if compromised

6. **Store the derived signing key securely**
   * Compute it once at application startup
   * Keep it in memory, don't log it

## Testing Your Implementation

Use the webhook test endpoint to verify your signature calculation:

```bash theme={null}
# First, derive your signing key
SIGNING_KEY=$(echo -n "sk_live_xyz789" | sha256sum | cut -d' ' -f1)

# Then create signature
TIMESTAMP=$(date +%s000)
MESSAGE="${TIMESTAMP}POST/v1/webhooks/test{\"webhookUrl\": \"https://your-app.com/webhook\"}"
SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "$SIGNING_KEY" | cut -d' ' -f2)

curl -X POST https://api.ceypay.io/v1/webhooks/test \
  -H "Content-Type: application/json" \
  -H "x-api-key: ak_live_abc123" \
  -H "x-timestamp: $TIMESTAMP" \
  -H "x-signature: $SIGNATURE" \
  -d '{"webhookUrl": "https://your-app.com/webhook"}'
```

If you get a 401 Unauthorized, check:

1. `x-api-key` contains ONLY the key ID (no `.sk_live_...` part)
2. Signing key is SHA256 hash of your secret
3. Timestamp is current (within 5 minutes) and in milliseconds
4. Signature calculation matches exactly
5. HTTP method is uppercase
6. Path includes query parameters if any

## Error Responses

### 401 Unauthorized - Invalid x-api-key Format

```json theme={null}
{
  "statusCode": 401,
  "message": "Invalid x-api-key format: send only the key ID (ak_live_xxx), not the full key",
  "error": "Unauthorized"
}
```

**Fix**: Send only the key ID in `x-api-key`, not the full `keyId.secret` format.

### 401 Unauthorized - Invalid Signature

```json theme={null}
{
  "statusCode": 401,
  "message": "Invalid signature",
  "error": "Unauthorized"
}
```

**Fix**: Ensure you're using the derived signing key (SHA256 hash of secret) for HMAC.

### 401 Unauthorized - Timestamp Outside Valid Window

```json theme={null}
{
  "statusCode": 401,
  "message": "Request timestamp outside valid window",
  "error": "Unauthorized"
}
```

**Fix**: Ensure timestamp is current (within 5 minutes) and in milliseconds.

### 401 Unauthorized - Invalid API Key

```json theme={null}
{
  "statusCode": 401,
  "message": "Invalid or inactive API key",
  "error": "Unauthorized"
}
```

**Fix**: Verify your API key ID is correct and hasn't been revoked.

## Rate Limits

API requests are rate-limited per endpoint. See [rate-limits.md](/api/v1/rate-limits) for details.

Rate limit headers are included in every response:

```
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640000000
```

## Need Help?

<CardGroup cols={3}>
  <Card title="Quick Start Guide" icon="rocket" href="/api/v1/quickstart">
    Get started with your first payment in minutes
  </Card>

  <Card title="Error Codes" icon="circle-exclamation" href="/api/v1/errors">
    Understand and troubleshoot API errors
  </Card>

  <Card title="Contact Support" icon="headset" href="mailto:support@ceypay.io">
    Reach out to our team for assistance
  </Card>
</CardGroup>
