{"openapi":"3.0.3","info":{"title":"Hostra API","version":"1.0.0","description":"Aggregated OpenAPI document for all Hostra bounded contexts. See /v1/{context}/openapi.json for per-context specs."},"paths":{"/auth/login":{"post":{"summary":"Log in","operationId":"login","description":"Verifies email+password and sets a fresh session cookie. Login is permitted before email verification; the response carries email_verified so the frontend can prompt.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}}},"responses":{"200":{"description":"Session cookie set.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"400":{"description":"Malformed JSON body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Invalid credentials (no email enumeration).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Account disabled.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded (per IP).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/auth/logout":{"post":{"summary":"Log out","operationId":"logout","description":"Revokes the session and clears the cookie. Idempotent. Requires a session cookie.","security":[{"sessionCookie":[]}],"responses":{"204":{"description":"Session revoked; cookie cleared."},"401":{"description":"No session.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/auth/m2m/register":{"post":{"summary":"Register a new merchant account","operationId":"registerMerchant","description":"Provisions a new Active merchant, issues the first API key with the configured default scopes, and (when notification is enabled) delivers a welcome email to the supplied address. The plaintext API key is returned exactly once. This endpoint is public (no authentication) and is protected by per-IP rate limiting; a browser SPA may call it directly (CORS is handled by the platform).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}}},"responses":{"201":{"description":"Merchant provisioned and the first API key issued. The plaintext key is returned exactly once.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterResponse"}}}},"400":{"description":"Malformed JSON body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Validation failure (empty organization name or invalid email).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded (per IP); retry later. See Retry-After.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal error.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"Self-service registration is disabled on this deployment.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/auth/me":{"get":{"summary":"Current identity","operationId":"whoAmI","description":"Returns the identity behind the session cookie. Requires a session. Alias: GET /v1/profile.","security":[{"sessionCookie":[]}],"responses":{"200":{"description":"Identity.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeResponse"}}}},"401":{"description":"No/invalid session.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"patch":{"summary":"Update the human profile","operationId":"updateMe","description":"Edits the human profile (display name today; email/password changes are separate flows). Requires a session. Returns the refreshed identity. Alias: PATCH /v1/profile.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeUpdateRequest"}}}},"responses":{"200":{"description":"Profile updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeResponse"}}}},"401":{"description":"No/invalid session.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Empty name.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/auth/password-reset/confirm":{"post":{"summary":"Confirm a password reset","operationId":"confirmPasswordReset","description":"Consumes a single-use reset token and sets a new password. Public.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordResetConfirmRequest"}}}},"responses":{"204":{"description":"Password updated."},"410":{"description":"Token invalid, expired, or already used.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Weak password.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/auth/password-reset/request":{"post":{"summary":"Request a password reset","operationId":"requestPasswordReset","description":"Always returns 202 (no email enumeration). If the email is registered, a reset link is sent.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordResetRequest"}}}},"responses":{"202":{"description":"Always returned; a reset email is sent if the email is registered."},"400":{"description":"Malformed JSON body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/auth/register":{"post":{"summary":"Register a human account","operationId":"registerUser","description":"Creates a user, an owned merchant, and a session; sets the hostra_session cookie; sends a verification email (best-effort). Public (no credential).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}}},"responses":{"201":{"description":"Account created; session cookie set.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterResponse"}}}},"400":{"description":"Malformed JSON body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Email already registered.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Validation failure (empty name, invalid email, weak password).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded (per IP).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal error.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/auth/verify-email":{"post":{"summary":"Verify email","operationId":"verifyEmail","description":"Consumes a single-use verification token delivered by email. Public.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyEmailRequest"}}}},"responses":{"204":{"description":"Email verified."},"400":{"description":"Missing token.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"410":{"description":"Token invalid, expired, or already used.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/auth/verify-email/resend":{"post":{"summary":"Resend the verification email","operationId":"resendVerificationEmail","description":"Issues a fresh email-verification token for the authenticated user and enqueues the verification email. Session-required (a visitor cannot trigger emails to arbitrary addresses — anti-enumeration). Idempotent in effect: repeated calls issue new tokens; old ones remain valid until expiry/consumption. Returns 204 on success regardless of whether the notifier is enabled (Sent is server-side observability, not exposed to the client).","security":[{"sessionCookie":[]}],"responses":{"204":{"description":"Verification email re-issued (or token persisted when notifications are disabled)."},"401":{"description":"No/invalid session.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal error.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/profile":{"get":{"summary":"Current identity (profile alias)","operationId":"getProfile","description":"Frontend-facing alias of GET /v1/auth/me. Same session cookie, same MeResponse.","security":[{"sessionCookie":[]}],"responses":{"200":{"description":"Identity.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeResponse"}}}},"401":{"description":"No/invalid session.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"patch":{"summary":"Update profile (alias)","operationId":"patchProfile","description":"Frontend-facing alias of PATCH /v1/auth/me. Body: { \"name\": \"...\" }.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeUpdateRequest"}}}},"responses":{"200":{"description":"Profile updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeResponse"}}}},"401":{"description":"No/invalid session.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Empty name.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/quotes":{"post":{"summary":"Create a quote","description":"Compute and persist a server-side quote for a card_to_crypto transfer. The quote is valid for 60s (expires_at). Repeated calls produce distinct quotes (each gets a fresh quote_id); idempotency is NOT required at the quote layer.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateQuoteRequest"}}}},"responses":{"201":{"description":"Quote created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Quote"}}}},"422":{"description":"Validation failed","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"400":{"description":"Malformed JSON","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/quotes/docs":{"get":{"summary":"Swagger UI","responses":{"200":{"description":"Swagger UI HTML"}}}},"/quotes/openapi.json":{"get":{"summary":"OpenAPI document","responses":{"200":{"description":"OpenAPI 3.0 JSON"}}}},"/quotes/{id}":{"get":{"summary":"Fetch a quote","description":"Retrieve a quote by its id. Returns 410 Gone when the quote is past expires_at (the row remains for audit, but a transfer bound to it is rejected).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Quote","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Quote"}}}},"404":{"description":"Quote not found","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"410":{"description":"Quote expired","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/settlements":{"post":{"summary":"Submit a settlement","operationId":"submitSettlement","security":[{"IdempotencyKey":[]},{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitRequest"}}}},"responses":{"201":{"description":"Settlement accepted","headers":{"Location":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Settlement"}}}},"400":{"description":"Malformed JSON","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, unknown, revoked, or expired API key","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Authenticated merchant is suspended or disabled","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Validation failure","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded (per API key or per IP); retry later. See Retry-After.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"get":{"summary":"List settlements","operationId":"listSettlements","security":[{"BearerAuth":[]}],"parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["pending","in_flight","retrying","succeeded","failed","reversed"]}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}],"responses":{"200":{"description":"A page of settlements (only the caller's own, per merchant isolation)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListResponse"}}}},"401":{"description":"Missing, unknown, revoked, or expired API key","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Authenticated merchant is suspended or disabled","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Invalid filter","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded; retry later. See Retry-After.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/settlements/{id}":{"get":{"summary":"Fetch a settlement by idempotency key","operationId":"getSettlement","security":[{"BearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The settlement (only if owned by the caller's merchant)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Settlement"}}}},"401":{"description":"Missing, unknown, revoked, or expired API key","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Authenticated merchant is suspended or disabled","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Not found, or owned by another merchant (cross-merchant access is reported as 404 to avoid an existence oracle)","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded; retry later. See Retry-After.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/settlements/{id}/reverse":{"post":{"summary":"Reverse an approved settlement","operationId":"reverseSettlement","security":[{"BearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReverseRequest"}}}},"responses":{"200":{"description":"Reversal completed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReverseResponse"}}}},"202":{"description":"Reversal pending at provider","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"Missing, unknown, revoked, or expired API key","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Authenticated merchant is suspended or disabled","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Not found, or owned by another merchant","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Not reversible or declined","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Validation failure","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded; retry later. See Retry-After.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/transfers":{"post":{"summary":"Create a card_to_crypto transfer","description":"Creates a parent saga over two linked child settlements. The accepted quote_id binds the executed transfer to the quote the client accepted (no client-side fee tampering). Idempotent on the Idempotency-Key header.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTransferRequest"}}}},"responses":{"201":{"description":"Transfer created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Transfer"}}}},"422":{"description":"Validation failed","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"400":{"description":"Malformed JSON","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"410":{"description":"Accepted quote expired","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"get":{"summary":"List transfers (server-authoritative history)","description":"Returns a page of transfer sagas, most recent first, with cursor/offset pagination (ADR-010 Phase 4 follow-up). Optional ?status= filters by saga lifecycle state. Merchant isolation: when auth is enabled, only the authenticated merchant's transfers are returned. Requires the transfers:read scope.","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["pending","leg1_in_flight","leg1_succeeded","leg2_in_flight","succeeded","compensating","failed"]}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0}}],"responses":{"200":{"description":"Paginated list of transfers","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferListResponse"}}}},"422":{"description":"Invalid query parameter","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/transfers/docs":{"get":{"summary":"Swagger UI","responses":{"200":{"description":"Swagger UI HTML"}}}},"/transfers/openapi.json":{"get":{"summary":"OpenAPI document","responses":{"200":{"description":"OpenAPI 3.0 JSON"}}}},"/transfers/{id}":{"get":{"summary":"Fetch a transfer","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Transfer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Transfer"}}}},"404":{"description":"Transfer not found","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/card-art":{"get":{"tags":["Cards"],"summary":"List card art designs","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CardArt"}}}}}}}}}},"/v1/cards":{"get":{"tags":["Cards"],"summary":"List cards for the authenticated principal","description":"Web/session: omit user_id — identity comes from the session cookie Principal. M2M API-key callers may pass ?user_id= as a fallback. Without a session and without user_id → 401.","parameters":[{"name":"user_id","in":"query","required":false,"schema":{"type":"string"},"description":"M2M only. Web clients MUST omit."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Card"}}}}}}},"401":{"$ref":"#/components/responses/Problem"},"403":{"$ref":"#/components/responses/Problem"}}},"post":{"tags":["Cards"],"summary":"Issue card","description":"Resolves internal CardAccount for the authenticated user (session) or body.user_id (M2M), issues via IssuerProvider, activates. Returns 409 ACTIVE_CARD_ALREADY_EXISTS if an active/frozen virtual card already exists.","requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCardRequest"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Card"}}}},"400":{"$ref":"#/components/responses/Problem"},"409":{"description":"ACTIVE_CARD_ALREADY_EXISTS","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"502":{"$ref":"#/components/responses/Problem"}}}},"/v1/cards/{id}":{"get":{"tags":["Cards"],"summary":"Get card detail","description":"Returns status, network, expiry, wallet_status, last_four, cardholder, display_label.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Card"}}}},"404":{"$ref":"#/components/responses/Problem"}}},"patch":{"tags":["Cards"],"summary":"Rename card (display_name)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"display_name":{"type":"string"}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Card"}}}},"404":{"$ref":"#/components/responses/Problem"}}}},"/v1/cards/{id}/balance":{"get":{"tags":["Controls"],"summary":"Card balance projection","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Balance"}}}}}}},"/v1/cards/{id}/close":{"post":{"tags":["Cards"],"summary":"Close card","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK — status=closed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Card"}}}},"404":{"$ref":"#/components/responses/Problem"}}}},"/v1/cards/{id}/events":{"get":{"tags":["Controls"],"summary":"Card activity events","description":"Cabinet Activity feed. Empty data is valid until auth/clear webhooks populate the projection. Path is /v1/... — never /api/v1/...","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CardEvent"}}}}}}},"401":{"$ref":"#/components/responses/Problem"},"404":{"$ref":"#/components/responses/Problem"}}}},"/v1/cards/{id}/freeze":{"post":{"tags":["Cards"],"summary":"Freeze card","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK — status=frozen","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Card"}}}},"409":{"description":"CARD_ALREADY_FROZEN","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"$ref":"#/components/responses/Problem"}}}},"/v1/cards/{id}/limits":{"get":{"tags":["Controls"],"summary":"Get spend limits","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Limits"}}}}}},"patch":{"tags":["Controls"],"summary":"Update spend limits","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Limits"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Limits"}}}}}}},"/v1/cards/{id}/replace":{"post":{"tags":["Cards"],"summary":"Replace card (lost/stolen)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"New card; old status=blocked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Card"}}}},"404":{"$ref":"#/components/responses/Problem"}}}},"/v1/cards/{id}/secure-details":{"post":{"tags":["Controls"],"summary":"Reveal PAN/CVV (time-boxed)","description":"PCI reveal. Loopback → mode=demo with pan/cvv JSON. Lithic → mode=embed with embed_url iframe (PAN never touches Hostra). Pass target_origin for Lithic (e.g. https://hostracore.com).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"password":{"type":"string","description":"Optional step-up password from the cabinet UI."},"target_origin":{"type":"string","description":"Browser origin for Lithic embed iframe, e.g. https://hostracore.com"}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecureDetails"}}}},"401":{"$ref":"#/components/responses/Problem"},"404":{"$ref":"#/components/responses/Problem"}}}},"/v1/cards/{id}/transactions":{"get":{"tags":["Controls"],"summary":"Card transactions","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Transaction"}}}}}}}}}},"/v1/cards/{id}/unfreeze":{"post":{"tags":["Cards"],"summary":"Unfreeze card","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK — status=active","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Card"}}}},"409":{"description":"CARD_NOT_FROZEN","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"$ref":"#/components/responses/Problem"}}}},"/v1/cards/{id}/wallet/apple":{"post":{"tags":["Wallet"],"summary":"Provision Apple Pay","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletProvisionRequest"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletProvisionResponse"}}}},"501":{"$ref":"#/components/responses/Problem"}}}},"/v1/cards/{id}/wallet/google":{"post":{"tags":["Wallet"],"summary":"Provision Google Pay","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletProvisionRequest"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletProvisionResponse"}}}},"501":{"$ref":"#/components/responses/Problem"}}}},"/v1/cards/{id}/wallet/status":{"get":{"tags":["Wallet"],"summary":"Wallet token status","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletStatus"}}}},"404":{"$ref":"#/components/responses/Problem"}}}},"/webhooks/endpoint":{"put":{"summary":"Register or replace the merchant webhook endpoint","description":"Stores the merchant's HTTPS URL + HMAC secret. The secret is never returned in cleartext (GET returns a masked preview). Requires scope webhooks:write.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointRequest"}}}},"responses":{"204":{"description":"Stored"},"422":{"description":"Invalid URL/secret","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Merchant context required","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"get":{"summary":"Read the merchant webhook endpoint","description":"Returns the merchant's webhook config with a masked secret. Requires scope webhooks:read.","responses":{"200":{"description":"Webhook config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointResponse"}}}},"404":{"description":"No webhook configured","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"delete":{"summary":"Remove the merchant webhook endpoint","description":"Deletes the merchant's webhook config; subsequent transfer-status events are soft-skipped (no recipient). Requires scope webhooks:write.","responses":{"204":{"description":"Deleted"}}}},"/webhooks/openapi.json":{"get":{"summary":"OpenAPI document","responses":{"200":{"description":"OpenAPI 3.0 JSON"}}}},"/webhooks/replay":{"post":{"summary":"Re-queue all events for a transfer","description":"Re-arms delivered and dead-lettered events for the given transfer for immediate re-delivery. Requires scope webhooks:write.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplayRequest"}}}},"responses":{"200":{"description":"Events re-queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplayResponse"}}}},"422":{"description":"Missing transfer_id","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}}},"components":{"schemas":{"Balance":{"type":"object","properties":{"card_id":{"type":"string"},"currency":{"type":"string"},"balance_minor":{"type":"integer","format":"int64"},"available_minor":{"type":"integer","format":"int64"},"hold_minor":{"type":"integer","format":"int64"}}},"Card":{"type":"object","required":["id","status","network","last_four","display_label","cardholder","card_art","wallet_status"],"properties":{"id":{"type":"string"},"user_id":{"type":"string"},"program_id":{"type":"string","enum":["HOSTRA_VIRTUAL_USD","HOSTRA_VIRTUAL_EUR","HOSTRA_PREPAID","HOSTRA_BUSINESS"]},"type":{"type":"string","enum":["virtual","physical"]},"status":{"type":"string","enum":["active","frozen","blocked","closed"]},"network":{"type":"string","enum":["visa","mastercard"]},"last_four":{"type":"string","pattern":"^[0-9]{4}$"},"display_name":{"type":"string"},"display_label":{"type":"string","example":"Visa **** 8421"},"product_name":{"type":"string","example":"Hostra Virtual Debit Card"},"card_art":{"type":"string","example":"hostra_black_v1","description":"Design token for dashboard / mobile card art."},"theme":{"type":"string","enum":["dark","light"],"example":"dark"},"brand":{"type":"string","example":"Hostra"},"issuer_name":{"type":"string","example":"Hostra"},"cardholder":{"type":"string","example":"Ada Lovelace"},"expiry_month":{"type":"integer","minimum":1,"maximum":12},"expiry_year":{"type":"integer"},"currency":{"type":"string"},"currency_symbol":{"type":"string","example":"$"},"wallet_status":{"type":"string","enum":["none","requested","provisioning","active","suspended","removed","failed"]},"wallet":{"$ref":"#/components/schemas/Wallet"},"balance_minor":{"type":"integer","format":"int64"},"available_minor":{"type":"integer","format":"int64"},"hold_minor":{"type":"integer","format":"int64"},"pending_minor":{"type":"integer","format":"int64"},"limits":{"$ref":"#/components/schemas/Limits"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"CardArt":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"}}},"CardEvent":{"type":"object","properties":{"id":{"type":"string"},"card_id":{"type":"string"},"type":{"type":"string"},"status":{"type":"string"},"amount_minor":{"type":"integer","format":"int64"},"currency":{"type":"string"},"description":{"type":"string"},"occurred_at":{"type":"string","format":"date-time"}}},"CreateCardRequest":{"type":"object","properties":{"program_id":{"type":"string","enum":["HOSTRA_VIRTUAL_USD","HOSTRA_VIRTUAL_EUR","HOSTRA_PREPAID","HOSTRA_BUSINESS"],"default":"HOSTRA_VIRTUAL_USD","description":"Stable program enum. Legacy aliases (hostra-default) map to HOSTRA_VIRTUAL_USD."},"type":{"type":"string","enum":["virtual","physical"],"default":"virtual"},"currency":{"type":"string","default":"USD"},"display_name":{"type":"string"},"kyc_reference":{"type":"string"},"user_id":{"type":"string","description":"M2M API-key callers only. Session callers MUST omit."},"cardholder":{"type":"string","description":"M2M only. Web/session MUST omit — taken from user profile after verify."}}},"CreateQuoteRequest":{"type":"object","required":["send_currency","send_amount","receive_asset","receive_network"],"properties":{"send_currency":{"type":"string","description":"ISO 4217 alpha-3 fiat currency the client pays (e.g. USD)"},"send_amount":{"type":"string","description":"Send amount in major units as a decimal string (e.g. \"100.00\")"},"receive_asset":{"type":"string","description":"On-chain asset ticker to disburse (e.g. USDT, USDC)"},"receive_network":{"type":"string","description":"canonical.Network key of the destination chain (tron/ethereum/polygon/bsc)"}}},"CreateTransferRequest":{"type":"object","required":["flow","quote_id","card_pm","wallet","network","asset"],"properties":{"flow":{"type":"string","enum":["card_to_crypto"]},"quote_id":{"type":"string","description":"The accepted quote id (from POST /v1/quotes)"},"card_pm":{"type":"string","description":"The card payment-method token (Stripe pm_...) being charged"},"wallet":{"type":"string","description":"The beneficiary on-chain wallet address"},"network":{"type":"string","enum":["tron","ethereum","polygon","bsc"]},"asset":{"type":"string","description":"On-chain asset to disburse (USDT, USDC)"}}},"Leg":{"type":"object","required":["index","kind","settlement_key","status"],"properties":{"index":{"type":"integer"},"kind":{"type":"string","enum":["card_charge","on_chain"]},"settlement_key":{"type":"string"},"status":{"type":"string","enum":["not_started","in_flight","succeeded","failed","reversing","reversed","reverse_failed"]},"client_secret":{"type":"string","description":"Provider-side client_secret surfaced when the card_charge leg returns requires_action (ADR-011 on-session 3DS). Empty for the on_chain leg, off-session card_charge legs, and legs not yet in_flight. The client uses it with the provider SDK to complete the 3DS challenge out-of-band."},"next_action_type":{"type":"string","description":"Provider's next_action type (e.g. Stripe's use_stripe_sdk, redirect_to_url) when the card_charge leg is in_flight awaiting a 3DS challenge. Empty otherwise."}}},"Limits":{"type":"object","properties":{"daily_spend_minor":{"type":"integer","format":"int64"},"monthly_spend_minor":{"type":"integer","format":"int64"},"per_transaction_minor":{"type":"integer","format":"int64"},"atm_daily_minor":{"type":"integer","format":"int64"},"online_enabled":{"type":"boolean"},"international_enabled":{"type":"boolean"},"atm_enabled":{"type":"boolean"}}},"ListResponse":{"type":"object","required":["items","count","limit"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Settlement"}},"count":{"type":"integer"},"limit":{"type":"integer"},"status":{"type":"string"}}},"LoginRequest":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email"},"password":{"type":"string"},"remember_me":{"type":"boolean","description":"Accepted by the API. Session absolute TTL remains server-configured."}}},"LoginResponse":{"type":"object","required":["user_id","merchant_id","email_verified","session_expires_at"],"properties":{"user_id":{"type":"string","format":"uuid"},"merchant_id":{"type":"string","format":"uuid"},"email_verified":{"type":"boolean"},"session_expires_at":{"type":"string","format":"date-time"}}},"MeResponse":{"type":"object","required":["user_id","merchant_id","name","email","email_verified","scopes"],"properties":{"user_id":{"type":"string","format":"uuid"},"merchant_id":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Human display name (editable via PATCH /v1/auth/me or /v1/profile)."},"preferred_name":{"type":"string","description":"Alias of name for web forms."},"first_name":{"type":"string","description":"Best-effort split of name for web forms."},"last_name":{"type":"string","description":"Best-effort split of name for web forms."},"email":{"type":"string","format":"email"},"email_verified":{"type":"boolean"},"scopes":{"type":"array","items":{"type":"string"}}}},"MeUpdateRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Full display name."},"preferred_name":{"type":"string","description":"Web alias of name (same storage)."},"first_name":{"type":"string"},"last_name":{"type":"string"}}},"Money":{"type":"object","required":["currency","amount"],"properties":{"currency":{"type":"string","description":"ISO 4217 currency code","example":"USD","pattern":"^[A-Z]{3}$"},"amount":{"type":"string","description":"Major-unit decimal string","example":"12.34"}}},"PasswordResetConfirmRequest":{"type":"object","required":["token","new_password"],"properties":{"token":{"type":"string"},"new_password":{"type":"string","minLength":8}}},"PasswordResetRequest":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}},"Problem":{"type":"object","required":["type","title","status"],"properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"},"errors":{"type":"object","additionalProperties":{"type":"string"}}}},"Quote":{"type":"object","required":["quote_id","send_currency","send_amount","receive_asset","receive_network","rate","platform_fee","network_fee","receive_amount","expires_at","created_at"],"properties":{"quote_id":{"type":"string"},"send_currency":{"type":"string"},"send_amount":{"type":"string","description":"Send amount in major units (decimal string)"},"receive_asset":{"type":"string"},"receive_network":{"type":"string"},"rate":{"type":"string","description":"SendCurrency → ReceiveAsset rate (decimal string, e.g. \"1.0000\")"},"platform_fee":{"type":"string","description":"Platform fee in send-currency major units (decimal string)"},"network_fee":{"type":"string","description":"On-chain gas estimate in send-currency major units (decimal string)"},"receive_amount":{"type":"string","description":"Receive amount in asset major units (decimal string, e.g. \"98.00\" USDT)"},"expires_at":{"type":"string","format":"date-time"},"created_at":{"type":"string","format":"date-time"}}},"RegisterRequest":{"type":"object","required":["organization_name","email"],"properties":{"organization_name":{"type":"string","minLength":1,"description":"Human-readable name of the new merchant account.","example":"Acme Inc."},"email":{"type":"string","format":"email","description":"Contact address for the welcome email. In the M2M identity plane the email is not persisted as a credential; it is used only to deliver the welcome email.","example":"owner@acme.com"},"scopes":{"type":"array","items":{"type":"string"},"description":"Optional. Overrides the configured default scopes for the issued key. The registrar may clamp or ignore scopes it does not allow for self-service provisioning.","example":["settlements:write","settlements:read"]}}},"RegisterResponse":{"type":"object","required":["merchant_id","api_key_id","api_key","api_key_prefix","scopes"],"properties":{"merchant_id":{"type":"string","format":"uuid","description":"The stable, globally-unique identifier of the newly provisioned merchant."},"api_key_id":{"type":"string","format":"uuid","description":"The stable, non-secret identifier of the issued API key (used in audit logs)."},"api_key":{"type":"string","description":"The plaintext API key (hstra_\u003cprefix\u003e_\u003csecret\u003e). Presented EXACTLY ONCE; store it securely. Never stored, never logged, never recoverable.","example":"hstra_a1b2c3d4_e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"},"api_key_prefix":{"type":"string","description":"The non-secret lookup handle of the issued key. Safe to display; lets the merchant recognize the key later.","example":"a1b2c3d4"},"scopes":{"type":"array","items":{"type":"string"},"description":"The effective RBAC scopes granted to the issued key.","example":["settlements:write","settlements:read"]}}},"ReplayRequest":{"type":"object","required":["transfer_id"],"properties":{"transfer_id":{"type":"string"}}},"ReplayResponse":{"type":"object","properties":{"requeued":{"type":"integer","description":"Number of events re-armed"}}},"ReverseRequest":{"type":"object","required":["reason"],"properties":{"reason":{"type":"string"}}},"ReverseResponse":{"type":"object","required":["idempotency_key","status","attempt"],"properties":{"idempotency_key":{"type":"string"},"status":{"type":"string"},"attempt":{"type":"integer"},"action_code":{"type":"string"},"approval_code":{"type":"string"}}},"SecureDetails":{"type":"object","required":["enabled","card_id"],"properties":{"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["demo","embed"],"description":"demo=loopback JSON pan/cvv; embed=Lithic iframe URL (PAN never hits Hostra)"},"card_id":{"type":"string"},"embed_url":{"type":"string","description":"Lithic signed iframe URL when mode=embed"},"pan":{"type":"string"},"cvv":{"type":"string"},"expiry_month":{"type":"integer"},"expiry_year":{"type":"integer"},"ttl_seconds":{"type":"integer"},"revealed_until":{"type":"string","format":"date-time"},"reason":{"type":"string"},"code":{"type":"string","example":"SECURE_DETAILS_NOT_ENABLED"}}},"Settlement":{"type":"object","required":["idempotency_key","method","amount","source","destination","recipient_name","status","attempt","created_at","updated_at"],"properties":{"idempotency_key":{"type":"string"},"method":{"type":"string"},"amount":{"$ref":"#/components/schemas/Money"},"source":{"type":"string"},"destination":{"type":"string"},"recipient_name":{"type":"string"},"reference":{"type":"string"},"status":{"type":"string","enum":["pending","in_flight","retrying","succeeded","failed","reversed"]},"provider_id":{"type":"string"},"attempt":{"type":"integer"},"stan":{"type":"string"},"retrieval_reference_number":{"type":"string"},"transaction_identifier":{"type":"string"},"approval_code":{"type":"string"},"action_code":{"type":"string"},"ledger_transaction_id":{"type":"string"},"funding_transaction_id":{"type":"string"},"failure_reason":{"type":"string"},"next_attempt_at":{"type":"string","format":"date-time"},"client_secret":{"type":"string","description":"Provider-side client_secret surfaced when an on-session card_charge returns requires_action (ADR-011 on-session 3DS). Empty for off-session charges, card_push, and on_chain."},"next_action_type":{"type":"string","description":"Provider's next_action type (e.g. Stripe's use_stripe_sdk, redirect_to_url) when status=in_flight awaiting a 3DS challenge. Empty otherwise."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"SubmitRequest":{"type":"object","required":["method","amount","source","destination","recipient_name"],"properties":{"idempotency_key":{"type":"string","description":"Optional; canonical key is the Idempotency-Key header. Must match header when both present."},"method":{"type":"string","enum":["card_push","card_charge","on_chain"],"description":"card_push = outbound push-to-card/bank (Treasury → external); card_charge = inbound card charge (external card → Treasury, ADR-008 Stripe PaymentIntent); on_chain = outbound on-chain crypto disbursement to a wallet via a custodian (ADR-010). For on_chain, destination is '\u003cnetwork\u003e:\u003cwallet_address\u003e' (e.g. 'tron:TXyz...', 'ethereum:0xabc...')"},"amount":{"$ref":"#/components/schemas/Money"},"source":{"type":"string","description":"Funding Ledger account id"},"destination":{"type":"string","description":"External destination reference (e.g. card token)"},"recipient_name":{"type":"string"},"reference":{"type":"string"}}},"Transaction":{"type":"object","properties":{"id":{"type":"string"},"card_id":{"type":"string"},"kind":{"type":"string","enum":["authorization","clear","refund"]},"status":{"type":"string"},"amount_minor":{"type":"integer","format":"int64"},"currency":{"type":"string"},"merchant":{"type":"string"},"mcc":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}},"Transfer":{"type":"object","required":["id","flow","status","quote_id","legs","created_at","updated_at"],"properties":{"id":{"type":"string"},"flow":{"type":"string"},"status":{"type":"string","enum":["pending","leg1_in_flight","leg1_succeeded","leg2_in_flight","succeeded","compensating","failed"]},"quote_id":{"type":"string"},"send_currency":{"type":"string"},"send_amount":{"type":"string"},"receive_asset":{"type":"string"},"receive_network":{"type":"string"},"receive_amount":{"type":"string"},"platform_fee":{"type":"string"},"network_fee":{"type":"string"},"legs":{"type":"array","items":{"$ref":"#/components/schemas/Leg"}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"TransferListResponse":{"type":"object","required":["items","count","total","offset","limit","has_more"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Transfer"}},"count":{"type":"integer","description":"Number of items in this page"},"total":{"type":"integer","description":"Total matching the filter (independent of the page window)"},"offset":{"type":"integer","description":"Zero-based page offset echoed back"},"limit":{"type":"integer","description":"Page size used (echoed back)"},"has_more":{"type":"boolean","description":"True when more pages exist (offset + count \u003c total)"},"status":{"type":"string","description":"The status filter applied, if any"}}},"VerifyEmailRequest":{"type":"object","required":["token"],"properties":{"token":{"type":"string","description":"Opaque token delivered by email."}}},"Wallet":{"type":"object","properties":{"apple":{"$ref":"#/components/schemas/WalletChannel"},"google":{"$ref":"#/components/schemas/WalletChannel"}}},"WalletChannel":{"type":"object","properties":{"supported":{"type":"boolean"},"status":{"type":"string","enum":["none","requested","provisioning","active","suspended","removed","failed"]}}},"WalletProvisionRequest":{"type":"object","properties":{"device_id":{"type":"string"}}},"WalletProvisionResponse":{"type":"object","properties":{"token_id":{"type":"string"},"status":{"type":"string"},"wallet":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}},"WalletStatus":{"type":"object","properties":{"card_id":{"type":"string"},"status":{"type":"string"},"tokens":{"type":"array","items":{"type":"object","properties":{"token_id":{"type":"string"},"wallet":{"type":"string"},"status":{"type":"string"},"device_id":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}}}}},"WebhookEndpointRequest":{"type":"object","required":["url","secret"],"properties":{"url":{"type":"string","description":"HTTPS endpoint to POST events to (http rejected)"},"secret":{"type":"string","description":"High-entropy HMAC key (shared secret)"}}},"WebhookEndpointResponse":{"type":"object","properties":{"merchant_id":{"type":"string"},"url":{"type":"string"},"secret_mask":{"type":"string","description":"Redacted preview (e.g. 'abc123***')"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}}