openapi: 3.1.0 info: title: Frag Public API version: "1.2.0" description: | Accept one payment from many coins, tokens, and chains. Receive one clean settlement. All endpoints live under `/api/public/v1/*` and require a bearer secret key (`sk_test_…` or `sk_live_…`) except where noted (`/api/public/health`, `/api/public/v1/tokens`, and `/api/public/v1/checkout/{id}` are unauthenticated). contact: name: Frag url: https://frag.cash servers: - url: https://fragpay.lovable.app description: Production - url: https://frag.cash description: Production (custom domain) security: - bearerAuth: [] tags: - name: Health - name: Payment intents - name: Checkout - name: Refunds - name: Payouts - name: Webhook endpoints - name: Tokens components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: sk_test_… / sk_live_… parameters: IdempotencyKey: in: header name: Idempotency-Key required: false schema: { type: string, maxLength: 255 } description: Safe-retry key. Replays within 24h return the original response; same key + different body returns 409. schemas: Error: type: object properties: error: { type: string } code: { type: string } details: { type: object, additionalProperties: true } PaymentIntent: type: object properties: id: { type: string, format: uuid } amount: { type: string } currency: { type: string } mode: { type: string, enum: [test, live] } status: type: string enum: [ created, requires_payment, quoted, awaiting_signature, executing, partially_filled, settled, failed, expired, cancelled, refunded, ] checkout_url: { type: string, format: uri } settlement_chain: { type: string } settlement_token: { type: string } settlement_address: { type: string } customer_email: { type: string, nullable: true } expires_at: { type: string, format: date-time } created_at: { type: string, format: date-time } metadata: { type: object, additionalProperties: true } RouteCandidate: type: object properties: id: { type: string } provider: { type: string, description: "lifi-default | lifi-cctp | jupiter-direct | ..." } chosen: { type: boolean } ai_pick: { type: boolean } score: { type: number } from_chain: { type: string } to_chain: { type: string } from_token: { type: string } to_token: { type: string } to_amount_usd: { type: number } fee_usd: { type: number } gas_usd: { type: number } duration_sec: { type: integer } reason: { type: string, nullable: true } error: { type: string, nullable: true } created_at: { type: string, format: date-time } Refund: type: object properties: id: { type: string, format: uuid } payment_intent_id: { type: string, format: uuid } amount_usd: { type: number } chain: { type: string } token: { type: string } destination: { type: string } reason: { type: string } status: { type: string, enum: [pending, paid, failed, cancelled] } tx_hash: { type: string, nullable: true } created_at: { type: string, format: date-time } paid_at: { type: string, nullable: true } Payout: type: object properties: id: { type: string, format: uuid } payment_intent_id: { type: string, nullable: true } rail: { type: string } destination: { type: string } chain: { type: string } token: { type: string } amount: { type: string } currency: { type: string } status: { type: string, enum: [ queued, pending, processing, pending_provider, retry_scheduled, paid, failed, paused, ], } tx_hash: { type: string, nullable: true } failure_reason: { type: string, nullable: true } attempts: { type: integer } next_attempt_at: { type: string, nullable: true } paid_at: { type: string, nullable: true } created_at: { type: string, format: date-time } WebhookEndpoint: type: object properties: id: { type: string, format: uuid } url: { type: string, format: uri } events: { type: array, items: { type: string } } enabled: { type: boolean } created_at: { type: string, format: date-time } secret: type: string description: Signing secret. Returned only in create + rotate-secret responses. Store immediately. Token: type: object properties: chain: { type: string } address: { type: string } symbol: { type: string } name: { type: string } decimals: { type: integer } verified: { type: boolean } CheckoutView: type: object properties: id: { type: string, format: uuid } amount: { type: string } currency: { type: string } status: { type: string } settlement_chain: { type: string } settlement_token: { type: string } expires_at: { type: string, format: date-time } legs: type: array items: type: object properties: chain: { type: string } token: { type: string } amount_usd: { type: number } status: { type: string } paths: /api/public/health: get: tags: [Health] summary: Health check (unauthenticated) security: [] responses: "200": description: OK content: application/json: schema: type: object properties: status: { type: string, example: ok } ts: { type: string, format: date-time } /api/public/v1/payment-intents: post: tags: [Payment intents] summary: Create a payment intent operationId: createPaymentIntent parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }] requestBody: required: true content: application/json: schema: type: object required: [amount] properties: amount: { type: number, minimum: 0.01, maximum: 1000000 } currency: { type: string, default: USD } settlement_chain: { type: string } settlement_token: { type: string } settlement_address: { type: string } customer_email: { type: string, format: email } expires_in_seconds: { type: integer, minimum: 60, maximum: 2592000 } metadata: { type: object, additionalProperties: true } responses: "201": description: Created content: { application/json: { schema: { $ref: "#/components/schemas/PaymentIntent" } } } "400": { description: Invalid input, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }, } "401": { description: Unauthorized } "409": { description: Idempotency key mismatch, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }, } "429": { description: Rate limited } get: tags: [Payment intents] summary: List payment intents operationId: listPaymentIntents parameters: - { in: query, name: limit, schema: { type: integer, maximum: 100, default: 20 } } - { in: query, name: status, schema: { type: string } } responses: "200": description: OK content: application/json: schema: type: object properties: data: { type: array, items: { $ref: "#/components/schemas/PaymentIntent" } } /api/public/v1/payment-intents/{id}: parameters: [{ in: path, name: id, required: true, schema: { type: string, format: uuid } }] get: tags: [Payment intents] summary: Retrieve a payment intent operationId: retrievePaymentIntent responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/PaymentIntent" } } }, } "404": { description: Not found } /api/public/v1/payment-intents/{id}/cancel: parameters: [{ in: path, name: id, required: true, schema: { type: string, format: uuid } }] post: tags: [Payment intents] summary: Cancel a payment intent operationId: cancelPaymentIntent responses: "200": { description: Cancelled, content: { application/json: { schema: { $ref: "#/components/schemas/PaymentIntent" } } }, } "409": { description: Cannot cancel in current status } /api/public/v1/payment-intents/{id}/transactions: parameters: [{ in: path, name: id, required: true, schema: { type: string, format: uuid } }] get: tags: [Payment intents] summary: List transaction legs contributing to this intent operationId: listPaymentIntentTransactions responses: "200": description: OK content: application/json: schema: type: object properties: data: { type: array, items: { type: object, additionalProperties: true } } /api/public/v1/payment-intents/{id}/routing: parameters: [{ in: path, name: id, required: true, schema: { type: string, format: uuid } }] get: tags: [Payment intents] summary: List router candidates evaluated for this intent (chosen + rejected) operationId: listPaymentIntentRoutingCandidates responses: "200": description: OK content: application/json: schema: type: object properties: data: { type: array, items: { $ref: "#/components/schemas/RouteCandidate" } } "404": { description: Not found } /api/public/v1/checkout/{id}: parameters: [{ in: path, name: id, required: true, schema: { type: string, format: uuid } }] get: tags: [Checkout] summary: Public read-only checkout view (no auth). Used by hosted checkout + polling clients. operationId: getCheckoutView security: [] responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/CheckoutView" } } }, } "404": { description: Not found } /api/public/v1/refunds: post: tags: [Refunds] summary: Create a refund operationId: createRefund parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }] requestBody: required: true content: application/json: schema: type: object required: [intent_id, amount_usd] properties: intent_id: { type: string, format: uuid, description: "Payment intent to refund. Must be settled or partially funded." } amount_usd: { type: number, description: "Refund amount in USD. Capped by the intent's refundable balance." } destination: { type: string, description: "Optional payer address to refund to. If omitted, the refund waits in `pending` with `failure_reason: missing_destination` until the merchant supplies one from the dashboard.", } reason: { type: string, maxLength: 280, example: "customer_request" } example: intent_id: "pi_11111111-1111-1111-1111-111111111111" amount_usd: 25.00 destination: "0xabc0000000000000000000000000000000000000" reason: "customer_request" responses: "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/Refund" } } }, } "409": { description: Intent not settled } "422": { description: Refund exceeds refundable balance } get: tags: [Refunds] summary: List refunds operationId: listRefunds parameters: - { in: query, name: payment_intent_id, schema: { type: string, format: uuid } } - { in: query, name: limit, schema: { type: integer, maximum: 200, default: 50 } } responses: "200": description: OK content: application/json: schema: type: object properties: data: { type: array, items: { $ref: "#/components/schemas/Refund" } } /api/public/v1/refunds/{id}: parameters: [{ in: path, name: id, required: true, schema: { type: string, format: uuid } }] get: tags: [Refunds] summary: Retrieve a refund operationId: retrieveRefund responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Refund" } } }, } "404": { description: Not found } /api/public/refunds/{id}/status: parameters: [{ in: path, name: id, required: true, schema: { type: string, format: uuid } }] get: tags: [Refunds] summary: Payer-facing refund status (unauthenticated, safe view) description: | Public, unauthenticated status endpoint safe to expose in customer emails and receipts. Destination address is redacted to the last 6 characters; no merchant PII is returned. operationId: refundStatus security: [] responses: "200": description: OK content: application/json: schema: type: object properties: id: { type: string, format: uuid } payment_intent_id: { type: string, format: uuid } amount_usd: { type: number } chain: { type: string, nullable: true } token: { type: string, nullable: true } destination_tail: { type: string, description: "e.g. '…a1b2c3'" } reason: { type: string } status: type: string enum: [pending, processing, retry_scheduled, succeeded, failed, cancelled] tx_hash: { type: string, nullable: true } failure_reason: { type: string, nullable: true } attempts: { type: integer } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } paid_at: { type: string, format: date-time, nullable: true } terminal: { type: boolean } "404": { description: Not found } /api/public/v1/payouts: get: tags: [Payouts] summary: List payouts operationId: listPayouts parameters: - { in: query, name: status, schema: { type: string } } - { in: query, name: payment_intent_id, schema: { type: string, format: uuid } } - { in: query, name: limit, schema: { type: integer, maximum: 200, default: 50 } } responses: "200": description: OK content: application/json: schema: type: object properties: data: { type: array, items: { $ref: "#/components/schemas/Payout" } } /api/public/v1/payouts/{id}: parameters: [{ in: path, name: id, required: true, schema: { type: string, format: uuid } }] get: tags: [Payouts] summary: Retrieve a payout operationId: retrievePayout responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Payout" } } }, } "404": { description: Not found } /api/public/v1/payouts/{id}/replay: parameters: [{ in: path, name: id, required: true, schema: { type: string, format: uuid } }] post: tags: [Payouts] summary: Requeue a failed payout for another dispatch attempt operationId: replayPayout responses: "200": { description: Requeued, content: { application/json: { schema: { $ref: "#/components/schemas/Payout" } } }, } "409": { description: Payout is not in a replayable state } /api/public/v1/webhook-endpoints: post: tags: [Webhook endpoints] summary: Create a webhook endpoint operationId: createWebhookEndpoint requestBody: required: true content: application/json: schema: type: object required: [url, events] properties: url: { type: string, format: uri } events: { type: array, items: { type: string } } enabled: { type: boolean, default: true } responses: "201": description: Created — response includes plaintext `secret`; store immediately. content: { application/json: { schema: { $ref: "#/components/schemas/WebhookEndpoint" } } } get: tags: [Webhook endpoints] summary: List webhook endpoints operationId: listWebhookEndpoints responses: "200": description: OK content: application/json: schema: type: object properties: data: { type: array, items: { $ref: "#/components/schemas/WebhookEndpoint" } } /api/public/v1/webhook-endpoints/{id}: parameters: [{ in: path, name: id, required: true, schema: { type: string, format: uuid } }] get: tags: [Webhook endpoints] summary: Retrieve a webhook endpoint operationId: retrieveWebhookEndpoint responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/WebhookEndpoint" } } }, } "404": { description: Not found } patch: tags: [Webhook endpoints] summary: Update a webhook endpoint operationId: updateWebhookEndpoint requestBody: content: application/json: schema: type: object properties: url: { type: string, format: uri } events: { type: array, items: { type: string } } enabled: { type: boolean } responses: "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/WebhookEndpoint" } } }, } delete: tags: [Webhook endpoints] summary: Delete a webhook endpoint operationId: deleteWebhookEndpoint responses: "200": { description: Deleted } "204": { description: Deleted } /api/public/v1/webhook-endpoints/{id}/rotate-secret: parameters: [{ in: path, name: id, required: true, schema: { type: string, format: uuid } }] post: tags: [Webhook endpoints] summary: Rotate the signing secret. Previous secret remains valid for 24h. operationId: rotateWebhookEndpointSecret responses: "200": description: OK — new plaintext `secret` returned once. content: { application/json: { schema: { $ref: "#/components/schemas/WebhookEndpoint" } } } /api/public/v1/tokens: get: tags: [Tokens] summary: List supported tokens (unauthenticated) operationId: listTokens security: [] parameters: - { in: query, name: chain, schema: { type: string } } responses: "200": description: OK content: application/json: schema: type: object properties: data: { type: array, items: { $ref: "#/components/schemas/Token" } }