The 'wallet' APIs are related to making donations/purchases (and receiving money).
A user's wallet may have any of the following in it:
- Consumer card information - one or more "card"s which correspond to a Stripe payment method.
- Provider account information - a singular connected Stripe express account
For now, we are not considering provider account information.
As well as cards/provider accounts, a user may have, associated with their wallet, any number of transactions. Transactions are independent of ownership of pay-for-apps etc. They are merely logs of the user's activity. Unlike cards, and provider information, which are stored on Stripe, the transactions are mirrored into the backend's database so that we can store semantic info about them.
If the user is not logged in, all APIs will return a 403
. Ditto if a user
tries to retrieve a transaction belonging to another user.
We group all wallet related APIs under /wallet/
because that way they're
nicely collated. In addition, the fact we're using Stripe is as abstracted as
possible from the API, though where there are Stripe-specific bits, they're
clearly labelled as Stripe so that if we want to switch to a different provider
in the future we can with fairly well constrained and grepable things to deal
with at the API level.
Returns a JSON object with the wallet details
{
"status": "ok",
"cards": [],
"account": {}
}
The cards
entry will be an empty list if the user's wallet has no saved cards,
which is perfectly permissible as the user may have chosen to not save card
details.
The account
entry will be absent if the user has not set up a payment
account for flatpaks they control. (For now, this will always be absent)
If there are any cards, each card is a dict of the form:
{
"id": "...",
"brand": "visa",
"country": "US",
"exp_month": 8,
"exp_year": 2022,
"last4": "1234"
}
This permits a reasonable render of the cards in the frontend. The country will
be an ISO-3166-1 alpha-2 country code. The card's brand will be one of:
American Express, Diners Club, Discover, JCB, MasterCard, UnionPay, Visa, or
Unknown. For consistency, it will always be lowercased and spaces will be
removed, leading to values like americanexpress
or jcb
.
If the user wants to bin a card off from their account's wallet, the frontend
will make a POST to the backend, passing the full card details in. If there
is not a card which exactly matches what is POSTed in, then an error will be
returned. If the card is successfully deleted, a 201 No Content
will be
returned. Errors will take the form of:
{
"status": "error",
"error": "..."
}
The error may be any of a number of issues such as "card not found" or "failure talking to stripe".
Because a user could end up with a significant number of transactions over time, this API is designed to support paging. As such, the following query string arguments are available:
sort
- takes one of:recent
,oldest
and sorts the transactions in that order. The default if not provided isrecent
. The sort key iscreated
.since
- takes a transaction ID and returns the next transactions after the named transactionlimit
- the maximum number of transactions to return, if this number is more than 100, the backend will clamp it to 100
The returned data is in the form:
[
{
"id": "...",
"value": 1234,
"currency": "usd",
"kind": "donation",
"status": "success",
"reason": "...",
"created": 1646210893,
"updated": 1646210893
} // ...
]
The status
value will be one of new
, pending
, retry
, success
,
or cancelled
. If the status is cancelled
or retry
, then there will
be a reason
field too. If the status is not one of those then there will not
be a reason
field.
The currency
is always a 3 letter ISO currency code in lower case. The value
is always shown in the currency's smallest denomination (e.g. number of cents).
The frontend is at liberty to use a browser built-in such as Intl.NumberFormat to then render the currency nicely based not only on the currency, but also the user's locale. It will be useful to know the set of currencies which are integral (have no decimals) Stripe has such info at hand. This information will be especially important when it comes to getting users to make payments because we will need to be careful. Of course, if we limit ourselves to only dollars, things will be easier.
The kind
field will be donation
if it was a pure donation, or else it
will be purchase
. In the latter case, some parts of the purchase may end up
being donations by default, this will show in the detailed transaction data
(see below).
The created
field is when the transaction was first created. The updated
field is when the transaction was last updated (e.g. when it completed or
failed).
Retrieve more details about a given transaction. This might be used in an expansion of a transaction, or on an "Invoice" page. As such, this endpoint will return the same information as the transactions list, but also information about the rows of the transaction
{
"summary": {
// Info as above
},
"card": {
// Information about the card which paid this, like the cards in the wallet
},
"details": [
// detail rows
]
}
Detail rows provide some information about the transaction's payouts. Each row consists of some information about who received how much from the transaction.
{
"recipient": "some.flatpak.name",
"amount": "...",
"currency": "...",
"kind": "..."
}
Where the recipient was Flathub itself, it shall be listed as "org.flathub.Flathub", and where the recipient was a platform, that will match the platform specification such as "org.freedesktop.Platform" or "org.gnome.Platform". The frontend may need to special-case these recipients rather than linking to their appinfo from the appstream.
Possible runtimes are listed here:
https://docs.flatpak.org/en/latest/available-runtimes.html though we suggest
that the frontend limits to the core Platform
runtime names for simplicity.
When the frontend wishes to make a transaction it should post the transaction in
the same shape as would be retrieved from the /wallet/transactions/{id}
endpoint, without the card
entry or the summary/status
field.
The backend will validate the request, compute a relevant payment intent, and
prepare an new
transaction.
If the transaction is bad, or an error occurs during the preparation of the new transaction then an error message will be returned of the form:
{
"status": "error",
"error": "...."
}
Errors might be due to the transaction data being inconsistent, or if the backend cannot construct the transaction with Stripe.
On success the backend will return:
{
"status": "ok",
"id": "..."
}
Where the id
is the ID of the newly created transaction. At this point, the
transaction is ready to be paid.
If there is a transaction in the new
or retry
state then the frontend can
attempt to make payment. Since this involves particular interaction with Stripe,
the frontend will need to retrieve the relevant information to be able to make a
Stripe payment. To do that the frontend will need to find out from the user if
they want to reuse a card already in their wallet, and if not, will have to take
card details etc. for the payment.
If the user opts to reuse a card, then the frontend should post an update to the transaction to set the card:
The transaction must be in new
or retry
status. The submitted card
must be of the form used in the above APIs and must match a card exactly
including ID etc. or it will not be accepted by the update.
Result of this API call will be:
{
"status": "ok",
// or
"status": "error",
"error": "..."
}
This is the only endpoint which doesn't require the user be logged in. It returns the public stripe data necessary for any page using stripe to initialise the stripe API.
{
"status": "ok",
"public_key": "...."
}
If the backend isn't configured with stripe data then this will give an error instead:
{
"status": "error",
"error": "..."
}
For transactions handled by stripe, fetching this will return stripe-specific
data which can be used by the frontend with Stripe.js elements etc. Only
transactions in new
or retry
state can have their stripe data fetched
Any other transaction will return 400 Bad Request
{
"client_secret": "...",
"card": {
// ...
}
}
If there is a pre-recorded card for this transaction then we return it here so that it can be part of the UI. If the card previously failed for whatever reason then the user may have to select to enter fresh details.
Stripe webhooks will cause the transaction to move through pending and thence
to either retry
or success
.
If the user has tried and tried, and doesn't want to try again, then the
frontend can cancel a transaction by requesting this endpoint. The transaction
must be in new
or retry
for this to work.
In addition, due to the asynchronous nature of Stripe, it's possible for a
cancelled transaction to move back to retry
or to success
if the transaction
is updated by webhook. Cancelling a transaction you have attempted to make will
not prevent Stripe from trying to perform the payment if it's already in flight.