Skip to main content

Overview

Every API request must include three headers:

x-api-key

Your API key ID only (format: ak_live_xxx)

x-timestamp

Current Unix timestamp in milliseconds

x-signature

HMAC-SHA256 signature of the request
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.

Getting Your API Key

1

Log in to your CeyPay dashboard

2

Navigate to Settings > API Keys

3

Click Generate API Key

4

Copy the complete API key (you'll only see it once!)

5

Store it securely (treat it like a password)

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

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

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
  }
});

Common Mistakes & Troubleshooting

1. Sending Full API Key in Header

Wrong: Sending the full key including secret
headers: {
  'x-api-key': 'ak_live_abc123.sk_live_xyz789'  // Wrong!
}
Correct: Send only the key ID
headers: {
  'x-api-key': 'ak_live_abc123'  // Correct!
}

2. Using Raw Secret Instead of Derived Key

Wrong: Signing with raw secret
const signature = crypto.createHmac('sha256', secret).update(message).digest('hex');
Correct: Sign with derived key (SHA256 hash of secret)
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
const timestamp = Math.floor(Date.now() / 1000); // Wrong!
Correct: Use milliseconds
const timestamp = Date.now().toString(); // Correct!

4. Incorrect Message Concatenation

Wrong: Adding spaces or separators
const message = `${timestamp} ${method} ${path} ${body}`; // Wrong!
Correct: Direct concatenation with no separators
const message = timestamp + method + path + body; // Correct!

5. Query Parameters in Path

For GET requests with query parameters, include them in the path: Correct:
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:
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:
# 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

{
  "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

{
  "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

{
  "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

{
  "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 for details. Rate limit headers are included in every response:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640000000

Need Help?

Quick Start Guide

Get started with your first payment in minutes

Error Codes

Understand and troubleshoot API errors

Contact Support

Reach out to our team for assistance