Authentication & Security
Authentication & Security
keshflippay partner APIs use API-key based HMAC signatures. All authenticated routes require the headers shown below. Some routes additionally enforce partner scopes and rate limits.
Request Headers
Send these headers with every authenticated request:
| Header | Description |
|---|---|
X-API-Key | Your partner API key. |
X-Timestamp | UNIX timestamp in seconds. |
X-Signature | HMAC-SHA256 of the string `METHOD |
BODYmust be the exact JSON string sent with the request (empty string forGET).PATHis the request path with version prefix, e.g./api/v1/crypto/withdrawals.
Signature Algorithm
Build the message and compute the signature as follows:
- Construct
stringToSign = METHOD + '|' + PATH + '|' + TIMESTAMP + '|' + BODY - Compute
signature = HMAC_SHA256(apiSecret, stringToSign).hex() - Send the result in the
X-Signatureheader.
- TypeScript / Node.js
- Python
- cURL (OpenSSL)
import crypto from "crypto";
export function sign({
method,
path,
body = "",
apiSecret,
timestamp = Math.floor(Date.now() / 1000).toString(),
}: {
method: string;
path: string; // e.g. "/api/v1/crypto/withdrawals"
body?: string; // exact JSON string ("" for GET)
apiSecret: string;
timestamp?: string;
}) {
const stringToSign = `${method}|${path}|${timestamp}|${body}`;
const signature = crypto
.createHmac("sha256", apiSecret)
.update(stringToSign)
.digest("hex");
return { timestamp, signature };
}
import hmac, hashlib, time
from typing import Tuple
def sign(method: str, path: str, body: str, api_secret: str, timestamp: str | None = None) -> Tuple[str, str]:
ts = timestamp or str(int(time.time()))
string_to_sign = f"{method}|{path}|{ts}|{body}"
sig = hmac.new(api_secret.encode("utf-8"), string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
return ts, sig
TIMESTAMP=$(date +%s)
METHOD=POST
PATH="/api/v1/crypto/deposits"
BODY='{"partnerId":"<partnerId>","asset":"USDC","chainId":"1","amount":"100.00","idempotencyKey":"dep_001"}'
SIGNATURE=$(printf "%s|%s|%s|%s" "$METHOD" "$PATH" "$TIMESTAMP" "$BODY" | openssl dgst -sha256 -hmac "<apiSecret>" -binary | xxd -p -c 256)
curl -sS -X $METHOD "https://api.keshflippay.com$PATH" -H "Content-Type: application/json" -H "X-API-Key: <apiKey>" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY"
Scopes
Some endpoints require specific partner scopes. Examples:
address:create— create crypto deposit addressesaddress:read— list partner addresseswithdrawal:create— initiate withdrawalstransaction:read— verify transactions
If a request is missing the required scope, the API returns 403 Forbidden.
Rate Limits
Per-partner hourly limits are enforced. Responses include rate-limit headers:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in the window. |
X-RateLimit-Remaining | Requests left in the current window. |
X-RateLimit-Reset | Seconds until the window resets. |
X-RateLimit-Window | Window size in seconds. |
Exceeding limits returns 429 Too Many Requests with machine-readable details.
Replay Protection and Timestamps
- Requests with timestamps outside the allowed skew window are rejected.
- Use a fresh timestamp per request.
- If you implement nonces, ensure nonce uniqueness per request and avoid reusing signed payloads.
- Keep server clocks in sync (e.g., via NTP) to prevent
timestamp out of rangeerrors.
Key Rotation
- New secrets can temporarily coexist with old secrets during a rotation window.
- Rotate secrets regularly and update your deployment environments atomically.
Webhook Security
Outbound webhooks are signed. Validate all incoming webhooks before processing.
| Header | Description |
|---|---|
X-Webhook-Event | Event name (e.g., crypto.deposit.updated). |
X-Webhook-Timestamp | Event timestamp. |
X-Webhook-Signature | HMAC-SHA256 over the raw JSON payload using your webhookSecret. |
Always verify against the raw request body and use a constant-time compare.
- Express (Node.js)
- FastAPI (Python)
import crypto from "crypto";
import express from "express";
const app = express();
app.post("/webhooks/keshflippay",
express.json({ verify: (req, _res, buf) => { (req as any).rawBody = buf; } }),
(req, res) => {
const signature = req.headers["x-webhook-signature"] as string | undefined;
const payload = (req as any).rawBody?.toString("utf8") ?? JSON.stringify(req.body);
const expected = crypto
.createHmac("sha256", process.env.WEBHOOK_SECRET!)
.update(payload)
.digest("hex");
const valid = crypto.timingSafeEqual(
Buffer.from(signature ?? "", "utf8"),
Buffer.from(expected, "utf8")
);
if (!valid) return res.status(401).json({ success: false, message: "Invalid signature" });
return res.json({ success: true });
}
);
app.listen(3000);
from fastapi import FastAPI, Header, Request, HTTPException
import hmac, hashlib, os
app = FastAPI()
WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET", "")
@app.post("/webhooks/keshflippay")
async def handler(request: Request, x_webhook_signature: str | None = Header(default=None)):
body = await request.body()
expected = hmac.new(WEBHOOK_SECRET.encode(), body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(x_webhook_signature or "", expected):
raise HTTPException(status_code=401, detail="Invalid signature")
return {"success": True}
Error Responses (Summary)
Common HTTP status codes:
| Code | Meaning |
|---|---|
400 | Validation error (malformed payload or missing fields). |
401 | Authentication failed (invalid signature/headers). |
403 | Forbidden (inactive partner, missing scope, or KYC not verified). |
404 | Not found. |
409 | Conflict (idempotency or duplicate request). |
429 | Rate limit exceeded. |
5xx | Server errors. |
See Errors & Troubleshooting for details.
Next Steps
- Proceed to Crypto APIs or Fiat APIs.
- Configure Webhooks for event-driven integrations.
- Browse the API Reference for all endpoints.