# Variable Recurring Payments ## Use Case - **Coil** is an application that wishes to connect to Alice's Open Payments account - **Fynbos** is Alice's account provider - **Uphold** is Bob's account provider - **Alice** is an account holder at Fynbos and a user at Coil - **Bob** is an account holder at Uphold - Alice is visiting Bob's website and wishes to send Bob a tip. - Bob has a Payment Pointer from Uphold that he has embedded on his website. - Alice has the Coil browser extension installed ### Out of Scope - How does Fynbos verify Coil's identity as a client? - How does Alice verify that the account she is tipping belongs to Bob? ### Step 1: Alice connects her Fynbos account to Coil Alice wishes to give Coil the ability to send tips on her behalf from her Fynbos account. Alice provides Coil with her Payment Pointer: `https://fynbos.me/alice` Coil queries the Payment Pointer to get the grant request endpoint URL. ```http GET /alice HTTP/1.1 Host: fynbos.me Accept: application/op-account-v1+json ``` Fynbos responds: ```http HTTP/1.1 200 Success Content-Type: application/op-account-v1+json { "id": "https://fynbos.me/alice", "publicName": "Alice" "assetCode": "USD", "assetScale": 2 "authServer": "https://fynbos.dev/auth" } ``` - `id` is the unique URL identifying this account. It may be different to the URL that was used in the original client request if the account has multiple aliases or the request weas redirected from another URL. - `publicName` is a name that Alice chose to display puclicly for her account. Since this endpoint can be accessed anonymously this value is set by the account holder and can be any value. It is not used for verfication. - `assetCode` and `assetScale` indicate the currency of the account. - `authServer` is the endpoint of the AS where grants can be obtained to perform actions on this account. Coil makes a Grant Request to Fynbos via the AS endpoint at `https://fynbos.dev/auth`. The body of the request is: ```http POST /auth HTTP/1.1 Host: fynbos.dev Content-Type: application/json Signature-Input: sig1=... Signature: sig1=... Digest: sha256=... { "access_token": { "access": [ { "type": "outgoing-payment", "actions": [ "create", "authorize" ], "locations": [ "https://fynbos.me/alice" ] } ] }, "client": { "display": { "name": "Coil", "uri": "https://coil.com" }, "key": { "proof": "httpsig", "jwk": { "kty": "RSA", "e": "AQAB", "kid": "xyz-1", "alg": "RS256", "n": "kOB5rR4Jv0GMeL...." } } }, "interact": { "start": [ "redirect" ], "finish": { "method": "redirect", "uri": "https://coil.com/return/876FGRD8VC", "nonce": "LKLTI25DK82FX4T4QFZC" } } } ``` - `access_token` details the requested grant. Coil is requesting the ability to create and authorise outgoing payments from Alice's account. - `client` is where Coil describes itself and provides the key that it uses to identify itself. Since Coil is presumably already known to Fynbos this request will be authenticated (signed with that key). - `interact` details the mechanisms available for Fynbos to interact with Alice via Coil if required. Fynbos sends back an instruction to Coil to redirect Alice to a Fynbos web page to authenticate herself and consent to the grant request. ```http HTTP/1.1 201 Created Content-Type: application/json { "interact": { "redirect": "https://fynbos.dev/auth/4CF492MLVMSW9MKMXKHQ", "finish": "MBDOFXG4Y5CVJCX821LH" }, "continue": { "access_token": { "value": "80UPRY5NM33OMUKMKSKU" }, "uri": "https://fynbos.dev/continue/4CF492MLVMSW9MKMXKHQ" } } ``` - `interact` is the details of the redirect instruction with a unique key to secure the callback - `continue` defines the callback URL and an access token to use when making the callback Coil directs Alice to `https://fynbos.dev/auth/4CF492MLVMSW9MKMXKHQ` which is a web page hosted by Fynbos. Fynbos authenticates Alice and then prompts Alice to consent to giving Coil access to her account. Fynbos allows Alice to specify a limit on what Coil will be able to do. Example: > Coil will be able to make payments from your account up to a total of $____ per ____. Alice specifies $10 per month and Fynbos creates and records the new grant issued to Coil in its own system. Fynbos redirects Alice to `https://coil.com/return/876FGRD8VC?hash=p28jsq0Y2KK3WS__a42tavNC64ldGTBroywsWxT4md_jZQ1R2HZT8BOWYHcLmObM7XHPAdJzTZMtKBsaraJ64A &interact_ref=4IFWWIKYBC2PQ6U56NL1` - The URL was provided by Coil in the grant request - `interact_ref` is a random value generated by Fynbos - `hash` was calculated by Fynbos based on: - the `nonce` Coil provided in the original request (`LKLTI25DK82FX4T4QFZC`), - the `nonce` Fynbos provided in the response (`MBDOFXG4Y5CVJCX821LH`) - the `interact_ref`, and - the URL of the grant request endpoint `https://fynbos.dev/auth`. Coil validates the hash and then makes a continue request to Fynbos at `https://fynbos.dev/continue/4CF492MLVMSW9MKMXKHQ` using the access token `80UPRY5NM33OMUKMKSKU`. ```http POST /continue/4CF492MLVMSW9MKMXKHQ HTTP/1.1 Authorization: GNAP 80UPRY5NM33OMUKMKSKU Host: fynbos.dev Signature-Input: sig1=... Signature: sig1=... ``` Fynbos responds to Coil with the following response body: ```http HTTP/1.1 200 Success Content-Type: application/json { "access_token": { "value": "OS9M2PMHKUR64TB8N6BW7OZB8CDFONP219RP1LT0", "manage": "https://fynbos.dev/auth/token/PRY5NM33OM4TB8N6BW7", "expires_in": 762534, "access": [ { "type": "outgoing-payment", "actions": [ "create", "authorize", ], "locations": [ "https://fynbos.me/alice" ], "limits": { "sendAmount": { "amount": 1000, "assetCode": "USD", "assetScale": 2 }, "interval": "P1M", "startAt": "2022-02-01T00:00:00.000Z", "expiresAt": "2023-01-31T23:59:59.999Z", } }] }, "continue": { "access_token": { "value": "YT98RY5NM33OMUKMK65U" }, "uri": "https://fynbos.dev/auth/continue" } } ``` Coil now has a grant to create and authorize outgoing payments from Alice's account up to a value of $10 per month. The `limits` section of the grant defines the limit Alice has applied to the grant she's given Coil. The limit applies to the `sendAmount` therefor Coil is only able to send up to that limit under this grant. It will be up to Fynbos to track how much Coil has sent using outgoing payments and ensure it doesn't exceed the limit. The `interval` of `P1M` means that the $10 limit is reset every month. `P1M` follows the definition of a duration per RFC 3339 and ISO 8601. The `startAt` property indicates when the intervals start. In this case, Fynbos is applying the limit using a calendar month so the `startAt` is set to the first second of the month. The `expiresAt` is optional, but in this case Fynbos has only granted Coil permission to create outgoing payments for 1 year. This is different to the token expiry which is expressed in `expires_in`. An expired token can be refreshed without affecting the grant. ### Step 2: Alice tips Bob through the Coil extension Alice is on Bob's website and the Coil extension has parsed Bob's Payment Pointer from the page: `https://uphold.com/bob` Alice wants to send a tip to Bob and clicks the "Tip" button on the extension. Alice elects to send $2 and provides a message of *"Great blog bob!"*. Coil creates the outgoing payment from Alice's account by posting a new outgoing payment resource to her Payment Pointer: ```http POST /alice HTTP/1.1 Host: fynbos.me Content-Type: application/op-outgoing-payment-v1+json Accept: application/op-outgoing-payment-v1+json Signature-Input: gnap=("@request-target" "host" "authorization");created=1624564850;keyid="xyz-client" Signature: gnap=:EN/rExQ/knVi61P5AFhyMGN7aVPzk/9C7nsYAWF2RvzsoV1uNxGZklM55qCIQpuhoNty4EhiH7iwuzZBbRCQcQ==: Authorization: GNAP OS9M2PMHKUR64TB8N6BW7OZB8CDFONP219RP1LT0 { "state": "authorized", "receivingAccount": "https://uphold.com/bob", "sendAmount": { "amount": 200, "assetCode": "USD", "assetScale": 2 }, "description": "Great blog bob!" } ``` - `state` is already set to authorized as Coil has permission to do this and isn't going to wait for a quote from Fynbos before authorizing. - `receivingAccount` is Bob's Payment Pointer. - `sendAmount` is the amount to send from Alice's account. - `description` is used to provide a description for Bob on the incoming payment. Within Fynbos, the resource server (Rafiki) checks the grant associated with the provided token by making an introspection request to the authorisation server (AS) and gets the following response body: ```json { "active": true, "grant": "PRY5NM33OM4TB8N6BW7", "access": [ { "type": "outgoing-payment", "actions": [ "create" "authorize" ], "locations": [ "https://fynbos.me/alice/" ], "startAt": "2022-02-03T18:25:43.511Z", "expiresAt": "2023-02-03T18:25:43.511Z", "limits": { "startAt": "2022-02-03T18:25:43.511Z", "expiresAt": "2023-02-03T18:25:43.511Z", "interval": "P1M", "sendAmount": { "amount": 1000, "assetCode": "USD", "assetScale": 2 } } } ], "key": { "proof": "httpsig", "jwk": { "kty": "RSA", "e": "AQAB", "kid": "xyz-1", "alg": "RS256", "n": "kOB5rR4Jv0GMeL...." } }, } ``` - `active` indicates the token is valid (hasn't expired or been revoked.) - `access` describes the permissions granted by this token - `key` defines the client key that the grant is bound to and that should be used to sign the request (i.e. the token is not a bearer token) The RS tracks balances for each time interval as required for every `grant` with an interval based limit. Even though the AS confirms that this access token is valid, the RS may decline the client's request if the amount that it wants to send exceeds the limit per interval. In this case, Fynbos has no record of any transactions using this grant in the current month so it creates a new balance to track payments for this month (February 2022). The opening balance of `PRY5NM33OM4TB8N6BW7_FEB_22` is $0 and will go up to $2 if this payment completes. Fynbos is happy that Coil is authorised to create the payment so it does so and returns the following response: ```http HTTP/1.1 201 Created Content-Type: application/op-outgoing-payment-v1+json { "id": "https://fynbos.me/alice/fi7td6dito8yf6t" "accountId": "https://fynbos.me/alice/", "state": "authorized", "sendAmount": { "amount": 200, "assetCode": "USD", "assetScale": 2 }, "description": "Great blog bob!" "receipts: [] } ``` Fynbos now attempts to create an incoming payment at Bob's payment pointer to accept the payment. ```http POST /bob HTTP/1.1 Host: uphold.com Content-Type: application/op-incoming-payment-v1+json Accept: application/op-incoming-payment-v1+json Signature-Input: gnap=("@request-target" "host" "authorization");created=1624564850;keyid="fynbos-1" Signature: gnap=:EN/rExQ/knVi61P5AFhyMGN7aVPzk/9C7nsYAWF2RvzsoV1uNxGZklM55qCIQpuhoNty4EhiH7iwuzZBbRCQcQ==: Authorization: GNAP M2PMHKUR64TB8N6BW7OZB8CDFONP219RP1LT0765 { "description": "Great blog bob!", "receiptsEnabled": true } ``` - `description` provides a free text description that can be shown to Bob by his provider. - `receiptsEnabled` requests that the receiver return receipts. Uphold creates the incoming payment and returns the Interledger payment details. Since Fynbos requested receipts, Uphold has generated a random nonce and secret for signing the receipts. Bob may have configured his account at Uphold to use a known seed for generating the receipt secret, in which case he is able to generate the secret himself once he knows the nonce. ```http HTTP/1.1 201 Created Content-Type: application/op-incoming-payment-v1+json { "id": "https://uphold.com/bob/87tfi7td6dito8yf", "accountId": "https://uphold.com/bob", "state": "pending", "ilpAddress": "g.uphold.Cty6C+YB5X9FhSOUPCL", "sharedSecret": "6jR5iNIVRvqeasJeCty6C+YB5X9FhSOUPCL/5nha5Vs=", "receiptNonce": "Cty6C+YB5X9FhSOUPCL" } ``` - `id` the URL identifier of the new incoming payment - `accountId` the URL identifier of the account getting paid - `state` the state of the payment is pending so it is still able to receive payments - `ilpAddress` the ILP address to send to - `sharedSecret` the shared secret to use for the STREAM connection - `receiptNonce` indicates that the STREAM connection will return receipts with this nonce. Fynbos opens a STREAM connection and starts sending payments via Interledger until it has sent $2 from Alice's account. It stores the latest receipt for each ILP STREAM stream as they are received. When Fynbos is finished sending it marks the payment complete: ```http PUT /bob/87tfi7td6dito8yf HTTP/1.1 Host: uphold.com Content-Type: application/op-incoming-payment-v1+json Accept: application/op-incoming-payment-v1+json { "status": "complete" } ``` This helps Uphold with the internal processing of the payment. Uphold knows that no further payments will be recieved so it can report the payment as complete to Bob including the final amount received. Uphold responds: ```http HTTP/1.1 200 Success Content-Type: application/op-incoming-payment-v1+json { "id": "https://uphold.com/bob/87tfi7td6dito8yf", "accountId": "https://uphold.com/bob", "state": "completed", "receivedAmount": { "amount": 198, "assetCode": "USD", "assetScale": 2 }, "description": "Great blog bob!", "receipts": [ { "nonce": "Cty6C+YB5X9FhSOUPCL", "stream": 0, "total": 198, "hmac": "6C+YB5X9FhSO3UPCCty6C+YB5X9FhSOUP5CL" } ] } ``` - `status` indicated that the incoming payment is complete and the STREAM connection won't accept any more incoming packets. - `receivedAmount` indicates the amount received ($1.98). - `receipts` is an array of the latest receipt for each stream on the underlying STREAM connection. At any time Coil may poll the outgoing-payment for the latest state: ```http GET /alice/fi7td6dito8yf6t HTTP/1.1 Host: fynbos.me Accept: application/outgoing-payment-v1+json ``` The response includes the STREAM receipts that have been received by Fynbos. A response that is received while the payment is in progress might look like this: ```http HTTP/1.1 200 Success Content-Type: application/op-outgoing-payment-v1+json Cache-Control: no-cache { "id": "https://fynbos.me/alice/fi7td6dito8yf6t", "accountId": "https://fynbos.me/alice/", "state": "authorized", "receivingPayment": "https://uphold.com/bob/87tfi7td6dito8yf", "sendAmount": { "amount": 200, "assetCode": "USD", "assetScale": 2 }, "description": "Great blog bob!", "receipts": [ { "nonce": "VR66CYB5X9FhSOUPCL", "stream": 0, "total": 73, "hmac": "9B025X9FhSO3UPCCty6C+YB5X9FhSOUP5CL" } ] } ``` A response after the payment is completed: ```http HTTP/1.1 200 Success Content-Type: application/op-outgoing-payment-v1+json Cache-Control: max-age=348756 { "id": "https://fynbos.me/alice/fi7td6dito8yf6t", "accountId": "https://fynbos.me/alice/", "state": "completed", "receivingPayment": "https://uphold.com/bob/87tfi7td6dito8yf", "sendAmount": { "amount": 200, "assetCode": "USD", "assetScale": 2 }, "description": "Great blog bob!", "receipts": [ { "nonce": "Cty6C+YB5X9FhSOUPCL", "stream": 0, "total": 198, "hmac": "6C+YB5X9FhSO3UPCCty6C+YB5X9FhSOUP5CL" } ] } ``` Bob is able to query the incoming payment at Uphold and get the receipt secret. The request from Bob must be authenticated as only Bob should get the receipt secret in the response. ```http GET /bob/87tfi7td6dito8yf HTTP/1.1 Host: uphold.com Accept: application/op-incoming-payment-v1+json Signature-Input: gnap=("@request-target" "host" "authorization");created=1624564850;keyid="bob" Signature: gnap=:EN/rExQ/knVi61P5AFhyMGN7aVPzk/9C7nsYAWF2RvzsoV1uNxGZklM55qCIQpuhoNty4EhiH7iwuzZBbRCQcQ==: Authorization: GNAP OS9M2PMHKUR64TB8N6BW7OZB8CDFONP219RP1LT0 ``` The response includes the full state of the incoming payment and because Bob has the permission to see it, it also includes the receipt secret. ```http HTTP/1.1 200 Success Content-Type: application/op-incoming-payment-v1+json { "id": "https://uphold.com/bob/87tfi7td6dito8yf", "accountId": "https://uphold.com/bob", "state": "processing", "receivedAmount": { "amount": 12, "assetCode": "USD", "assetScale": 2 }, "description": "Great blog bob!", "receiptSecret": "Aws5B78B8H9FhTRSPAB", "receipts": [ { "nonce": "Cty6C+YB5X9FhSOUPCL", "stream": 0, "total": 12, "hmac": "6CsYB5X9FhSO3UPCCty7FB5X9FhSOUP5CL" } ] } ``` ### Edge Case: Alice tips Charly through the Coil extension and exceeds the $10 grant Alice is on Charly's website and the Coil extension has parsed Charly's Payment Pointer from the page: `https://uphold.com/charly` Alice wants to send a tip to Charly and clicks the "Tip" button on the extension. Alice elects to send $5 and provides a message of "Fantastic knowledge base, Charly!". **Note that Alice has already tipped $7 over the course of the current month and the tip she wants to send will exceed the grant she gave to Coil.** Coil creates the outgoing payment at Alice's Payment Pointer: ```http POST /alice HTTP/1.1 Host: fynbos.me Content-Type: application/op-outgoing-payment-v1+json Accept: application/op-outgoing-payment-v1+json Signature-Input: gnap=("@request-target" "host" "authorization");created=1624564850;keyid="xyz-client" Signature: gnap=:EN/rExQ/knVi61P5AFhyMGN7aVPzk/9C7nsYAWF2RvzsoV1uNxGZklM55qCIQpuhoNty4EhiH7iwuzZBbRCQcQ==: Authorization: GNAP OS9M2PMHKUR64TB8N6BW7OZB8CDFONP219RP1LT0 { "state": "authorized", "receivingAccount": "https://uphold.com/charly", "sendAmount": { "amount": 500, "assetCode": "USD", "assetScale": 2 }, "description": "Fantastic knowledge base, Charly!" } ``` Within Fynbos the resource server (Rafiki) checks the grant associated with the provided token and gets the following response. ```json { "active": true, "grant": "PRY5NM33OM4TB8N6BW7", "access": [ { "type": "outgoing-payment", "actions": [ "create" "authorize" ], "locations": [ "https://fynbos.me/alice/" ], "startAt": "2022-02-03T18:25:43.511Z", "expiresAt": "2023-02-03T18:25:43.511Z", "limits": { "startAt": "2022-02-03T18:25:43.511Z", "expiresAt": "2023-02-03T18:25:43.511Z", "interval": "P1M", "sendAmount": { "amount": 1000, "assetCode": "USD", "assetScale": 2 } } } ], "key": { "proof": "httpsig", "jwk": { "kty": "RSA", "e": "AQAB", "kid": "xyz-1", "alg": "RS256", "n": "kOB5rR4Jv0GMeL...." } }, } ``` The RS keeps balances for each time interval for every `grant`. Even though the AS confirms that this access token is valid, the RS declines the client's request because the amount that it wants to send exceeds the limit per interval. The balance of `PRY5NM33OM4TB8N6BW7_FEB_22` is $7. Fynbos returns a response: ```http HTTP/1.1 409 Conflict Content-Type: application/op-outgoing-payment-v1+json { "error": "Payment exceeds limit." } ``` The Coil extension prompts Alice that the tip exceeds her monthly limit and that she needs to extend the grant. She agrees to that. Coil continues its grant request with the AS ```http POST auth/continue HTTP/1.1 Host: fynbos.dev Content-Type: application/json Authorization: GNAP YT98RY5NM33OMUKMK65U Signature-Input: sig1=... Signature: sig1=... Digest: sha256=... { "access_token": { "access": [ { "type": "outgoing-payment", "actions": [ "create", "authorize" ], "locations": [ "https://fynbos.me/alice" ], "limits": { "startAt": "2022-02-03T18:25:43.511Z", "expiresAt": "2023-02-03T18:25:43.511Z", "interval": "P1M", "sendAmount": { "amount": 1000, "assetCode": "USD", "assetScale": 2 } } }, { "type": "outgoing-payment", "actions": [ "create", "authorize" ], "locations": [ "https://fynbos.me/alice" ], "limits": { "expiresAt": "2022-02-28T23:59:59.999Z", "sendAmount": { "amount": 200, "assetCode": "USD", "assetScale": 2 } } } ] }, "client": { "display": { "name": "Coil", "uri": "https://coil.com" }, "key": { "proof": "httpsig", "jwk": { "kty": "RSA", "e": "AQAB", "kid": "xyz-1", "alg": "RS256", "n": "kOB5rR4Jv0GMeL...." } } }, "interact": { "start": [ "redirect" ], "finish": { "method": "redirect", "uri": "https://coil.com/return/876FGRD8VC", "nonce": "LKLTI25DK82FX4T4QFZC" } } } ``` Fynbos recognises that Coil is asking for additional access on top of the existing grant. It sends back an instruction to Coil to redirect Alice to a Fynbos web page to authenticate herself and consent to the updated grant request. ```json { "interact": { "redirect": "https://fynbos.dev/auth/HQ4CF492MLVMSW9MKMXK", "finish": "LHMBDOFXG4Y5CVJCX821" }, "continue": { "access_token": { "value": "KU80UPRY5NM33OMUKMKS" }, "uri": "https://fynbos.dev/auth/continue" } } ``` Coil directs Alice to https://fynbos.dev/auth/HQ4CF492MLVMSW9MKMXK which is a web page hosted by Fynbos. Fynbos authenticates Alice and then prompts Alice to consent to giving Coil access to her account wth the updated limit of $12 for the current month. She consents Fynbos responds to Coil with the following: ```json { "access_token": { "value": "T0OS9M2PMHKUR64TB8N6BW7OZB8CDFONP219RP1L", "manage": "https://fynbos.dev/auth/token/W7PRY5NM33OM4TB8N6B", "expires_in": 762534, "grant": "PRY5NM33OM4TB8N6BW7", "access": [ { "type": "outgoing-payment", "actions": [ "create", "authorize", ], "locations": [ "https://fynbos.me/alice" ], "limits": { "startAt": "2022-02-03T18:25:43.511Z", "expiresAt": "2023-02-03T18:25:43.511Z", "sendAmount": { "amount": 1000, "assetCode": "USD", "assetScale": 2 }, "interval": "P1M" } },{ "type": "outgoing-payment", "actions": [ "create", "authorize" ], "locations": [ "https://fynbos.me/alice" ], "limits": { "expiresAt": "2022-02-28T23:59:59.999Z", "sendAmount": { "amount": 200, "assetCode": "USD", "assetScale": 2 } } }] }, "continue": { "access_token": { "value": "5UYT98RY5NM33OMUKMK6" }, "uri": "https://fynbos.dev/auth/continue" } } ``` Coil now has a grant to create and authorize outgoing payments from Alice's account up to a value of $12 for this month. The rest of the flow is as described in [Step 2](#Step-2-Alice-tips-Bob-through-the-Coil-extension).