Security Model
The ItPay Protocol is designed with defense in depth — every API call is authenticated, every payload is verified, and the protocol never touches merchant funds. This page details the cryptographic foundations, anti-abuse measures, and data protection policies that secure the protocol.
1. HMAC-SHA256 Signature Verification
Every API request to the ItPay Gateway must carry an Authorization header proving the caller possesses a valid API key secret.
Understanding HMAC-SHA256 Signatures
HMAC-SHA256 (Hash-based Message Authentication Code with SHA-256) provides a way to verify both the authenticity and integrity of a message using a shared secret key. Unlike simple hashing, HMAC ensures that only parties possessing the secret can generate valid signatures, making it resistant to length-extension attacks and other cryptographic weaknesses.
The signing payload includes the request's timestamp, HTTP method, path, and body — every component that identifies the intent of the request is bound to the signature.
Request Signing
The signature is computed as:
Signature = HMAC-SHA256(
timestamp + method + path + body,
api_key_secret
)
Always include the raw request body in the signing payload. Never sign a pre-parsed or reformatted version of the body, as whitespace differences can produce a different signature.
Components:
| Component | Value | Example |
|---|---|---|
timestamp | Unix epoch seconds as a 10-digit string | 1745712345 |
method | Uppercase HTTP method | POST |
path | URL path (no query string) | /v1/payment-intents |
body | Raw request body (empty string for GET/DELETE) | {"amount":500,"currency":"USD"} |
How Signature Verification Works in Practice
When an agent sends a payment request to the ItPay Gateway, the agent creates an HMAC-SHA256 signature using its secret API key. The gateway looks up the agent's stored secret, recomputes the HMAC independently, and compares the two. A matching signature proves the agent possesses the secret key and that the request payload hasn't been altered in transit.
Signing Swimlane
The diagram below shows the complete request-signing lifecycle from the Agent composing the request through ItPay's verification and processing.
sequenceDiagram
participant Agent as Agent (API Consumer)
participant ItPay as ItPay Gateway
actor DB as Key Store
Agent->>Agent: Compose HTTP request (method, path, body)
Agent->>Agent: Read api_key_secret from env/config
Agent->>Agent: Compute timestamp = unix_epoch_seconds
Agent->>Agent: Build signing payload = timestamp + method + path + body
Agent->>Agent: signature = HMAC-SHA256(payload, api_key_secret)
Agent->>ItPay: POST /v1/payment-intents
Agent->>ItPay: Authorization: ItPay {agent_id}:{signature}
Agent->>ItPay: X-ItPay-Timestamp: {timestamp}
ItPay->>DB: Lookup stored secret for agent_id
DB-->>ItPay: api_key_secret (bcrypt-verified)
ItPay->>ItPay: Recompute HMAC-SHA256(timestamp + method + path + body, secret)
ItPay->>ItPay: Compare computed == received signature
alt Signature matches
ItPay->>ItPay: Process request
ItPay-->>Agent: 200 OK / 201 Created
else Signature mismatch
ItPay-->>Agent: 401 Unauthorized + error code
end
Header Format
Authorization: ItPay {agent_id}:{signature}
X-ItPay-Timestamp: {unix_timestamp}
agent_id— your public agent identifier (e.g.agent_abc123)signature— hex-encoded HMAC-SHA256 outputX-ItPay-Timestamp— the same Unix timestamp used in signing
Python Example
import hashlib
import hmac
import time
def sign_request(
api_key_secret: str,
method: str,
path: str,
body: str = "",
) -> tuple[str, str]:
"""Return (signature_hex, timestamp)."""
timestamp = str(int(time.time()))
payload = timestamp + method.upper() + path + body
signature = hmac.new(
key=api_key_secret.encode("utf-8"),
msg=payload.encode("utf-8"),
digestmod=hashlib.sha256,
).hexdigest()
return signature, timestamp
# Usage
secret = "sk_liv...f456"
sig, ts = sign_request(secret, "POST", "/v1/payment-intents", '{"amount":500,"currency":"USD"}')
headers = {
"Authorization": f"ItPay agent_abc123:{sig}",
"X-ItPay-Timestamp": ts,
"Content-Type": "application/json",
}
TypeScript Example
import { createHmac } from "node:crypto";
function signRequest(
apiKeySecret: string,
method: string,
path: string,
body: string = ""
): { signature: string; timestamp: string } {
const timestamp = String(Math.floor(Date.now() / 1000));
const payload = timestamp + method.toUpperCase() + path + body;
const signature = createHmac("sha256", apiKeySecret)
.update(payload)
.digest("hex");
return { signature, timestamp };
}
// Usage
const secret = "sk_liv...f456";
const { signature, timestamp } = signRequest(
secret,
"POST",
"/v1/payment-intents",
JSON.stringify({ amount: 500, currency: "USD" })
);
const headers = {
Authorization: `ItPay agent_abc123:${signature}`,
"X-ItPay-Timestamp": timestamp,
"Content-Type": "application/json",
};
Server-Side Verification
The gateway verifies every request by re-computing the HMAC with the stored secret for the claimed agent_id. If the signatures don't match — or the timestamp is outside the accepted window — the request is rejected with HTTP 401 Unauthorized.
The gateway stores only a bcrypt hash of API key secrets. The raw secret can never be retrieved — it can only be verified against the hash. If a secret is compromised, it must be rotated through the ItPay dashboard.
Error Scenarios
| Scenario | Condition | HTTP Status | Error Code | Response Body |
|---|---|---|---|---|
| Invalid signature | HMAC does not match | 401 Unauthorized | invalid_signature | {"error":"invalid_signature","message":"Signature verification failed"} |
| Expired timestamp | X-ItPay-Timestamp more than 5 min in the past | 401 Unauthorized | expired_timestamp | {"error":"expired_timestamp","message":"Timestamp is outside the accepted window"} |
| Premature timestamp | X-ItPay-Timestamp more than 5 min in the future | 401 Unauthorized | premature_timestamp | {"error":"premature_timestamp","message":"Timestamp is in the future beyond the allowed skew"} |
| Missing Authorization | No Authorization header | 401 Unauthorized | missing_authorization | {"error":"missing_authorization","message":"Authorization header is required"} |
| Missing Timestamp | No X-ItPay-Timestamp header | 401 Unauthorized | missing_timestamp | {"error":"missing_timestamp","message":"X-ItPay-Timestamp header is required"} |
| Unknown agent_id | agent_id in header does not exist | 401 Unauthorized | unknown_agent | {"error":"unknown_agent","message":"Agent not found"} |
Example: Rejected Invalid Signature
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": "invalid_signature",
"message": "Signature verification failed",
"request_id": "req_abc123"
}
2. Anti-Replay Protection
ItPay prevents replay attacks through a combination of time-bounded windows, nonce verification, and idempotency guarantees.
Understanding Anti-Replay Protection
A replay attack occurs when an attacker intercepts a valid API request and retransmits it to trick the server into performing the same action again — such as processing a duplicate payment. ItPay's anti-replay system uses three complementary layers to eliminate this risk:
- Timestamp window — Limits the time frame in which any request is valid
- Cryptographic nonce — Ensures each request is unique, even within the valid time window
- Idempotency keys — Guarantees at-most-once execution regardless of delivery attempts
How Anti-Replay Protection Works in Practice
When an agent sends a payment request, it includes a unique nonce along with its HMAC signature. The ItPay Gateway first verifies the signature, then checks that the timestamp falls within the 5-minute window. Finally, it records the nonce in Redis with a
SETNXcommand — if the nonce already exists, the request is rejected as a replay. This means even if an attacker captures the exact request and retransmits it within seconds, it will be rejected.
Anti-Replay Swimlane
sequenceDiagram
participant Agent as Agent (API Consumer)
participant ItPay as ItPay Gateway
participant Redis as Redis Cache
Agent->>Agent: Compute timestamp (unix epoch)
Agent->>Agent: Generate nonce (crypto.randomUUID)
Agent->>Agent: Sign payload = timestamp + nonce + method + path + body
Agent->>ItPay: POST /v1/payment-intents
Agent->>ItPay: Authorization: ItPay {agent_id}:{sig}
Agent->>ItPay: X-ItPay-Timestamp: {timestamp}
Agent->>ItPay: X-ItPay-Nonce: {nonce}
ItPay->>ItPay: Verify signature (see Section 1)
ItPay->>ItPay: Check |server_time - timestamp| <= 300 seconds
alt Timestamp outside 5-min window
ItPay-->>Agent: 401 Unauthorized (expired_timestamp)
else Timestamp valid
ItPay->>Redis: SETNX nonce:{nonce} "seen" EX 300
Redis-->>ItPay: 1 (first time) or 0 (already seen)
alt Nonce already used (replay)
ItPay-->>Agent: 409 Conflict (replay_detected)
else Fresh nonce
ItPay->>ItPay: Process request
ItPay-->>Agent: 200 OK
end
end
Timestamp Window
Every signed request is valid only within a 5-minute window around its X-ItPay-Timestamp:
- Requests with timestamps more than 5 minutes in the past are rejected as expired
- Requests with timestamps more than 5 minutes in the future are rejected as premature
- Clock skew between client and server is automatically accounted for within this window
Nonce Verification
Each state-mutating request carries a unique nonce to prevent replay within the timestamp window:
X-ItPay-Nonce: {cryptographically_random_uuid}
- The nonce is passed through
SETNXin Redis with a TTL matching the timestamp window - If a nonce has already been seen within the TTL, the request is rejected as a replay
- Nonces are generated client-side using
crypto.randomUUID()(Node.js) oros.urandom(Python)
Idempotency via Redis SETNX
State-mutating endpoints (creating PaymentIntents, processing callbacks) use Redis SETNX to guarantee at-most-once execution:
- The
payment_intent_idis used as the idempotency key SETNXreturns success only if the key has not been processed before- The key expires after the PaymentIntent's lifecycle completes
> SETNX payment_intent:pay_2xK3m9QrL8vN5pW1 "processing" EX 3600
(integer) 1 → first time, proceed
> SETNX payment_intent:pay_2xK3m9QrL8vN5pW1 "processing" EX 3600
(integer) 0 → already processed, return cached result
This ensures that even if a callback webhook is delivered multiple times (e.g. due to network retries), the same payment_intent_id callback is processed exactly once.
Python Nonce Example
import os
import hashlib
import hmac
import time
def sign_request_with_nonce(
api_key_secret: str,
method: str,
path: str,
body: str = "",
) -> tuple[str, str, str]:
timestamp = str(int(time.time()))
nonce = os.urandom(16).hex() # 32-char hex nonce
payload = timestamp + nonce + method.upper() + path + body
signature = hmac.new(
key=api_key_secret.encode("utf-8"),
msg=payload.encode("utf-8"),
digestmod=hashlib.sha256,
).hexdigest()
return signature, timestamp, nonce
TypeScript Nonce Example
import { createHmac, randomUUID } from "node:crypto";
function signRequestWithNonce(
apiKeySecret: string,
method: string,
path: string,
body: string = ""
): { signature: string; timestamp: string; nonce: string } {
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = randomUUID();
const payload = timestamp + nonce + method.toUpperCase() + path + body;
const signature = createHmac("sha256", apiKeySecret)
.update(payload)
.digest("hex");
return { signature, timestamp, nonce };
}
Error Scenarios
| Scenario | Condition | HTTP Status | Error Code | Response Body |
|---|---|---|---|---|
| Replay attack | Nonce already seen in cache | 409 Conflict | replay_detected | {"error":"replay_detected","message":"This request has already been processed"} |
| Expired request | Server time - timestamp > 300s | 401 Unauthorized | expired_timestamp | {"error":"expired_timestamp","message":"Timestamp is outside the accepted window"} |
| Duplicated idempotency key | SETNX returns 0 for PaymentIntent | 200 OK (cached) | — | Returns previously stored result |
3. QR Security
QR codes are the primary human-facing payment interface. ItPay employs multiple layers of protection to prevent QR-based fraud.
Dynamic QR Codes
Every QR is dynamically generated per payment request — never static or pre-printed. The QR encodes a unique, signed payload:
itpay://pay/pay_2xK3m9QrL8vN5pW1?channel=alipay&amount=5.00¤cy=USD&sig=hmac_hex
Always display a dynamic QR code on-screen rather than printing static codes. A static QR cannot be updated with new signatures or expiry times, making it vulnerable to replay and tampering.
Short Expiry (15 Minutes)
| Property | Value |
|---|---|
| QR lifetime | 15 minutes from generation |
| Post-expiry behavior | Gateway rejects the QR; payer must request a new one |
| Rationale | Limits the window for QR theft, screenshot reuse, or MitM attacks |
In-Channel Signature
The QR data carries an embedded HMAC-SHA256 signature that the payment channel verifies before presenting the checkout screen. This prevents tampering with the amount, currency, or destination after the QR is generated.
Single-Use Consumption
Once a QR is scanned and the associated PaymentIntent transitions to succeeded, the QR is permanently invalidated. Any subsequent attempt to scan the same QR returns an error.
4. Webhook Verification
ItPay signs every webhook payload with your registered webhook secret so you can verify the delivery came from ItPay and was not tampered with.
Understanding Webhook Verification
Webhook endpoints are publicly accessible by nature, making them a target for attackers who may attempt to forge callback notifications. ItPay's webhook signature scheme ensures your application can cryptographically verify that incoming webhooks genuinely originated from the ItPay Gateway and have not been modified in transit.
The scheme uses a timestamp + payload signing approach that prevents both forgery and replay attacks on your webhook endpoint.
How Webhook Verification Works in Practice
When a payment settlement event occurs, the ItPay Gateway computes an HMAC-SHA256 signature over the raw JSON body prepended with a timestamp. This signature is sent in the
X-ItPay-Signatureheader. Your application retrieves its stored webhook secret, recomputes the HMAC using the same raw body and timestamp, and compares the result using a constant-time comparison function. A match confirms the webhook is authentic and timely.
Webhook Verification Swimlane
sequenceDiagram
participant ItPay as ItPay Gateway
participant Agent as Agent (Webhook Receiver)
actor Store as Webhook Secret Store
Note over ItPay: Payment settlement event occurs
ItPay->>ItPay: Compute timestamp t = now()
ItPay->>ItPay: signed_payload = t + "." + raw_json_body
ItPay->>ItPay: signature = HMAC-SHA256(signed_payload, webhook_secret)
ItPay->>Agent: POST /webhooks/payment
ItPay->>Agent: X-ItPay-Signature: t={t},v1={signature}
ItPay->>Agent: Content-Type: application/json
Agent->>Store: Retrieve stored webhook_secret
Store-->>Agent: webhook_secret
Agent->>Agent: Parse X-ItPay-Signature header (t, v1)
Agent->>Agent: Check |now - t| <= max_age (300s default)
alt Timestamp expired
Agent-->>ItPay: Reject (ignore or log warning)
else Timestamp valid
Agent->>Agent: signed_payload = t + "." + raw_body
Agent->>Agent: computed = HMAC-SHA256(signed_payload, secret)
Agent->>Agent: constant_time_compare(computed, v1)
alt Signatures match
Agent->>Agent: Process webhook event
Agent-->>ItPay: 200 OK
else Signature mismatch
Agent-->>ItPay: 400 Bad Request (ignore event)
end
end
Signature Header
X-ItPay-Signature: t=1745712345,v1=abc123def456...
t— Unix timestamp of when the webhook was sentv1— hex-encoded HMAC-SHA256 of the raw JSON body
Verification Algorithm
- Extract the
tandv1values from theX-ItPay-Signatureheader - Compute the expected signature:
HMAC-SHA256(t + "." + raw_body, webhook_secret) - Compare the computed signature against
v1using constant-time comparison
Constant-Time Comparison
Always use a constant-time comparison function to prevent timing side-channel attacks:
import hashlib
import hmac
def verify_webhook_signature(
raw_body: bytes,
signature_header: str,
webhook_secret: str,
max_age_seconds: int = 300,
) -> bool:
"""
Verify a webhook signature. Returns True if valid, False otherwise.
"""
parts = dict(
item.split("=", 1)
for item in signature_header.split(",")
)
timestamp = parts.get("t")
expected_sig = parts.get("v1")
if not timestamp or not expected_sig:
return False
# Reject webhooks older than max_age_seconds
if int(time.time()) - int(timestamp) > max_age_seconds:
return False
signed_payload = f"{timestamp}.{raw_body.decode('utf-8')}"
computed_sig = hmac.new(
key=webhook_secret.encode("utf-8"),
msg=signed_payload.encode("utf-8"),
digestmod=hashlib.sha256,
).hexdigest()
# Constant-time comparison — do NOT use ==
return hmac.compare_digest(computed_sig, expected_sig)
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyWebhookSignature(
rawBody: string,
signatureHeader: string,
webhookSecret: string,
maxAgeSeconds: number = 300
): boolean {
const parts = Object.fromEntries(
signatureHeader.split(",").map((p) => p.split("=", 2))
);
const timestamp = parts["t"];
const expectedSig = parts["v1"];
if (!timestamp || !expectedSig) return false;
// Reject expired webhooks
if (Math.floor(Date.now() / 1000) - Number(timestamp) > maxAgeSeconds) {
return false;
}
const signedPayload = `${timestamp}.${rawBody}`;
const computedSig = createHmac("sha256", webhookSecret)
.update(signedPayload)
.digest("hex");
// Constant-time comparison
return timingSafeEqual(
Buffer.from(computedSig),
Buffer.from(expectedSig)
);
}
Never use standard string comparison (==) to verify webhook signatures. Python's hmac.compare_digest() or Node.js's timingSafeEqual() perform constant-time comparison that prevents attackers from inferring the signature byte-by-byte through timing measurements.
Webhook Error Scenarios
| Scenario | Condition | Action | Logged As |
|---|---|---|---|
| Missing signature header | No X-ItPay-Signature header | Reject webhook | missing_webhook_signature |
| Malformed signature | Header cannot be parsed | Reject webhook | malformed_webhook_signature |
| Expired webhook | Timestamp exceeds max_age | Reject webhook | expired_webhook |
| Signature mismatch | HMAC does not match | Reject webhook | invalid_webhook_signature |
| Unknown webhook secret | Agent rotates secret; old signature fails | Reject until secret updated | unknown_webhook_secret |
5. Fund Safety (不碰钱)
ItPay follows a strict no-fund-holding (不碰钱) architecture. The protocol never touches, pools, or controls merchant funds at any point in the payment lifecycle.
Understanding Fund Safety
The "不碰钱" (bù pèng qián — "don't touch the money") principle is the most important design constraint of the ItPay Protocol. Unlike payment aggregators (Stripe, Square) that collect funds into a merchant account and then disburse them, ItPay never intermediates funds at any stage. The gateway generates payment intents, tracks state, and provides reconciliation data — but the actual movement of money happens entirely between the payer's wallet and the merchant's account through the payment channel.
How Fund Safety Works in Practice
When a payer scans an ItPay-generated QR code, their payment app communicates directly with the payment channel (Alipay, WeChat, PromptPay). The channel verifies the QR's integrity independently using the embedded HMAC, then settles funds directly into the merchant's linked account. ItPay receives only a settlement callback notification — the money itself never passes through ItPay's infrastructure. This eliminates counterparty risk, regulatory burden, and the need for a money transmitter license.
Fund-Flow Diagram
The diagram below traces exactly how money moves — and where ItPay sits purely as a reconciliation layer.
sequenceDiagram
participant Payer as Payer Wallet
participant Channel as Payment Channel<br/>(Alipay/WeChat/PromptPay)
participant ItPay as ItPay Gateway<br/>(Reconciliation Only)
participant Merchant as Merchant Account
Note over Payer,Merchant: Money NEVER flows through ItPay
ItPay->>Payer: 1. Generate & present QR code
Payer->>Channel: 2. Scan QR, approve payment in app
Channel->>Channel: 3. Verify QR integrity (HMAC check)
Channel->>Merchant: 4. Settle funds directly to merchant
Channel->>ItPay: 5. Settlement callback (webhook)
ItPay->>ItPay: 6. Update PaymentIntent status
ItPay->>Merchant: 7. Reconciliation webhook
Merchant->>Merchant: 8. Reconcile settlement<br/>vs. ItPay records
Note over Payer,Merchant: I. ItPay initiates — generates QR, tracks state<br/>II. Channel handles — QR scan → fund transfer → settlement<br/>III. ItPay observes — receives callback, updates status<br/>IV. Merchant reconciles — compares channel settlement vs. ItPay ledger
Flow breakdown:
Payer ──→ Payment Channel (Alipay/WeChat/PromptPay) ──→ Merchant
│
Settlement notification
│
▼
ItPay Gateway
(reconciliation only)
- Payment initiation — ItPay generates the PaymentIntent and QR, but the actual payment flow goes directly between the payer and the payment channel
- Channel settlement — The payment channel (Alipay, WeChat, etc.) handles the entire funds transfer and settles directly to the merchant's own account
- Callback notification — The channel sends a settlement callback to ItPay's webhook endpoint
- Reconciliation only — ItPay tracks the state machine (PaymentIntent status, invoice status) but never intermediates the funds
Platform Fees
Platform operators collect their fees through one of two mechanisms:
- Channel split (preferred) — If the payment channel supports split settlement (e.g. Alipay's merchant sub-account split), the fee is deducted at the channel level before funds reach the merchant
- Manual invoice — Where channel split is unavailable, the platform issues a separate fee invoice to the merchant, settled via the same ItPay flow
Design Rationale
- Regulatory simplicity — No money transmitter or payment aggregator licenses required for gateway operators
- Counterparty risk elimination — Merchant funds are never at risk from the gateway operator's insolvency
- Audit clarity — Settlement happens at the channel level; ItPay's records are purely reconciliation data
6. KYC Levels
ItPay enforces tiered KYC (Know Your Customer) / KYB (Know Your Business) requirements. Higher verification levels unlock higher transaction limits.
| Level | Requirements | Daily Volume Limit | Per-Transaction Limit | Features |
|---|---|---|---|---|
| 0 — Anonymous | None (wallet address or phone only) | $100 | $10 | QR receive only, no webhooks |
| 1 — Basic | Phone + government ID verification | $1,000 | $100 | Full API access, webhooks |
| 2 — Enhanced | Level 1 + address proof + selfie verification | $10,000 | $1,000 | Higher rate limits, refund capability |
| 3 — Institutional | Level 2 + business registration + compliance questionnaire | $100,000+ | $10,000 | Custom limits, multi-user, reconciliation API |
Verification Flow
- Merchant submits documents via the ItPay dashboard or API
- Automated document verification (OCR + liveness check)
- Manual review for Levels 2 and 3 (24-48 hour SLA for Level 2, 3-5 business days for Level 3)
- Upon approval, the merchant's
kyc_levelfield is updated on their profile
Rate Limits by KYC Level
| Level | Requests / Second |
|---|---|
| 0 | 10 |
| 1 | 100 |
| 2 | 500 |
| 3 | 2,000+ (configurable) |
7. Encryption & Data Protection
Transport Security (TLS 1.3)
All ItPay API endpoints require TLS 1.3:
- HTTPS-only — no plain-text HTTP endpoints
- Certificate pinning is recommended for production SDK clients
- Perfect Forward Secrecy (PFS) enforced via ECDHE key exchange
- Minimum cipher suite:
TLS_AES_128_GCM_SHA256
mTLS for Channel Connections
For high-security connections between ItPay and payment channels (Alipay, WeChat, PromptPay), mutual TLS (mTLS) is used:
sequenceDiagram
participant ItPay as ItPay Gateway
participant Channel as Payment Channel<br/>(Alipay/WeChat)
ItPay->>Channel: ClientHello (TLS 1.3)
Channel->>ItPay: ServerHello + Certificate + CertificateRequest
ItPay->>ItPay: Verify channel's server certificate (CA chain)
ItPay->>Channel: Client Certificate (ItPay identity)
Channel->>Channel: Verify ItPay's client certificate
Channel->>ItPay: mTLS handshake complete
ItPay->>Channel: Encrypted API request (payment intent)
Channel->>ItPay: Encrypted API response
Note over ItPay,Channel: Both ends mutually authenticated
Note over ItPay,Channel: Traffic encrypted with TLS 1.3 session keys
| Property | Value |
|---|---|
| Protocol | TLS 1.3 (no fallback to 1.2) |
| Client auth | Required — ItPay presents a client certificate signed by a trusted CA |
| Server auth | Required — channel presents a server certificate from a trusted CA |
| Certificate rotation | Every 90 days via automated ACME/SPIFFE enrollment |
| Revocation | OCSP stapling enforced on every connection |
At-Rest Encryption
Sensitive data is encrypted at rest using AES-256-GCM:
| Data Type | Encryption | Key Management |
|---|---|---|
| KYC documents (ID scans, address proofs) | AES-256-GCM with per-document key | AWS KMS / GCP Cloud KMS with automatic key rotation |
| API key secrets | bcrypt hash (not reversible) | N/A — secrets are hashed, never stored in plaintext |
| PII fields (name, phone, address) | AES-256-GCM column-level encryption | Envelope encryption with monthly key rotation |
| Payment channel tokens | AES-256-GCM with HSM-backed master key | Hardware Security Module with annual rotation |
| Logs & audit trails | Signed with Ed25519 for tamper evidence | Public keys published for independent verification |
PII Minimization
ItPay follows the data minimization principle:
- Only the minimum required PII is collected for each KYC level
- PII is never shared with payment channels (ItPay passes only the PaymentIntent ID)
- KYC documents are auto-purged 90 days after verification completes (retained only for regulatory compliance periods where required)
- Logs are scrubbed of PII before storage —
agent_idreferences are used instead of real names or contact details - Merchant data is logically isolated per tenant using row-level security in the database
Security Headers (API Gateway)
All API responses include standard security headers:
| Header | Value |
|---|---|
Strict-Transport-Security | max-age=63072000; includeSubDomains; preload |
Content-Security-Policy | default-src 'none' |
X-Content-Type-Options | nosniff |
X-Frame-Options | DENY |
Cache-Control | no-store |
Summary
| Layer | Protection | Mechanism |
|---|---|---|
| API Authentication | Request origin verification | HMAC-SHA256 per-request signing + nonce |
| Anti-Replay | Prevents request reuse | 5-min timestamp window + nonce + Redis SETNX idempotency |
| QR Security | Prevents QR fraud | Dynamic generation, 15-min expiry, single-use consumption, in-channel HMAC |
| Webhook Integrity | Verifies callback authenticity | HMAC-SHA256 with constant-time comparison |
| Fund Safety | No fund intermediation | Channel-direct settlement, platform fees via split/invoice |
| Identity | Merchant trust | Tiered KYC Levels 0–3 |
| Data Protection | Encryption in transit and at rest | TLS 1.3, mTLS for channel connections, AES-256-GCM, PII minimization |