From ce441fbe95f37871baa114177aceb7c883e66522 Mon Sep 17 00:00:00 2001 From: Marco Casaglia Date: Thu, 30 Apr 2026 11:58:03 +0200 Subject: [PATCH] Payments Chain --- docs/general.md | 2 +- docs/payment_chain.md | 134 +++++++++++++++++++++++++++++++++++++++++ docs/request_to_pay.md | 12 ++++ openapi.json | 33 +++++++++- 4 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 docs/payment_chain.md diff --git a/docs/general.md b/docs/general.md index f9e13b1..5de86c0 100644 --- a/docs/general.md +++ b/docs/general.md @@ -68,7 +68,7 @@ APIs allow users to initiate traditional payment types: In addition, FlowPay extends traditional payment methods by providing value-added services such as - **Bulk payment**: payer can initiate a single payment with a single Strong Customer Authentication (SCA) to pay multiple payment requests or documents at once. -- **Payment chain**: user can authorise a payment to be executed when a previous payment has been successfully received. +- **Payment Chain**: user can authorise an RTP that executes when admitted FlowPay funding operations have credited a dedicated technical position. See `docs/payment_chain.md`. - **Locked payment**: the user can authorise a payment to be executed if a previous payment has been successfully received. The check is performed by the client application that initiated the payment request. Each of these services may route funds via a FlowPay technical account when required by business rules, while preserving original payer/payee information in remittance data. diff --git a/docs/payment_chain.md b/docs/payment_chain.md new file mode 100644 index 0000000..3737e74 --- /dev/null +++ b/docs/payment_chain.md @@ -0,0 +1,134 @@ +# Payment Chain + +Payment Chain lets a partner create an RTP whose execution depends on funds expected later. The chain subject is the RTP `payer` and, when specified, the RTP `debtor`; the payout destination is defined by `payee` and, for split or bulk scenarios, `additionalPayees`. + +The payer/debtor authorizes the payment up front. FlowPay executes the RTP only after admitted funding operations have credited and reconciled the dedicated technical position for that request. + +Use Payment Chain when an intermediate beneficiary expects to receive funds through FlowPay and wants to route those funds automatically to one or more final beneficiaries. + +## Actors + +- Chain subject: the RTP `payer` and actual `debtor`; this is the intermediate beneficiary whose funds are expected and who authorizes the payment. +- Funding operations: admitted FlowPay payments linked to the request and reconciled on the dedicated technical position. +- Final beneficiaries: the RTP `payee` and any `additionalPayees`. +- Partner: creates the RTP, links funding operations to it, and owns the business logic. +- FlowPay: manages the technical position, reconciliation, states, callbacks, and final payment execution. + +## Core rules + +- The partner must be enabled for Payment Chain during onboarding. +- The request is created through the standard Request To Pay API with `paymentMethod.technology` set to `chain`. +- The RTP `amount` is the target amount that must be funded. +- The RTP `payee` and `additionalPayees` define the final payment to execute. +- The dedicated IBAN, when exposed, is internal to FlowPay and can only receive admitted FlowPay funding operations. +- The execution trigger fires when the reconciled balance is greater than or equal to the RTP `amount`. +- Excess funds are liquidated to the payer/debtor on their verified IBAN. +- If the target is not reached within the operational window, available funds are liquidated to the payer/debtor. +- The request can be cancelled without financial impact only before the first admitted credit. +- After final forwarding or closure, late credits do not reactivate the request. + +## Sequence + +![](https://mermaid.ink/img/c2VxdWVuY2VEaWFncmFtCiAgYXV0b251bWJlcgogIHBhcnRpY2lwYW50IFBhcnRuZXIgYXMgUGFydG5lciBiYWNrZW5kCiAgcGFydGljaXBhbnQgQVBJIGFzIEZsb3dQYXkgQVBJCiAgcGFydGljaXBhbnQgQ2hhaW4gYXMgQ2hhaW4gdGVjaG5pY2FsIHBvc2l0aW9uCiAgcGFydGljaXBhbnQgRnVuZGluZyBhcyBGdW5kaW5nIEZsb3dQYXkgcGF5bWVudHMKICBwYXJ0aWNpcGFudCBGaW5hbCBhcyBGaW5hbCBwYXllZShzKQogIFBhcnRuZXItPj5BUEk6IFBPU1QgL3BheW1lbnQtcmVxdWVzdHMgKHBheWVyL2RlYnRvciA9IGNoYWluIHN1YmplY3QsIHBheW1lbnRNZXRob2Q6IGNoYWluKQogIE5vdGUgb3ZlciBQYXJ0bmVyLEFQSTogcGF5ZWUgYW5kIGFkZGl0aW9uYWxQYXllZXMgZGVmaW5lIHRoZSBmaW5hbCBzaW5nbGUsIHNwbGl0LCBvciBidWxrIHBheW1lbnQKICBBUEktLT4-UGFydG5lcjogMjAxIHsgcmVxdWVzdElkLCBsaW5rL3N0YXR1cyB9CiAgUGFydG5lci0-PkFQSTogQ29tcGxldGUgYXV0aG9yaXphdGlvbiAvIFNDQSBmb3IgY2hhaW4gbWV0aG9kCiAgQVBJLS0-PlBhcnRuZXI6IHBheW1lbnQuc3RhdHVzX2NoYW5nZSAoYXV0aG9yaXplZCkKICBBUEktPj5DaGFpbjogUHJlcGFyZSBkZWRpY2F0ZWQgdGVjaG5pY2FsIHBvc2l0aW9uCiAgbG9vcCBBZG1pdHRlZCBmdW5kaW5nIG9wZXJhdGlvbnMKICAgIEZ1bmRpbmctPj5BUEk6IENvbXBsZXRlIEZsb3dQYXkgcGF5bWVudCBsaW5rZWQgdG8gcmVxdWVzdAogICAgQVBJLT4-Q2hhaW46IFJlY29uY2lsZSBpbmNvbWluZyBmdW5kcwogIGVuZAogIGFsdCBSZWNvbmNpbGVkIGJhbGFuY2UgPj0gcmVxdWVzdCBhbW91bnQKICAgIEFQSS0-PkZpbmFsOiBFeGVjdXRlIFJUUCBwYXlvdXQgdXNpbmcgcGF5ZWUvYWRkaXRpb25hbFBheWVlcwogICAgQVBJLS0-PlBhcnRuZXI6IHBheW1lbnQuc3RhdHVzX2NoYW5nZSAoZm9yd2FyZGVkKQogIGVsc2UgRnVuZGluZyB3aW5kb3cgZXhwaXJlcyBiZWxvdyBhbW91bnQKICAgIEFQSS0-PkZpbmFsOiBMaXF1aWRhdGUgYXZhaWxhYmxlIGZ1bmRzIHRvIHBheWVyL2RlYnRvciB2ZXJpZmllZCBJQkFOCiAgICBBUEktLT4-UGFydG5lcjogcGF5bWVudC5zdGF0dXNfY2hhbmdlIChmb3J3YXJkZWQgb3IgcmVqZWN0ZWQpCiAgZW5kCg) + +## Public lifecycle + +Payment Chain reuses the existing payment request states. The public lifecycle remains focused on the request status; funding reconciliation and balance checks are handled by FlowPay while the request is waiting for execution. + +- `created`: the RTP has been created. No funds have been reconciled yet. +- `authorized`: the chain payment method is authorized and the technical position can receive admitted funding. +- `onHold`: funds are present or pending against final execution. Balance, deadline, and exceptions are monitored here. +- `forwarded`: the RTP payout has been forwarded to `payee` and `additionalPayees`, or default liquidation has been executed. +- `rejected`: the request is refused, cancelled before funding, or cannot be executed. + +![](https://mermaid.ink/img/c3RhdGVEaWFncmFtLXYyCiAgWypdIC0tPiBjcmVhdGVkCiAgY3JlYXRlZCAtLT4gYXV0aG9yaXplZDogY2hhaW4gbWV0aG9kIGF1dGhvcml6ZWQgLyBTQ0EgY29sbGVjdGVkCiAgY3JlYXRlZCAtLT4gcmVqZWN0ZWQ6IGNhbmNlbGxlZCBiZWZvcmUgZnVuZGluZwogIGF1dGhvcml6ZWQgLS0-IG9uSG9sZDogZmlyc3QgYWRtaXR0ZWQgZnVuZHMgcmVjb25jaWxlZAogIG9uSG9sZCAtLT4gb25Ib2xkOiBiYWxhbmNlIHVwZGF0ZWQgLyBiZWxvdyBSVFAgYW1vdW50CiAgb25Ib2xkIC0tPiBmb3J3YXJkZWQ6IGJhbGFuY2UgPj0gUlRQIGFtb3VudCBhbmQgUlRQIHBheW91dCBzZW50CiAgb25Ib2xkIC0tPiBmb3J3YXJkZWQ6IGZ1bmRpbmcgd2luZG93IGV4cGlyZXMgYW5kIGRlZmF1bHQgbGlxdWlkYXRpb24gc2VudAogIG9uSG9sZCAtLT4gcmVqZWN0ZWQ6IG5vdCBleGVjdXRhYmxlIC8gbm8gdmFsaWQgbGlxdWlkYXRpb24gcGF0aAogIGZvcndhcmRlZCAtLT4gWypdCiAgcmVqZWN0ZWQgLS0-IFsqXQo) + +## Example: chain with a single final payment + +In this example, the payer/debtor is the chain subject. The final payment is the RTP payout to `payee`. + +```bash +BASE_URL="https://api.sandbox.flowpay.it/v2/" +API_KEY="sk_test_xxx" + +curl -sS -X POST "$BASE_URL/payment-requests" \ + -H "Content-Type: application/json" \ + -H "X-API-Key: $API_KEY" \ + -H "Idempotency-Key: chain-order-9001" \ + -d '{ + "payer": "4a1d7c8e-3b2f-4a40-a74f-13df37a1d230", + "debtor": "4a1d7c8e-3b2f-4a40-a74f-13df37a1d230", + "payee": { + "name": "Supplier SRL", + "iban": "IT60X0542811101000000123456" + }, + "title": "Payment chain for order #9001", + "description": "Execute supplier payout when funding is received", + "remittanceInformation": "CHAIN-9001", + "amount": 100.00, + "currency": "EUR", + "paymentMethod": { + "technology": "chain" + }, + "callbackUrl": "https://merchant.example.com/api/payment/callback" + }' +``` + +Representative response: + +```json +{ + "paymentRequestId": "3bb35be2-9f2d-4217-b41e-e83b89312d8c", + "status": "created", + "amount": 100.00, + "currency": "EUR", + "payer": "4a1d7c8e-3b2f-4a40-a74f-13df37a1d230", + "payeeId": "8d73f75d-583d-4a81-aea2-c97dfccfb331", + "paymentMethod": { + "technology": "chain" + } +} +``` + +## Example: chain with split or bulk payout + +A split or bulk chain uses the same payout fields as the corresponding RTP. The final distribution is expressed with `payee` and `additionalPayees`. + +```json +{ + "payer": "4a1d7c8e-3b2f-4a40-a74f-13df37a1d230", + "debtor": "4a1d7c8e-3b2f-4a40-a74f-13df37a1d230", + "amount": 250.00, + "currency": "EUR", + "remittanceInformation": "CHAIN-9001", + "paymentMethod": { + "technology": "chain" + }, + "payee": { + "name": "Supplier SRL", + "iban": "IT60X0542811101000000123456" + }, + "additionalPayees": [ + { + "name": "Platform SPA", + "iban": "IT79Q0300203280941591243327", + "amount": 50.00, + "remittanceInformation": "Platform fee #9001" + } + ] +} +``` + +In this example, FlowPay executes the RTP when the chain is funded. The supplier receives the residual amount (`amount` minus `additionalPayees`), and the additional payee receives the configured amount. + +## Funding + +Funding must come from FlowPay operations enabled for the use case. FlowPay links admitted funding operations to the request and reconciles them on the dedicated technical position. + +When the reconciled amount reaches or exceeds the RTP `amount`, FlowPay schedules or sends the payout defined by the RTP. + +If the last funding operation exceeds the RTP amount, the difference is treated as excess and liquidated to the payer/debtor on their verified IBAN. + +## Callbacks and status reads + +Use the configured `callbackUrl` exactly as for other Request To Pay products. The stable callback event remains `payment.status_change`; the `status` field carries the public payment request status. Always treat the API read model as authoritative, and make webhook handlers idempotent. diff --git a/docs/request_to_pay.md b/docs/request_to_pay.md index f599050..ffbd4fb 100644 --- a/docs/request_to_pay.md +++ b/docs/request_to_pay.md @@ -130,6 +130,18 @@ What it enables: marketplaces, app stores, franchise models, partner revenue sha State evolution: similar to Bulk, the user authorizes once and funds always pass via the Technical Account, are processed immediately, and re-forwarded according to the split. The request may show a short onHold during dispatch, then advances to forwarded. Errors on outbound legs are handled by the dispatch engine and do not return the request to inProgress. Refunds mirror the original allocation proportions. +## Payment Chain + +Payment Chain lets a partner create an RTP whose execution depends on funds expected later. The chain subject is the RTP `payer` and, when specified, the RTP `debtor`; the payout destination is defined by `payee` and, for split or bulk scenarios, `additionalPayees`. + +![](https://mermaid.ink/img/c2VxdWVuY2VEaWFncmFtCiAgYXV0b251bWJlcgogIHBhcnRpY2lwYW50IFBhcnRuZXIgYXMgUGFydG5lciBiYWNrZW5kCiAgcGFydGljaXBhbnQgQVBJIGFzIEZsb3dQYXkgQVBJCiAgcGFydGljaXBhbnQgQ2hhaW4gYXMgQ2hhaW4gdGVjaG5pY2FsIHBvc2l0aW9uCiAgcGFydGljaXBhbnQgRnVuZGluZyBhcyBGdW5kaW5nIEZsb3dQYXkgcGF5bWVudHMKICBwYXJ0aWNpcGFudCBGaW5hbCBhcyBGaW5hbCBwYXllZShzKQogIFBhcnRuZXItPj5BUEk6IFBPU1QgL3BheW1lbnQtcmVxdWVzdHMgKHBheWVyL2RlYnRvciA9IGNoYWluIHN1YmplY3QsIHBheW1lbnRNZXRob2Q6IGNoYWluKQogIE5vdGUgb3ZlciBQYXJ0bmVyLEFQSTogcGF5ZWUgYW5kIGFkZGl0aW9uYWxQYXllZXMgZGVmaW5lIHRoZSBmaW5hbCBzaW5nbGUsIHNwbGl0LCBvciBidWxrIHBheW1lbnQKICBBUEktLT4-UGFydG5lcjogMjAxIHsgcmVxdWVzdElkLCBsaW5rL3N0YXR1cyB9CiAgUGFydG5lci0-PkFQSTogQ29tcGxldGUgYXV0aG9yaXphdGlvbiAvIFNDQSBmb3IgY2hhaW4gbWV0aG9kCiAgQVBJLS0-PlBhcnRuZXI6IHBheW1lbnQuc3RhdHVzX2NoYW5nZSAoYXV0aG9yaXplZCkKICBBUEktPj5DaGFpbjogUHJlcGFyZSBkZWRpY2F0ZWQgdGVjaG5pY2FsIHBvc2l0aW9uCiAgbG9vcCBBZG1pdHRlZCBmdW5kaW5nIG9wZXJhdGlvbnMKICAgIEZ1bmRpbmctPj5BUEk6IENvbXBsZXRlIEZsb3dQYXkgcGF5bWVudCBsaW5rZWQgdG8gcmVxdWVzdAogICAgQVBJLT4-Q2hhaW46IFJlY29uY2lsZSBpbmNvbWluZyBmdW5kcwogIGVuZAogIGFsdCBSZWNvbmNpbGVkIGJhbGFuY2UgPj0gcmVxdWVzdCBhbW91bnQKICAgIEFQSS0-PkZpbmFsOiBFeGVjdXRlIFJUUCBwYXlvdXQgdXNpbmcgcGF5ZWUvYWRkaXRpb25hbFBheWVlcwogICAgQVBJLS0-PlBhcnRuZXI6IHBheW1lbnQuc3RhdHVzX2NoYW5nZSAoZm9yd2FyZGVkKQogIGVsc2UgRnVuZGluZyB3aW5kb3cgZXhwaXJlcyBiZWxvdyBhbW91bnQKICAgIEFQSS0-PkZpbmFsOiBMaXF1aWRhdGUgYXZhaWxhYmxlIGZ1bmRzIHRvIHBheWVyL2RlYnRvciB2ZXJpZmllZCBJQkFOCiAgICBBUEktLT4-UGFydG5lcjogcGF5bWVudC5zdGF0dXNfY2hhbmdlIChmb3J3YXJkZWQgb3IgcmVqZWN0ZWQpCiAgZW5kCg) + +What it enables: chained settlement, supplier or platform payouts funded by expected incoming FlowPay payments, convergence of multiple funding operations into one controlled technical position, and automatic execution of the RTP after the request amount is funded. + +State evolution: the chain is created with the standard Request To Pay API and `paymentMethod` set to `chain`. After authorization it moves to `authorized`; once admitted funds arrive it uses `onHold` while FlowPay reconciles balance, amount, deadline, excesses, and operational conditions. When the reconciled balance reaches the RTP `amount`, FlowPay forwards the RTP payout to `payee` and `additionalPayees`. If the amount is not reached within the operational window, available funds are liquidated to the payer/debtor or the request is rejected if no valid liquidation path exists. + +Detailed examples, lifecycle diagrams, and callback handling are documented in [Payment Chain](./payment_chain.md). + ## PagoPA payment For public‑service payments in the Italian PagoPA ecosystem, RTP integrates a dedicated branch. You provide the entity tax code and payment notice number, and an email to receive the receipt. The hosted checkout guides the user through the PagoPA‑specific steps, while your integration pattern (redirects, callbacks, receipt download) remains the same. diff --git a/openapi.json b/openapi.json index 4903b19..2216f63 100644 --- a/openapi.json +++ b/openapi.json @@ -3265,7 +3265,8 @@ "bankAccount", "card", "bancomat", - "charge" + "charge", + "chain" ], "description": "Supported payment technologies." }, @@ -3277,7 +3278,8 @@ "enum": [ "openbanking", "card", - "bancomat_pay" + "bancomat_pay", + "chain" ] } }, @@ -3339,6 +3341,26 @@ } ] }, + { + "title": "Payment Chain", + "description": "Authorise an RTP payment method that executes the request payout once admitted funding reaches the request amount.", + "allOf": [ + { + "type": "object", + "properties": { + "technology": { + "const": "chain" + } + }, + "required": [ + "technology" + ] + }, + { + "$ref": "#/components/schemas/ChainTechnology" + } + ] + }, { "title": "Use saved payment method", "description": "Reference a saved payment method by ID.", @@ -3406,6 +3428,13 @@ "additionalProperties": false, "required": [] }, + "ChainTechnology": { + "type": "object", + "title": "Payment Chain", + "description": "Payment Chain executes the RTP payout once admitted funding reaches the request amount. The RTP `payer`/`debtor` identifies the chain subject, while `payee` and `additionalPayees` define the final payout.", + "properties": {}, + "additionalProperties": false + }, "SDDPaymentMethodPayload": { "type": "object", "title": "SDD payload",