The audience for the Mechant/Customer Interaction Guide is the merchant who wishes to set up a “web shop” that works with the Taler payment system.
The Taler payment cycle involves six parties: (a) customer, (b) exchange, (c) merchant, (d) customer’s bank, (e) exchange’s bank, (f) merchant’s bank.
The exchange is the central entity that mediates the wire transfer of real currency between (d), (e), (f) by way of coins, cryptographically secure tokens passed between (a), (b), (c).
There are six steps to a Taler payment cycle.
In step 1, (a) directs (d) to make real funds available to (b).
In step 2, (d) does a wire transfer of real funds to (e), fulfilling the request from step 1. (b) generates coins corresponding to those real funds; these are called the reserve.
In step 3, (a) withdraws coins, either wholly or partially, from (b). These coins are kept in a wallet under control of (a). The coins in the wallet are unlinkable to the identity of (a) that was revealed during the withdraw operation.
In step 4, (a) authorizes payment of coins from the wallet to (c). This transfers payment coins from the wallet to (c), and change coins from (b) to the wallet (unless the payment amount exactly matches the denomination of the coins in the wallet).
In step 5, (c) deposits coins into (b). At this point, (b) knows the identity of (c), but not of (a). Taler uses cryptography to validate that the coins are unique and were issued by (b), but (b) cannot determine to whom the coins were originally issued.
In step 6, (b) directs (e) to wire transfer real funds corresponding to the accumulated deposited coins to (f).
NB: The Taler payment cycle is part of the Taler payment system, which includes also an auditor component, not described here.
This guide focuses on step 4, the interaction between the customer and the merchant. In particular, we first review two basic interaction flows (with and without shopping cart), then describe Taler features involved in the interaction, the decisions you (the merchant) must make, and how to configure the Taler merchant backend to best support those decisions. Lastly, we present protocol traces for various fictitious interaction flows.
There are two basic payment flows, the first involving a shopping cart, and the second, without (individual product selection / purchase). We distinguish these because for some purchases, a shopping cart is overkill. In either case, Taler can integrate with your inventory management system. Additionally, Taler offers repurchase detection / prevention, most suitable for digital goods.
In the shopping cart experience, you first offer a product on the website. The customer adds the product to their shopping cart, at which point you may optionally lock the product in the inventory system for a certain period of time. The accumulated set of products in the shopping cart is the order. This process repeats until the customer is ready to move to the checkout phase.
At checkout, you may optionally support different payment methods (and make this choice available to the customer) for the order. This guide assumes you and the customer agree to use the Taler payment system.
At this point, you generate a contract and present it to the customer for authorization. The contract includes:
If the customer does nothing (timeout / the contract expires), the merchant backend automatically unlocks the product(s), allowing other consumers to add more items of the limited stock to their orders.
On the other hand, if the customer authorizes payment, the customer’s wallet transfers payment coins to you, previously locked products are removed from inventory, and (if possible) the wallet redirects the customer to the fulfillment URI.
The individual product selection / purchase experience is like the shopping cart experience with the following exceptions: - there is no shopping cart – the order is solely the selected product; - Taler payment method is assumed; - customer selection moves directly to checkout; - repurchase detection / prevention can be useful (for digital products).
This section describes aspects of Taler involved in the basic payment flows in more detail. Each aspect also includes one or more backend API calls that are demonstrated in the next section.
Taler can integrate with your inventory system to set aside a certain quantity of a product for some duration of time. This is called product locking. This is useful for physical goods, or for goods that have a limited supply, such as airline tickets. Even for digital goods, product locking may be useful to effect exclusivity.
To lock a product, use:
POST [/instances/$INSTANCE]/private/products/$PRODUCT_ID/lock
,
specifying a duration
and a quantity
.
If the customer removes a product from the shopping cart, you can unlock
the product by using the same API call, specifying a quantity
of 0 (zero).
(Products are also unlocked automatically on timeout / contract expiration.)
Before you can lock products, you need to manage the inventory, creating an entry for the product (assigning a $PRODUCT_ID) and configure the available stock. This can be done using the Taler merchant backoffice Web interface.
Note
Once we have documentation for that web interface, we should link to it here.
price
maintained by the backend.
Taxes can be set when the product is added to the inventory,
prior to any customer purchase experience
(see POST [/instances/$INSTANCE]/private/products
,
GET [/instances/$INSTANCE]/private/products
,
and GET [/instances/$INSTANCE]/private/products/$PRODUCT_ID
)
or specified explicitly by the frontend when adding
products to an order that are not managed by the backend inventory
(see POST [/instances/$INSTANCE]/private/orders
).The Taler protocol charges a deposit fee (see step 5, above), which you may choose to pay or to pass on to the customer. This can be configured to a maximum amount, per order.
You can set default_max_deposit_fee
in POST /management/instances
,
or override the default by setting max_fee
when creating an order.
There is also the wire fee (see step 6, above), which you may choose to pay or to pass on to the customer.
You can set default_max_wire_fee
in POST /management/instances
,
and max_wire_fee
in the contract.
If unspecified, the default value is zero (meaning you bear the entire fee).
You can amortize the wire fee across a number of customers
by setting default_wire_fee_amortization
in POST /management/instances
,
and wire_fee_amortization
in the contract.
This is the number of customer transactions over which you expect to
amortize wire fees on average.
If unspecified, the default value is one.
Note
POST /management/instances
must be done at
instance-setup time (prior to any purchase).
Although Taler allows the customer to remain anonymous, you may need to collect customer details (e.g. for shipping). Taler has support for forgetting such details, to comply with GDPR (for example). This can occur even in the face of refunds (see below).
To forget a set of details, first the details that are to be forgotten
must be marked by including the names of the respective fields
in one or more special _forgettable
field(s) in the contract.
Then, you can use:
PATCH [/instances/$INSTANCE]/private/orders/$ORDER_ID/forget
to forget those details.
The claim token is a sort of handle on the order and its payment. It is useful when the order ID is easily guessable (e.g. incrementing serial number), to prevent one customer hijacking the order of another. On the other hand, even if the order ID is not easily guessable, if you don’t care about order theft (e.g. infinite supply, digital goods) and you wish to reduce the required processing (e.g. smaller QR code), you can safely disable the claim token.
By default, Taler creates a claim token for each order.
To disable this, you can specify create_token
to be false
in POST [/instances/$INSTANCE]/private/orders
.
The refund deadline specifies the time after which you will prohibit refunds. Refunds may be full or partial. Refunds do not require customer details. You can configure the deadline to expire immediately to effect an “all sales are final” policy.
To set the deadline, specify refund_delay
in POST [/instances/$INSTANCE]/private/orders
.
To disable refunds altogether, omit this field.
The Taler protocol can automatically offer refunds to the customer’s wallet without their explicit prompting during the auto-refund period.
This is useful in the case where the purchase cannot be fulfilled (e.g. jammed vending machine), but there is no way to notify the customer about a refund.
If specified, after contract authorization, the customer’s wallet will repeatedly check for either fulfillment or refund, up to the end of the auto-refund period. (If neither occur within that period, the customer should complain to you for breach of contract.)
To set the auto-refund period, specify auto_refund
in POST [/instances/$INSTANCE]/private/orders
.
Taler can detect a repurchase attempt and prevent it from going through. This feature allows customers to purchase a digital good only once, but to later access the same digital good repeatedly (e.g. reload in browser, after network trouble, etc.) without having to pay again.
This feature is automatic in the protocol; you do not need to do anything to enable it.
Note
For repurchase detection / prevention to work reliably, you must use the same fulfillment URI for the same product and likewise different fulfillment URIs for different products.
This may be the actual product (digital goods), or a tracking URL (physical goods). If you issue a claim token with the contract, the customer can access the fulfillment URI from a different device than the one where the wallet is installed.
The fulfillment URI is normally included in the contract.
You specify it in POST [/instances/$INSTANCE]/private/orders
.
If the fulfillment URI contains the literal string ${ORDER_ID}
(including curly braces), that will be replaced by the order ID when
POSTing to the merchant. (FIXME: What does “POSTing to the merchant” mean?)
This is useful when the backend auto-generates the order ID.
In the following descriptions, C
stands for customer, W
stands for
customer’s wallet, M
stands for merchant (you), and E
stands for
exchange.
Unless otherwise noted, all API calls are directed toward the Taler backend.
Also, all the traces share the initial pre-sales configuration step.
In the pre-sales configuration step, you set up the default instance, and add products to the inventory.
NOTE: not sure we want to ultimately document this HERE. Most merchants should do _this_ part via the Merchant Web interface that Sebastian is building right now, and for that we want a separate guide that explains the API (as you do here), and the Web interface. In this document, we should focus on how the merchant integrates the (Web)front-end with the backend, not how the backend itself is configured. (This also applies to the other instance setup parts you described above => refer to other guide, but of course specify how we can override defaults from instance setup per-order.)
// InstanceConfigurationMessage
{
"accounts": [{"payto_uri":"payto://iban/CH9300762011623852957"}],
"id": "default",
"name": "Pretty Pianos",
"auth":
// InstanceAuthConfigurationMessage
{
"method": "external",
"token": "secret-token:eighty-eight-keys"
},
"default_max_wire_fee": "KUDOS:5.0",
"default_wire_fee_amortization": 1,
"default_max_deposit_fee": "KUDOS:10.0",
"default_wire_transfer_delay": "2 days",
"default_pay_delay": "5 hours"
}
// (backend returns 204 No content)
The fictitious store, Pretty Pianos, has only two products: - pianos (physical good); - Beethoven Sonatas (sheet music PDF files, digital good).
M: POST /instances/default/private/products
// ProductAddDetail
{
"product_id": "p001",
"description": "piano",
"unit": "unit",
"image": "data:image/png;base64,AAA=",
"price": "KUDOS:20000.0",
"taxes": [],
"total_stock": 3,
"next_restock": "2021-04-22",
"_forgettable": ["image"]
}
// (backend returns 204 No content)
Note that the image
field is mentioned by name in the _forgettable
field’s list value.
This means the image
value is marked as forgettable.
This will come into play later (see below).
M: POST /instances/default/private/products
// ProductAddDetail
{
"product_id": "f001",
"description": "Beethoven Sonatas",
"unit": "file",
"price": "KUDOS:9.87",
"taxes": [],
"total_stock": -1
}
// (backend returns 204 No content)
Note that there is no next_restock
field in this ProductAddDetail
object.
This is because the total_stock
field has value -1
(meaning “infinite”)
since the product is a PDF file.
The first scenario is a simple file purchase, without shopping cart, similar to the GNU Essay demo experience.
Because there are infinite supplies of product f001
,
there is really no need for inventory management.
However, you choose to anyway generate a separate order ID
in the backend for accounting purposes.
Also, since the product is an easily reproduced digital good,
you decline to offer the customer the ability to select a “quantity”
other than 1 (one), and decide that “all sales are final”
(no refund possible).
On the other hand, you wish to enable repurchase detection /
prevention feature, so that once customers pay for the PDF file,
they need never pay again for it.
When the customer clicks on the product’s “buy” button,
you first POST to /private/orders
to create an order:
M: POST /instances/default/private/orders
// PostOrderRequest
{
"order":
// Order (MinimalOrderDetail)
{
"amount": "KUDOS:9.87",
"summary": "Beethoven Sonatas",
"fulfillment_URI": "https://example.com/f001?${ORDER_ID}"
},
"create_token": true
}
Notes:
refund_delay
field (no refunds possible).create_token
field with value true
even though that is the default (for illustrative purposes).order
value is actually a MinimalOrderDetail
object.fulfillment_URI
value includes the product ID and the literal string ${ORDER_ID}
, to be replaced by the backend-generated order ID.The backend returns 200 OK
with the body:
// PostOrderResponse
{
"order_id": "G93420934823",
"token": "TEUFHEFBQALK"
}
Notes:
- The backend-generated order ID is G93420934823
.
- The claim token is TEUFHEFBQALK
.
(FIXME: Replace w/ more realistic examples?)
Now that there is an order in the system, the wallet claims the order.
W: POST /orders/G93420934823/claim
// ClaimRequest
{
"nonce": "lksjdflaksjfdlaksjf",
"token": "TEUFHEFBQALK"
}
Notes:
nonce
value is a randomly-generated string.G93420934823
.token
value is the claim token TEUFHEFBQALK
received in the PostOrderResponse
.The backend returns 200 OK
with body:
// ContractTerms
{
"summary": "one copy of Beethoven Sonatas",
"order_id": "G93420934823",
"amount": "KUDOS:9.87000000",
"fulfillment_url": "https://example.com/f001?G93420934823",
"max_fee": "KUDOS:0.01500000",
"max_wire_fee": "KUDOS:0.01500000",
"wire_fee_amortization": 1,
"products": [
// Product
{
"product_id": "f001",
"description": "Beethoven Sonatas"
}
],
"timestamp": { "t_ms": 1616537665000 },
"refund_deadline": { "t_ms": 1616537665000 },
"pay_deadline": { "t_ms": 1616537725000 },
"wire_transfer_deadline": { "t_ms": 1616537785000 },
"merchant_pub": FIXME,
"merchant_base_url": "https://example.com/",
"merchant":
// Merchant
{
},
"h_wire": FIXME,
"wire_method": FIXME,
"auditors": [
// Auditor
],
"exchanges": [
// Exchange
],
"nonce": "lksjdflaksjfdlaksjf"
}
Notes:
order_id
value is the one given in the PostOrderResponse
.timestamp
value represents 2021-03-23 22:14:25 UTC
in milliseconds after the epoch.refund_deadline
value is the same as the timestamp
value
(no refunds possible).pay_deadline
value is one minute after the timestamp
value.wire_transfer_deadline
value is two minutes after
the timestamp
value.products
value is a list of one element (one Product
object),
which omits the price
field since that is included in the
ContractTerms.amount
value. Also, its quantity
defaults to 1 (one).nonce
value is the same one specified by the wallet.At this point, the wallet displays the contract terms (or a subset of them) to the customer, who now has the option to accept the contract or reject it (either explicitly by pressing a “cancel” button, or implicitly by waiting for the offer to time out).
The customer accepts the contract:
W: POST /orders/G93420934823/pay
// PayRequest
{
"coins": [
// CoinPaySig
{
"coin_sig": ...,
"coin_pub": ...,
"ub_sig": ...,
"h_denom": ...,
"contribution": "KUDOS:8.0",
"exchange_url": ...
},
{
"coin_sig": ...,
"coin_pub": ...,
"ub_sig": ...,
"h_denom": ...,
"contribution": "KUDOS:2.0",
"exchange_url": ...
}
]
}
Notes:
PayRequest
object.The backend returns 200 OK
with body:
// PaymentResponse
{
"sig": "..." // EddsaSignature
}
FIXME: At this point, does the wallet need to query (status)? Also, does the frontend need to do anything else?
The wallet then redirects to the fulfillment URI, which displays (or makes available for download) the PDF file “Beethoven Sonatas”.
TODO/FIXME: Add more scenarios (including JSON).