{"openapi":"3.1.0","info":{"title":"Flow Arc API","version":"1","description":"Flow Arc public API. Every endpoint returns structured JSON and is versioned under `/api/v1`."},"servers":[{"url":"https://api.flowarc.uk","description":"production"},{"url":"http://localhost:3000","description":"local"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"API key issued via the Flow Arc dashboard. Send as `Authorization: Bearer fa_…`."}},"schemas":{"PaymentRequestResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"short_code":{"type":"string","pattern":"^[0-9A-Za-z]{12}$","example":"aB3dE5fG7hJ9","description":"12-char base62 slug used as the public pay-link identifier."},"amount_minor":{"type":"integer","exclusiveMinimum":0},"currency":{"type":"string","enum":["GBP"],"description":"ISO 4217 currency code. Only GBP is supported at MVP."},"description":{"type":"string"},"customer_email":{"type":["string","null"],"format":"email"},"customer_name":{"type":["string","null"]},"customer_mobile":{"type":["string","null"],"example":"+447911123456","description":"E.164 mobile number, or null if none was captured."},"customer_reference":{"type":["string","null"]},"expires_at":{"type":["string","null"],"format":"date-time"},"provider_override_id":{"type":["string","null"],"format":"uuid","description":"Creator-set or migration-back-filled pin. When non-null, the pay-link routes to this provider regardless of the tenant's current routing chain. Null for pay-links that follow the tenant's chain."},"resolved_provider_connection_id":{"type":["string","null"],"format":"uuid","description":"The provider_connection the next pay-link click will land on right now, live-resolved from override → fallback-triggered → tenant chain. Null when the tenant has no usable routing chain."},"fallback_triggered_at":{"type":["string","null"],"format":"date-time"},"status":{"type":"string","enum":["open","succeeded","cancelled","expired"],"description":"Lifecycle state of the payment request."},"created_via":{"type":"string","enum":["dashboard","api"],"description":"Which surface created the request. Set by the server, not the client."},"created_by_user_id":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"url":{"type":"string","format":"uri","example":"https://flowarc.uk/pay/AaZz09AaZz09","description":"Full pay-link URL a customer can be redirected to."},"qr_code_data_url":{"type":"string","pattern":"^data:image\\/png;base64,","description":"Base64 PNG (data URL) of the pay-link QR code in Flow brand primary red; ready for <img src=\"…\">."}},"required":["id","short_code","amount_minor","currency","description","customer_email","customer_name","customer_mobile","customer_reference","expires_at","provider_override_id","resolved_provider_connection_id","fallback_triggered_at","status","created_via","created_by_user_id","created_at","updated_at","url","qr_code_data_url"]},"ErrorResponse":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]},"CreatePaymentRequestInput":{"type":"object","properties":{"amount_minor":{"type":"integer","exclusiveMinimum":0,"maximum":99999999,"example":2499,"description":"Amount in the currency minor unit (pence for GBP)."},"currency":{"type":"string","enum":["GBP"],"default":"GBP","description":"ISO 4217 currency code. Only GBP is supported at MVP."},"description":{"type":"string","minLength":1,"maxLength":500,"example":"Invoice #42 – consultation"},"customer_email":{"type":"string","example":"payer@example.com"},"customer_name":{"type":"string","minLength":1,"maxLength":120,"example":"Ada Lovelace"},"customer_mobile":{"type":"string","example":"+447911123456","description":"E.164 mobile number; UK-first parsing (default region GB)."},"customer_reference":{"type":"string","minLength":1,"maxLength":120,"example":"patient-000123","description":"Tenant-supplied reference (e.g. patient number). Opaque to Flow Arc."},"expires_at":{"type":"string","format":"date-time","example":"2026-05-01T12:00:00.000Z","description":"Optional RFC-3339 timestamp after which the link is no longer payable."},"provider_override_id":{"type":"string","format":"uuid","description":"Optional. When set, pins this pay-link to the given provider connection regardless of the tenant's current routing chain. When omitted, pay-link clicks live-resolve to the tenant's `routing_config.primary_provider_connection_id` (or the secondary once fallback has triggered)."},"is_setup_test":{"type":"boolean","description":"When true, this pay-link is a setup-verification £0.01 link minted from the walkthrough. Reports filter these rows out and the create-time provider-readiness gate is bypassed so the link can land on a `setup_incomplete` override."}},"required":["amount_minor","description"]},"PaymentRequestListResponse":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/PaymentRequestResponse"}},"next_cursor":{"type":["string","null"],"description":"Cursor to pass as `cursor` on the next request, or null if no more pages."}},"required":["items","next_cursor"]},"PatchPaymentRequestInput":{"anyOf":[{"type":"object","properties":{"status":{"type":"string","enum":["cancelled"],"description":"Set to `cancelled` to cancel an open pay-link. No other transitions are supported."}},"required":["status"],"additionalProperties":false},{"type":"object","properties":{"provider_override_id":{"type":["string","null"],"format":"uuid","description":"Pin this open pay-link to a specific `provider_connection`. Pass `null` to clear the override and return to live routing-chain resolution. Only the provider override changes — `fallback_triggered_at` is preserved."}},"required":["provider_override_id"],"additionalProperties":false}]},"AlertRuleResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"tenant_id":{"type":"string","format":"uuid"},"name":{"type":"string"},"enabled":{"type":"boolean"},"event_type":{"type":"string","enum":["payment_failed","payment_succeeded","payment_refunded","fallback_triggered","payment_abandoned","provider_disconnected","subscription_past_due"],"description":"Canonical event the rule subscribes to."},"channel":{"type":"string","enum":["email","sms","webhook","in_app"],"description":"Dispatch channel."},"cadence":{"type":"string","enum":["realtime","daily_digest","weekly_digest"],"description":"Realtime dispatch per event, or batched digest."},"filter_json":{"type":"object","properties":{"amountMinMinor":{"type":"integer","minimum":0},"providers":{"type":"array","items":{"type":"string","enum":["stripe","dojo"]}},"failureCodes":{"type":"array","items":{"type":"string","enum":["insufficient_funds","card_expired","incorrect_cvc","card_declined","do_not_honor","fraud_suspected","authentication_required","processing_error","unknown"]}},"paymentMethods":{"type":"array","items":{"type":"string","enum":["card","bank_transfer","direct_debit","wallet","other"]}}},"additionalProperties":false},"recipient_emails":{"type":"array","items":{"type":"string"}},"recipient_phones":{"type":"array","items":{"type":"string"}},"recipient_roles":{"type":"array","items":{"type":"string"}},"webhook_endpoint_id":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","tenant_id","name","enabled","event_type","channel","cadence","filter_json","recipient_emails","recipient_phones","recipient_roles","webhook_endpoint_id","created_at","updated_at"]},"AlertRuleErrorResponse":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]},"AlertRuleInput":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120,"example":"Failed payment attempts","description":"Human-readable label shown in the dashboard + audit log."},"enabled":{"type":"boolean","default":true},"event_type":{"type":"string","enum":["payment_failed","payment_succeeded","payment_refunded","fallback_triggered","payment_abandoned","provider_disconnected","subscription_past_due"],"description":"Canonical event the rule subscribes to."},"channel":{"type":"string","enum":["email","sms","webhook","in_app"],"description":"Dispatch channel."},"cadence":{"type":"string","enum":["realtime","daily_digest","weekly_digest"],"default":"realtime","description":"Realtime dispatch per event, or batched digest."},"filter_json":{"type":"object","properties":{"amountMinMinor":{"type":"integer","minimum":0},"providers":{"type":"array","items":{"type":"string","enum":["stripe","dojo"]}},"failureCodes":{"type":"array","items":{"type":"string","enum":["insufficient_funds","card_expired","incorrect_cvc","card_declined","do_not_honor","fraud_suspected","authentication_required","processing_error","unknown"]}},"paymentMethods":{"type":"array","items":{"type":"string","enum":["card","bank_transfer","direct_debit","wallet","other"]}}},"default":{},"additionalProperties":false},"recipient_emails":{"type":"array","items":{"type":"string","format":"email"},"default":[]},"recipient_phones":{"type":"array","items":{"type":"string","minLength":1},"default":[]},"recipient_roles":{"type":"array","items":{"type":"string","minLength":1},"default":[]},"webhook_endpoint_id":{"type":"string","format":"uuid"}},"required":["name","event_type","channel"]},"AlertRuleListResponse":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AlertRuleResponse"}}},"required":["items"]},"PatchAlertRuleInput":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"enabled":{"type":"boolean"},"event_type":{"type":"string","enum":["payment_failed","payment_succeeded","payment_refunded","fallback_triggered","payment_abandoned","provider_disconnected","subscription_past_due"],"description":"Canonical event the rule subscribes to."},"channel":{"type":"string","enum":["email","sms","webhook","in_app"],"description":"Dispatch channel."},"cadence":{"type":"string","enum":["realtime","daily_digest","weekly_digest"],"description":"Realtime dispatch per event, or batched digest."},"filter_json":{"type":"object","properties":{"amountMinMinor":{"type":"integer","minimum":0},"providers":{"type":"array","items":{"type":"string","enum":["stripe","dojo"]}},"failureCodes":{"type":"array","items":{"type":"string","enum":["insufficient_funds","card_expired","incorrect_cvc","card_declined","do_not_honor","fraud_suspected","authentication_required","processing_error","unknown"]}},"paymentMethods":{"type":"array","items":{"type":"string","enum":["card","bank_transfer","direct_debit","wallet","other"]}}},"additionalProperties":false},"recipient_emails":{"type":"array","items":{"type":"string","format":"email"}},"recipient_phones":{"type":"array","items":{"type":"string","minLength":1}},"recipient_roles":{"type":"array","items":{"type":"string","minLength":1}},"webhook_endpoint_id":{"type":["string","null"],"format":"uuid"}}},"HealthResponse":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"version":{"type":"string","example":"1.2.3"},"gitCommit":{"type":"string","example":"abc1234"},"buildTime":{"type":"string","format":"date-time","example":"2026-04-16T12:00:00.000Z"},"serverTime":{"type":"string","format":"date-time","example":"2026-04-16T12:00:00.123Z"}},"required":["ok","version","gitCommit","buildTime","serverTime"]}},"parameters":{}},"paths":{"/api/v1/payment-requests":{"post":{"summary":"Create a payment request","description":"Creates a new pay-link (`payment_request`) scoped to the caller's tenant. The response includes the `short_code` used in the public URL. Pay-link routing is live: every click re-resolves the target provider from the tenant's current `routing_config`. Pass `provider_override_id` to pin this pay-link to a specific provider_connection instead.","tags":["payment-requests"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePaymentRequestInput"}}}},"responses":{"201":{"description":"Payment request created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentRequestResponse"}}}},"400":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Provider connection does not exist, belongs to another tenant, or is not currently `connected`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"get":{"summary":"List payment requests","description":"Returns the caller tenant's payment requests, newest first. Supports filtering by `status` and cursor-based pagination.","tags":["payment-requests"],"parameters":[{"schema":{"type":"string","enum":["open","succeeded","cancelled","expired"],"description":"Lifecycle state of the payment request."},"required":false,"description":"Lifecycle state of the payment request.","name":"status","in":"query"},{"schema":{"type":"string","description":"Opaque pagination cursor returned by the previous page. Omit for the first page."},"required":false,"description":"Opaque pagination cursor returned by the previous page. Omit for the first page.","name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":20,"description":"Page size (1-100). Defaults to 20."},"required":false,"description":"Page size (1-100). Defaults to 20.","name":"limit","in":"query"}],"responses":{"200":{"description":"Page of payment requests.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentRequestListResponse"}}}},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/payment-requests/{id}":{"get":{"summary":"Fetch a single payment request by id","tags":["payment-requests"],"parameters":[{"schema":{"type":"string","format":"uuid","description":"Payment request UUID."},"required":true,"description":"Payment request UUID.","name":"id","in":"path"}],"responses":{"200":{"description":"Payment request found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentRequestResponse"}}}},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No payment request with that id is visible to the caller tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"summary":"Mutate a payment request","description":"Two mutually-exclusive mutation paths. `{ \"status\": \"cancelled\" }` transitions an `open` request to `cancelled`. `{ \"provider_override_id\": \"<uuid>\" | null }` pins the pay-link to a specific provider connection (or clears the pin with `null`). Both require `status = open`; sending both in the same request returns 422.","tags":["payment-requests"],"parameters":[{"schema":{"type":"string","format":"uuid","description":"Payment request UUID."},"required":true,"description":"Payment request UUID.","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchPaymentRequestInput"}}}},"responses":{"200":{"description":"Payment request updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentRequestResponse"}}}},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No payment request with that id is visible to the caller tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Request is not in a state that allows this transition.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too many provider override changes for this pay-link in the recent window (3 per 10 minutes).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/alert-rules":{"post":{"summary":"Create an alert rule","description":"Creates an alert rule scoped to the caller's tenant. Rules fire when a matching event arrives (see `event_type`) — email + SMS rules need at least one recipient; webhook rules need a configured `webhook_endpoint_id`.","tags":["alert-rules"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleInput"}}}},"responses":{"201":{"description":"Alert rule created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleErrorResponse"}}}},"422":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleErrorResponse"}}}}}},"get":{"summary":"List alert rules","description":"Returns the caller tenant's alert rules, newest first. Supports filtering by `enabled`, `channel`, and `event_type`.","tags":["alert-rules"],"parameters":[{"schema":{"anyOf":[{"type":"string","enum":["true"]},{"type":"string","enum":["false"]},{"type":"boolean"}]},"required":false,"name":"enabled","in":"query"},{"schema":{"type":"string","enum":["email","sms","webhook","in_app"],"description":"Dispatch channel."},"required":false,"description":"Dispatch channel.","name":"channel","in":"query"},{"schema":{"type":"string","enum":["payment_failed","payment_succeeded","payment_refunded","fallback_triggered","payment_abandoned","provider_disconnected","subscription_past_due"],"description":"Canonical event the rule subscribes to."},"required":false,"description":"Canonical event the rule subscribes to.","name":"event_type","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Page of alert rules.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleListResponse"}}}},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleErrorResponse"}}}}}}},"/api/v1/alert-rules/{id}":{"get":{"summary":"Fetch a single alert rule by id","tags":["alert-rules"],"parameters":[{"schema":{"type":"string","format":"uuid","description":"Alert rule UUID."},"required":true,"description":"Alert rule UUID.","name":"id","in":"path"}],"responses":{"200":{"description":"Alert rule found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleErrorResponse"}}}},"404":{"description":"No alert rule with that id is visible to the caller tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleErrorResponse"}}}}}},"patch":{"summary":"Update an alert rule","description":"Partial update — any subset of fields may be provided. An empty body is rejected with 422.","tags":["alert-rules"],"parameters":[{"schema":{"type":"string","format":"uuid","description":"Alert rule UUID."},"required":true,"description":"Alert rule UUID.","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchAlertRuleInput"}}}},"responses":{"200":{"description":"Alert rule updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleErrorResponse"}}}},"404":{"description":"No alert rule with that id is visible to the caller tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleErrorResponse"}}}},"422":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleErrorResponse"}}}}}},"delete":{"summary":"Delete an alert rule","tags":["alert-rules"],"parameters":[{"schema":{"type":"string","format":"uuid","description":"Alert rule UUID."},"required":true,"description":"Alert rule UUID.","name":"id","in":"path"}],"responses":{"204":{"description":"Alert rule deleted."},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleErrorResponse"}}}},"404":{"description":"No alert rule with that id is visible to the caller tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleErrorResponse"}}}}}}},"/api/v1/health":{"get":{"summary":"Service health and version","tags":["meta"],"security":[],"responses":{"200":{"description":"Service is healthy; reports the running version and build metadata.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}}},"webhooks":{}}