Security

How JoPay protects merchants, partners, and payments.

No custody model

Labs holds nothing. No user wallet key, no session key, no signing authority over merchant or payer funds. Payments flow from the payer's wallet through an immutable forward-only contract (the Protocol's postbox) to the merchant's wallet. The app observes and indexes; it does not sign.

JoPay is structurally two things: the Protocol (two immutable Solidity contracts, MIT-licensed, no admin, deterministic CREATE2 addresses) and Labs (the interface you interact with — app, pay page, dashboard, emails). Labs uses the Protocol to coordinate payments, but it is not the Protocol. The Protocol has no key JoPay could custody and no path to move funds anywhere except the merchant's pre-set address.

Concretely: no pool of funds to hack, no hot wallet to compromise, no withdrawal queue to exploit. If Labs went fully offline, merchant and payer wallets keep working on their own — only the interface is missing. If Labs were fully compromised, an attacker would still need to separately compromise Sequence (for WaaS merchants) and the user's active session to move any funds. Three independent custodians, not one.

See Non-custody for the full explainer — Labs/Protocol split, payment flow, what Labs holds (and what it doesn't), and the worst-case compromise walkthrough.

Webhook security

All webhook deliveries are signed using HMAC-SHA256. When a partner configures a webhook URL, JoPay generates a shared secret. Every webhook payload is signed with this secret, and the signature is included in the delivery header.

The signature format is:

v1={hmac_signature},t={unix_timestamp}

Partners must verify the HMAC signature before trusting any webhook payload. This prevents attackers from forging payment confirmations. See the Webhook events reference for verification code examples.

Session management

Merchant sessions use industry-standard cookie security:

AttributeValuePurpose
HttpOnlytruePrevents JavaScript from reading the session cookie, blocking XSS theft.
SecuretrueCookie is only sent over HTTPS connections.
SameSiteLaxPrevents the cookie from being sent in cross-site requests, mitigating CSRF.

Sessions have a fixed expiration period. When a session expires, the merchant is redirected to the login page. There is no "remember me" option — sessions must be re-established after expiry.

CSRF protection

All state-changing operations (creating payments, updating settings, sending funds) require a valid CSRF token. Tokens are generated server-side, bound to the user's session, and validated on every mutating request. CSRF tokens expire after 2 days.

Content Security Policy

JoPay enforces a strict Content Security Policy (CSP) on all pages. The policy restricts which scripts, styles, images, and connections the browser is allowed to load. This prevents XSS attacks by blocking inline scripts and unauthorized external resources.

The CSP is configured in the application proxy layer and applies to all merchant-facing and customer-facing pages.

On-chain proof verification

When a customer sends USDC, JoPay's verification engine (Trails) checks the blockchain directly to confirm the transfer. Verification checks include:

  • Correct recipient address (merchant's wallet)
  • Correct asset (USDC or EURC on the specified chain)
  • Correct amount (within acceptable precision)
  • Transaction finality (sufficient block confirmations)

Proofs are cryptographic — they cannot be faked. A payment is only marked as verified when the on-chain data matches the payment request exactly.

Never mark a payment as complete based solely on a webhook notification. Always verify the proof_status field is verified before fulfilling an order.

Rate limiting

JoPay applies rate limits to all API endpoints to prevent abuse. Limits are enforced per-IP, per-merchant, and per-partner depending on the endpoint. When a rate limit is exceeded, the API returns a 429 Too Many Requests response.

Rate limit counters are stored in short-lived buckets (7-day TTL) and are automatically purged. See the Rate limits reference for the complete table.

Tenant isolation

Every database query for merchant data includes the partner_id in the WHERE clause. This ensures that merchants belonging to one partner can never access or see data belonging to another partner's merchants, even if a request is malformed or tampered with.

This is enforced at the query level, not as an application-layer check. There is no "load then check" pattern — unauthorized data is never fetched from the database in the first place.

Infrastructure security

  • HTTPS only: All traffic is encrypted in transit. HTTP requests are redirected to HTTPS.
  • No public environment variables: Server-side API keys (Trails, RPC endpoints) are never exposed to the browser via NEXT_PUBLIC_* variables.
  • IP hashing: Rate limit keys use hashed IP addresses, so raw IPs are never stored in rate limit tables.
  • Request body size limits: All proxy routes enforce maximum body sizes to prevent denial-of-service via oversized payloads.