Authentication Overview
Waffo Pancake supports two authentication methods for API access:
| Method | Use Case | Description |
|---|
| API Key | Server-to-server calls | Permanent authentication using RSA-SHA256 signatures |
| Store Slug | Public checkout flows | Public access using X-Store-Slug and X-Environment headers |
API Key Authentication
API Keys provide permanent server-to-server authentication using RSA-SHA256 signatures. The private key never leaves your server.
X-Merchant-Id: MER_2aUyqjCzEIiEcYMKj7TZtw
X-Timestamp: 1705312200
X-Signature: BASE64_ENCODED_SIGNATURE
Content-Type: application/json
API Key authentication does not require the X-Environment header. Each API Key is bound to either test or prod at creation time. The environment is determined by which key successfully verifies the signature.
Using the SDK (Recommended)
import { WaffoPancake } from "@waffo/pancake-ts";
const client = new WaffoPancake({
merchantId: process.env.WAFFO_MERCHANT_ID!,
privateKey: process.env.WAFFO_PRIVATE_KEY!,
});
// All requests are automatically signed
const { store } = await client.stores.create({ name: "My Store" });
Signing Algorithm (Manual Integration)
If you’re not using the SDK, you need to implement RSA-SHA256 request signing:
1. Build the canonical request:
canonicalRequest = METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + SHA256_BASE64(BODY)
2. Sign with RSA-SHA256:
signature = RSA-SHA256(canonicalRequest, privateKey)
3. Base64 encode:
X-Signature = Base64(signature)
Manual Signing Examples
const crypto = require('crypto');
const MERCHANT_ID = 'MER_2aUyqjCzEIiEcYMKj7TZtw';
const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----
...your private key...
-----END RSA PRIVATE KEY-----`;
async function callApiWithSignature(method, path, body) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const bodyStr = JSON.stringify(body);
const bodyHash = crypto.createHash('sha256').update(bodyStr).digest('base64');
// Build canonical request
const canonicalRequest = `${method}\n${path}\n${timestamp}\n${bodyHash}`;
// RSA-SHA256 sign
const signature = crypto.sign('sha256', Buffer.from(canonicalRequest), PRIVATE_KEY).toString('base64');
const response = await fetch(`https://api.waffo.ai${path}`, {
method,
headers: {
'Content-Type': 'application/json',
'X-Merchant-Id': MERCHANT_ID,
'X-Timestamp': timestamp,
'X-Signature': signature
},
body: bodyStr
});
return response.json();
}
// Usage
const result = await callApiWithSignature('POST', '/v1/actions/store/create-store', {
name: 'My Store'
});
MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw"
TIMESTAMP=$(date +%s)
BODY='{"name":"My Store"}'
BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0)
CANONICAL_REQUEST="POST
/v1/actions/store/create-store
$TIMESTAMP
$BODY_HASH"
# Sign with openssl (requires private_key.pem file)
SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0)
curl -X POST "https://api.waffo.ai/v1/actions/store/create-store" \
-H "Content-Type: application/json" \
-H "X-Merchant-Id: $MERCHANT_ID" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Signature: $SIGNATURE" \
-d "$BODY"
import hashlib
import time
import base64
import json
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
MERCHANT_ID = 'MER_2aUyqjCzEIiEcYMKj7TZtw'
PRIVATE_KEY = '''-----BEGIN RSA PRIVATE KEY-----
...your private key...
-----END RSA PRIVATE KEY-----'''
def call_api_with_signature(method, path, body):
timestamp = str(int(time.time()))
body_str = json.dumps(body, separators=(',', ':'))
body_hash = base64.b64encode(hashlib.sha256(body_str.encode()).digest()).decode()
# Build canonical request
canonical_request = f"{method}\n{path}\n{timestamp}\n{body_hash}"
# RSA-SHA256 sign
private_key = serialization.load_pem_private_key(PRIVATE_KEY.encode(), password=None)
signature = private_key.sign(canonical_request.encode(), padding.PKCS1v15(), hashes.SHA256())
signature_b64 = base64.b64encode(signature).decode()
response = requests.post(
f'https://api.waffo.ai{path}',
headers={
'Content-Type': 'application/json',
'X-Merchant-Id': MERCHANT_ID,
'X-Timestamp': timestamp,
'X-Signature': signature_b64
},
data=body_str
)
return response.json()
# Usage
result = call_api_with_signature('POST', '/v1/actions/store/create-store', {
'name': 'My Store'
})
Private Key Security
- Never expose the private key in client-side code
- Do not commit private keys to version control
- Store private keys in environment variables
- Rotate keys regularly, especially after team changes
- Timestamp must be within 5 minutes of server time
Store Slug Authentication
For public-facing checkout flows, use Store Slug authentication. This allows visitors to create checkout sessions and query public store data without API Key credentials.
X-Store-Slug: my-awesome-store-k8x2m9ab
X-Environment: test | prod
Content-Type: application/json
Example
curl -X POST https://api.waffo.ai/v1/actions/checkout/create-session \
-H "X-Store-Slug: my-awesome-store-k8x2m9ab" \
-H "X-Environment: test" \
-H "Content-Type: application/json" \
-d '{"productId": "PROD_4cWAslE1GKkGeaOMl9Vbmy", "productType": "onetime", "currency": "USD"}'
const response = await fetch('https://api.waffo.ai/v1/graphql', {
method: 'POST',
headers: {
'X-Store-Slug': 'my-awesome-store-k8x2m9ab',
'Content-Type': 'application/json',
'X-Environment': 'prod'
},
body: JSON.stringify({
query: `query { store { id name onetimeProducts { id name prices } } }`
})
});
Creating API Keys
Go to Developers Page
Navigate to Dashboard → Merchant → API & Development → API Keys
Create API Key
Click “Create API Key” to generate a new RSA key pair. The public key is sent to the server automatically.
Name and Configure
Give it a descriptive name (e.g., “Production Server”) and select the target environment (Test or Production).
Download Private Key
Download your private key immediately. It will not be shown again.
Deleting an API key is immediate and irreversible. Any requests signed with the deleted key will fail with 401 Unauthorized.
Authentication Method Comparison
| Feature | API Key | Store Slug |
|---|
| Server-side use | Yes | No |
| Client-side use | No | Yes |
| Requires X-Environment | No | Yes |
| Issue Session Token | Yes | No |
| Create Checkout Session | Yes | Yes |
| Product Management | Yes | No |
| GraphQL Queries | Yes | Yes (public data only) |
| Validity | Permanent (key-controlled) | - |
Authentication Errors
| Status | Error | Solution |
|---|
| 401 | Invalid signature | Check signing algorithm, private key, and timestamp freshness (5 min window) |
| 401 | Invalid or expired token | Re-authenticate or use a valid API Key |
| 403 | Insufficient permissions | Check if the role has access to the endpoint |
| 400 | Missing authentication | Ensure required headers are present |
{
"data": null,
"errors": [
{
"message": "Invalid signature",
"layer": "gateway"
}
]
}
Security Best Practices
- Never expose private keys in client-side code, version control, or public repositories
- Use HTTPS for all API requests
- Verify Webhook signatures to prevent forged requests
- Separate test and production keys — create distinct keys for each environment
- Rotate keys regularly, especially after team member changes
- Monitor API usage in the Dashboard for unusual activity