Security & Best Practices
Building financial, payment, and inventory integrations requires uncompromising security standards. Tracepos implements industry-standard defenses to protect merchant data, prevent financial discrepancies, and defend against network attacks.
1. Secret Key Management
Your X-Tracepos-Secret-Key is a confidential credential granting write access to your warehouse inventory and financial records.
Golden Rules
- Backend Only: Never store or transmit your Secret Key in client-side applications (React/Vue SPAs, iOS/Android mobile apps without an API gateway).
- Environment Variables: Store keys in environment variables (
process.env,.env, AWS Secrets Manager, Azure Key Vault). - Key Rotation: Rotate your API keys every 90 to 180 days from the Tracepos dashboard (Settings → API Keys). When rotating, generate a new key pair and test before revoking the legacy key.
2. Idempotency Keys: Preventing Duplicate Charges
In retail and POS terminal environments, network drops and cellular disconnects are common. If a request times out, retrying a POST /sales or POST /payments request without idempotency could result in:
- A customer being charged twice.
- Inventory being deducted twice.
- Duplicate accounting entries in the general ledger.
Implementation
Always send a client-generated unique identifier in the unique_id or Idempotency-Key header:
POST /api/v1/public/sales HTTP/1.1
Host: app.tracepos.net
X-Tracepos-Public-Key: YOUR_PUBLIC_KEY
X-Tracepos-Secret-Key: YOUR_SECRET_KEY
Idempotency-Key: pos-term-01-tx-99482716
Content-Type: application/json
{
"user_id": "cust_123",
"order_date": "2026-09-30",
"invoice_number": "INV-TX-99482716",
"product_items": [...]
}
If Tracepos receives a subsequent request with the same invoice_number or idempotency token within 24 hours, it returns the previously processed transaction without re-executing stock deductions or ledger entries.
3. HMAC-SHA256 Request Signing (Optional High-Security Mode)
For high-volume financial aggregators and unattended POS kiosks, Tracepos supports request signing via HMAC-SHA256 using your key's hmac_secret:
Signature Recipe
- Construct the canonical string:
TIMESTAMP.METHOD.PATH.REQUEST_BODY - Compute the HMAC-SHA256 hash using your
hmac_secret. - Transmit the signature in
X-Tracepos-Signatureand timestamp inX-Tracepos-Timestamp.
Node.js Example
const crypto = require('crypto');
function signRequest(method, path, bodyString, hmacSecret) {
const timestamp = Math.floor(Date.now() / 1000);
const payload = `${timestamp}.${method.toUpperCase()}.${path}.${bodyString}`;
const signature = crypto
.createHmac('sha256', hmacSecret)
.update(payload)
.digest('hex');
return {
'X-Tracepos-Timestamp': timestamp.toString(),
'X-Tracepos-Signature': signature,
};
}
4. Replay Attack Defense
Tracepos rejects requests where the client timestamp drifts by more than 300 seconds (5 minutes) from server UTC time. Ensure your server or POS terminal hardware synchronizes with NTP (Network Time Protocol) servers.
5. Webhook Signature Verification
When Tracepos sends webhook notifications to your application (e.g. sales.created), your server must verify the payload signature before processing:
Tracepos sends:
Authorization: Bearer <WEBHOOK_SECRET>X-Tracepos-Signature: <HMAC_SHA256_HEX>
Python Webhook Verifier
import hmac
import hashlib
def verify_tracepos_webhook(raw_payload_bytes: bytes, signature_header: str, webhook_secret: str) -> bool:
expected_signature = hmac.new(
webhook_secret.encode('utf-8'),
raw_payload_bytes,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected_signature, signature_header)
6. Rate Limiting & Graceful Backoff
When making bulk requests (e.g. catalog ingestion or batch sync), honor the HTTP 429 Too Many Requests status:
Recommended Backoff Pattern
When a 429 is encountered:
- Inspect the
Retry-Afterheader (seconds). - If
Retry-Afteris absent, apply Exponential Backoff with Full Jitter: $$\text{Sleep} = \text{random}(0, \min(\text{MAX_BACKOFF}, \text{BASE} \times 2^{\text{attempt}}))$$ - Do not immediately retry in a tight loop.
async function requestWithRetry(fn, maxRetries = 4) {
let attempt = 0;
while (attempt < maxRetries) {
try {
return await fn();
} catch (err) {
if (err.response && err.response.status === 429) {
attempt++;
const retryAfter = parseInt(err.response.headers['retry-after'] || '2', 10);
const jitter = Math.random() * 1000;
const delay = (retryAfter * 1000) + jitter;
console.warn(`Rate limited. Backing off for ${delay}ms (attempt ${attempt}/${maxRetries})`);
await new Promise(res => setTimeout(res, delay));
} else {
throw err;
}
}
}
}
