{"openapi":"3.1.0","info":{"title":"AEVION QPayNet","version":"1.0.0","description":"Embedded payment infrastructure for AEVION ecosystem (KZT). HMAC-signed webhooks, idempotent transfers, in-app wallets, merchant API keys, payment links.","contact":{"name":"AEVION","url":"https://aevion.app","email":"support@aevion.app"},"license":{"name":"Proprietary"}},"servers":[{"url":"https://api.aevion.app/api/qpaynet","description":"Production"}],"tags":[{"name":"Wallets"},{"name":"Transactions"},{"name":"Transfers"},{"name":"Deposits"},{"name":"Merchants"},{"name":"PaymentRequests"},{"name":"Webhooks"},{"name":"Public"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"},"merchantKey":{"type":"apiKey","in":"header","name":"X-API-Key"}},"schemas":{"Wallet":{"type":"object","required":["id","name","currency","balance","status"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"currency":{"type":"string","example":"KZT"},"balance":{"type":"number","description":"Available balance in KZT"},"status":{"type":"string","enum":["active","frozen","closed"]},"metadata":{"type":"object","description":"Partner-supplied data (max 4KB)","nullable":true}}},"Transaction":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"wallet_id":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["deposit","withdraw","transfer_in","transfer_out","merchant_charge","refund"]},"amount":{"type":"number"},"fee":{"type":"number"},"description":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"}}},"TransferRequest":{"type":"object","required":["fromWalletId","toWalletId","amount"],"properties":{"fromWalletId":{"type":"string","format":"uuid"},"toWalletId":{"type":"string","format":"uuid"},"amount":{"type":"number","minimum":1,"description":"KZT (max 100000 by default)"},"description":{"type":"string","maxLength":200}}},"ValidationError":{"type":"object","properties":{"error":{"type":"string","const":"validation_failed"},"field":{"type":"string"},"reason":{"type":"string"}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Stable machine-readable code"}}}},"responses":{"ValidationFailed":{"description":"Input validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"AuthRequired":{"description":"Bearer token missing or invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"security":[{"bearerAuth":[]}],"paths":{"/health":{"get":{"tags":["Public"],"summary":"Service health + pool stats","security":[],"responses":{"200":{"description":"OK"}}}},"/stats":{"get":{"tags":["Public"],"summary":"Aggregate ecosystem stats","security":[],"responses":{"200":{"description":"OK"}}}},"/wallets":{"get":{"tags":["Wallets"],"summary":"List my wallets","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/AuthRequired"}}},"post":{"tags":["Wallets"],"summary":"Create wallet (with optional partner metadata)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":80},"currency":{"type":"string","default":"KZT"},"metadata":{"type":"object","description":"Partner-supplied JSONB; max 4KB. Useful for merchant_order_id, customer_ref, etc."}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Wallet"}}}}}}},"/wallets/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Wallets"],"summary":"Wallet detail (owner)","responses":{"200":{"description":"OK"},"404":{"description":"Not found"}}},"patch":{"tags":["Wallets"],"summary":"Rename or update metadata. Pass `metadata: null` to clear.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":80},"metadata":{"type":"object","nullable":true}}}}}},"responses":{"200":{"description":"OK"},"400":{"$ref":"#/components/responses/ValidationFailed"}}}},"/wallets/{id}/public":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Public"],"summary":"Recipient lookup (no balance)","security":[],"responses":{"200":{"description":"OK"},"404":{"description":"Not found"}}}},"/wallets/{id}/close":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"post":{"tags":["Wallets"],"summary":"Close wallet (terminal). Requires zero balance + no pending payouts.","responses":{"200":{"description":"OK"},"400":{"description":"balance_must_be_zero / pending_payouts_must_complete / wallet_frozen / already_closed"},"404":{"description":"Not found"}}}},"/deposit":{"post":{"tags":["Deposits"],"summary":"Top up wallet (test/sandbox)","parameters":[{"name":"Idempotency-Key","in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["walletId","amount"],"properties":{"walletId":{"type":"string","format":"uuid"},"amount":{"type":"number"},"description":{"type":"string"}}}}}},"responses":{"200":{"description":"OK"},"400":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/deposit/checkout":{"post":{"tags":["Deposits"],"summary":"Stripe Checkout Session (real cards)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["walletId","amount"],"properties":{"walletId":{"type":"string"},"amount":{"type":"number","minimum":100}}}}}},"responses":{"200":{"description":"Returns Stripe checkout URL or stub URL when STRIPE_SECRET_KEY is unset"}}}},"/withdraw":{"post":{"tags":["Transactions"],"summary":"Withdraw from wallet (0.1% fee)","parameters":[{"name":"Idempotency-Key","in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["walletId","amount"],"properties":{"walletId":{"type":"string"},"amount":{"type":"number"},"description":{"type":"string"}}}}}},"responses":{"200":{"description":"OK"},"400":{"description":"insufficient_balance / wallet_inactive / validation"}}}},"/transfer":{"post":{"tags":["Transfers"],"summary":"P2P transfer (0.1% fee, atomic, KYC-aware)","parameters":[{"name":"Idempotency-Key","in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferRequest"}}}},"responses":{"200":{"description":"OK"},"400":{"description":"insufficient/inactive/same_wallet/over_max"},"403":{"description":"kyc_required"}}}},"/transactions":{"get":{"tags":["Transactions"],"summary":"Transaction history","parameters":[{"name":"walletId","in":"query","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/transactions.csv":{"get":{"tags":["Transactions"],"summary":"CSV export (max 5000 rows, 5/min)","responses":{"200":{"description":"text/csv"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/merchant/keys":{"post":{"tags":["Merchants"],"summary":"Issue merchant API key (shown once)","responses":{"201":{"description":"Created"}}},"get":{"tags":["Merchants"],"summary":"List my merchant keys","responses":{"200":{"description":"OK"}}}},"/merchant/keys/{id}":{"delete":{"tags":["Merchants"],"summary":"Revoke key","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}}},"/merchant/charge":{"post":{"tags":["Merchants"],"summary":"Charge a customer wallet via API key","security":[{"merchantKey":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","schema":{"type":"string"}},{"name":"X-Aevion-Timestamp","in":"header","schema":{"type":"string"},"description":"Unix seconds. REQUIRED when key was created with requireSignature=true. Reject window 5 minutes."},{"name":"X-Aevion-Signature","in":"header","schema":{"type":"string"},"description":"sha256=<hex(hmac(apiKey, `${ts}.${rawBody}`))>. REQUIRED when requireSignature=true."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["amount","customerWalletId"],"properties":{"amount":{"type":"number"},"customerWalletId":{"type":"string","format":"uuid"},"description":{"type":"string"}}}}}},"responses":{"200":{"description":"OK"},"400":{"description":"Validation / cannot_charge_own_wallet"},"401":{"description":"signature_required / signature_timestamp_drift / signature_mismatch"},"403":{"description":"invalid_or_revoked_key / scope_missing"}}}},"/requests":{"post":{"tags":["PaymentRequests"],"summary":"Create a payment link","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["toWalletId","amount","description"],"properties":{"toWalletId":{"type":"string","format":"uuid"},"amount":{"type":"number"},"description":{"type":"string","maxLength":200},"note":{"type":"string"},"expiresAt":{"type":"string"},"notifyUrl":{"type":"string","format":"uri"}}}}}},"responses":{"201":{"description":"Created (returns notifySecret once)"}}},"get":{"tags":["PaymentRequests"],"summary":"List my payment requests","responses":{"200":{"description":"OK"}}}},"/requests/{token}":{"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"get":{"tags":["Public"],"summary":"View payment request (no auth)","security":[],"responses":{"200":{"description":"OK"}}}},"/requests/{token}/pay":{"post":{"tags":["PaymentRequests"],"summary":"Pay a request (auth required)","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["fromWalletId"],"properties":{"fromWalletId":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"OK (fires HMAC-signed webhook async)"}}}},"/webhook-subs":{"post":{"tags":["Webhooks"],"summary":"Subscribe to events (set-once secret returned)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"},"events":{"type":"string","default":"payment_request.paid"}}}}}},"responses":{"201":{"description":"Created"}}},"get":{"tags":["Webhooks"],"summary":"List my subscriptions","responses":{"200":{"description":"OK"}}}},"/webhook-subs/{id}":{"delete":{"tags":["Webhooks"],"summary":"Revoke subscription","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}}},"/webhooks/test":{"post":{"tags":["Webhooks"],"summary":"Smoke-test your webhook URL (HMAC roundtrip)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url","secret"],"properties":{"url":{"type":"string","format":"uri"},"secret":{"type":"string","minLength":16}}}}}},"responses":{"200":{"description":"Returns delivery result + payload"}}}},"/openapi.json":{"get":{"tags":["Public"],"summary":"This document","security":[],"responses":{"200":{"description":"OK"}}}}},"x-error-codes":{"description":"Stable machine-readable error codes returned in `error` field. Designed to be matched directly by partner code without parsing free-form messages.","validation":{"validation_failed":"Input failed validation. `field` and `reason` give details.","metadata_too_large":"Wallet metadata > 4096 bytes."},"auth":{"auth_required":"Bearer token missing or invalid.","not_admin":"Caller email not in QPAYNET_ADMIN_EMAILS allowlist.","invalid_or_revoked_key":"Merchant API key (X-API-Key) unknown or revoked."},"money":{"insufficient_balance":"Wallet balance below requested amount + fee.","wallet_inactive":"Wallet is frozen or closed; money paths reject.","wallet_not_found":"Wallet id does not exist or not owned by caller.","cannot_transfer_to_same_wallet":"fromWalletId === toWalletId.","cannot_charge_own_wallet":"Merchant cannot charge their own wallet via API key.","transfer_amount_exceeds_max":"amount > QPAYNET_MAX_TRANSFER (default 100000 KZT).","daily_deposit_cap_exceeded":"Sum of last 24h deposits would exceed QPAYNET_DAILY_DEPOSIT_CAP.","kyc_required":"Monthly outgoing > QPAYNET_KYC_THRESHOLD; submit KYC at /qpaynet/kyc."},"idempotency":{"idempotency_key_body_mismatch":"Same Idempotency-Key but different body (409). Generate a fresh key."},"lifecycle":{"balance_must_be_zero":"Wallet close requires zero balance — withdraw or transfer first.","pending_payouts_must_complete":"Wallet close blocked by in-flight payouts.","already_closed":"Wallet already in 'closed' state.","wallet_frozen_contact_support":"Cannot close a frozen wallet — contact support to unfreeze first.","tx_already_refunded":"Original transaction already has a corresponding refund row.","tx_type_not_refundable":"Only deposit / transfer_in / merchant_charge are refundable.","refund_exceeds_original":"Partial refund > original amount."},"rate":{"rate_limit_exceeded":"Too many requests. Money limiter: 30/min default; CSV: 5/min; auth-read: 120/min. Tier overrides via QPAYNET_RATE_TIERS."}},"x-webhook-contract":{"eventTypes":["payment_request.paid"],"headers":{"X-Aevion-Event":"Event type (e.g. payment_request.paid)","X-Aevion-Event-Id":"Stable event identifier across retries. Use this as the dedup key on your side — same event-id means same logical event, regardless of how many times we retry.","X-Aevion-Timestamp":"Unix seconds. Reject if older than 5 minutes (replay protection).","X-Aevion-Signature":"sha256=<hex(hmac(secret, `${timestamp}.${rawBody}`))>"},"retries":"5 attempts, exp-backoff 30s/2m/10m/30m/2h. After exhaustion, dead-letter — visible via /admin/webhook-deliveries. Same X-Aevion-Event-Id across all attempts of a logical event.","verification":"Verify signature BEFORE parsing body. Use constant-time comparison. Reject if timestamp drift > 5 minutes. Dedupe by X-Aevion-Event-Id (we recommend INSERT ON CONFLICT DO NOTHING)."}}