1.11.6. Sandbox API

Sandbox emulates a minimal bank, to provide EBICS access to test Taler. The top-level API defines two main groups: demobanks and legacy. Currently, error types are common to both groups.

Demobanks

Sandbox is designed to allow multiple demobanks being hosted, where every demobank can have its own configuration (including a different currency). A demobank has a name, although currently only one demobank, named default, is supported. Such demobank activates the API segment /demobanks/default, under which several APIs are then served. The following sections describe all such APIs.

Circuit API

This API offers to manage a selected group of users who act as businesses for a local currency. Policies to disincentivize cashout operations may also apply, making therefore this setup a circuit within a wider traditional currency.

For brevity, the list of response statuses for each endpoint may not be exhaustive.

Note

This API requires to disable ordinary registrations in the configuration, to avoid other APIs from circumventing this registration policy. See libeufin-sandbox config --help.

The following endpoints are served under /demobanks/default/circuit-api.

The client side of this API is offered both along the CLI and the SPA. The SPA is generally served along Sandbox / path.

Accounts

POST /accounts

Create a new bank account. Only the administrator is allowed.

Request:

interface CircuitAccountRequest {
  // Username
  username: string;

  // Password.
  password: string;

  // Addresses where to send the TAN.  If
  // this field is missing, then the cashout
  // won't succeed.
  contact_data: CircuitContactData;

  // Legal subject owning the account.
  name: string;

  // 'payto' address pointing the bank account
  // where to send payments, in case the user
  // wants to convert the local currency back
  // to fiat.
  cashout_address: string;

  // IBAN of this bank account, which is therefore
  // internal to the circuit.  Randomly generated,
  // when it is not given.
  internal_iban?: string;
}
interface CircuitContactData {

  // E-Mail address
  email?: EmailAddress;

  // Phone number.
  phone?: PhoneNumber;
}

Response:

204 No content:
The account was successfully created.
400 Bad request:
Input data was invalid. For example, the client specified a invalid phone number or e-mail address.
403 Forbidden:
The response should indicate one of the following reasons.
  • A institutional username was attempted, like admin or bank.
  • A non admin user tried the operation.
409 Conflict:
At least one registration detail was not available, the error message should inform about it.
DELETE /accounts/$username

Delete the account whose username is $username. The deletion succeeds only if the balance is zero. Only the administrator is allowed.

Response:

204 No content:
The account was successfully deleted.
403 Forbidden:
The administrator specified a institutional username, like admin or bank.
404 Not found:
The username was not found.
412 Precondition failed:
The balance was not zero.
PATCH /accounts/$username

Allows the administrator and the user to reconfigure the account data of $username.

Note

Only the administrator has the rights to change the user legal name.

Request:

interface CircuitAccountReconfiguration {

  // Addresses where to send the TAN.
  contact_data: CircuitContactData;

  // Optional field.  'Payto' address pointing the
  // bank account where to send payments, in case the
  // user wants to convert the local currency back
  // to fiat.  If this field is missing, any cash-out
  // address will be deleted and the user loses their
  // cash-out capability.
  cashout_address?: string;

  // Legal name associated with $username.  NOTE:
  // only the administrator can change this value.
  name?: string;
}

Response:

204 No content:
Operation successful.
403 Forbidden:
The rights to change $username are not sufficient. That includes the case where a correctly authenticated user tries to change their legal name. It also includes the case where ‘admin’ tries to change its own account.
404 Not found:
The account pointed by $username was not found.
PATCH /accounts/$username/auth

Allows administrators and ordinary users to change the account’s password.

Request:

interface AccountPasswordChange {

  // New password.
  new_password: string;
}

Response:

204 No content:
Operation successful.
GET /accounts

Obtains a list of the accounts registered at the bank. It returns only the information that this API handles, without any balance or transactions list. The Access API offers that. This request is only available to the administrator.

Request:

Query Parameters
  • filterOptional. Pattern to filter on the account legal name. Given the filter ‘foo’, all the results will contain ‘foo’ in their legal name. Without this option, all the existing accounts are returned.

Response:

CircuitAccounts

interfaces CircuitAccounts {
  customers: CircuitAccountMinimalData[];
}
interface Balance {
  amount: Amount;
  credit_debit_indicator: "credit" | "debit";
}
interface CircuitAccountMinimalData {
  // Username
  username: string;

  // Legal subject owning the account.
  name: string;

  // current balance of the account
  balance: Balance;

  // Number indicating the max debit allowed for the requesting user.
  debitThreshold: string;
}
200 OK:
At least one account was found.
204 No Content:
No accounts were found for the given request.
403 Forbidden:
A ordinary user invoked this call.
GET /accounts/$username

Obtains information relative to the account owned by $username. The request is available to the administrator and $username itself.

Response:

interface CircuitAccountData {
  // Username
  username: string;

  // IBAN hosted at Libeufin Sandbox
  iban: string;

  contact_data: CircuitContactData;

  // Legal subject owning the account.
  name: string;

  // 'payto' address pointing the bank account
  // where to send cashouts.  This field is optional
  // because not all the accounts are required to participate
  // in the merchants' circuit.  One example is the exchange:
  // that never cashouts.  Registering these accounts can
  // be done via the access API.
  cashout_address?: string;
}
403 Forbidden:
The user is not allowed.

Cashouts

POST /cashouts

Initiates a conversion to fiat currency. The account to be credited is the one specified at registration time via the cashout_address parameter. The account to be debited is extracted from the authentication credentials. The bank sends a TAN to the customer to let them confirm the operation. The request is only available to ordinary users, not to the administrator.

Note

Consult the cashout rates call to learn about any applicable fee or exchange rate.

To test this operation without relying on any SMS/E-mail provider, Libeufin offers two methods: defining an environment variable called LIBEUFIN_CASHOUT_TEST_TAN or specifying the value file to the tan_channel field of the request object. Assuming LIBEUFIN_CASHOUT_TEST_TAN is set to T, every /confirm operation can use T as the TAN. Setting instead the tan_channel field to file will cause the server to (over)write every TAN to /tmp/libeufin-cashout-tan.txt. If both are used, the environment variable takes the precedence.

Request:

CashoutRequest

enum TanChannel {
  SMS = "sms",
  EMAIL = "email",
  FILE = "file"
}
interface CashoutRequest {

  // Optional subject to associate to the
  // cashout operation.  This data will appear
  // as the incoming wire transfer subject in
  // the user's external bank account.
  subject?: string;

  // That is the plain amount that the user specified
  // to cashout.  Its $currency is the circuit currency.
  amount_debit: Amount;

  // That is the amount that will effectively be
  // transferred by the bank to the user's bank
  // account, that is external to the circuit.
  // It is expressed in the fiat currency and
  // is calculated after the cashout fee and the
  // exchange rate.  See the /cashout-rates call.
  amount_credit: Amount;

  // Which channel the TAN should be sent to.  If
  // this field is missing, it defaults to SMS.
  // The default choice prefers to change the communication
  // channel respect to the one used to issue this request.
  tan_channel?: TanChannel;
}

Response:

interface CashoutPending {
  // UUID identifying the operation being created
  // and now waiting for the TAN confirmation.
  uuid: string;
}
202 Accepted:
The cashout request was correctly created and the TAN authentication now is pending.
400 Bad request:
The exchange rate was incorrectly applied.
403 Forbidden:
A institutional user (admin or bank) tried the operation.
409 Conflict:
The user did not share any contact data where to send the TAN.
412 Precondition failed:
The account does not have sufficient funds.
503 Service unavailable:
The bank does not support the TAN channel for this operation.
POST /cashouts/$cashoutId/abort

Aborts the $cashoutId operation. Original author and admin are both allowed.

Response:

204 No content:
$cashoutId was found in the pending state and got successfully aborted.
404 Not found:
$cashoutId is not found. Note: that happens also when $cashoutId got aborted before this request.
412 Precondition failed:
$cashoutId was already confirmed.
POST /cashouts/$cashoutId/confirm

Confirms the $cashoutId operation by accepting its TAN. The request should still be authenticated with the users credentials. Only the original author is allowed.

Request:

interface CashoutConfirm {

  // the TAN that confirms $cashoutId.
  tan: string;
}

Response:

204 No content:
$cashoutId was found in the pending state and got successfully confirmed.
403 Forbidden:
wrong TAN.
404 Not found:
$cashoutId is not found. Note: that happens also when $cashoutId got aborted before this request.
409 Conflict:
At least the following two cases are possible * an institutional user (admin or bank) tried the operation * the user changed their cash-out address between the creation and the confirmation of $cashoutId.
412 Precondition failed:
$cashoutId was already confirmed.
GET /cashouts/estimates

This endpoint shows how the bank would apply the cash-out ratio and fee to one input amount. Typically, frontends ask this endpoint before creating cash-out operations. There is no financial consequence to this endpoint and only registered users are allowed to request. At least one of the two query parameters should be provided. If both are given, then the server checks their correctness. Amounts must include the currency.

Request:

Query Parameters
  • amount_debit – this is the amount that the user will get deducted from their regional bank account.
  • amount_credit – this is the amount that the user will receive in their fiat bank account.

Response:

interface CashoutEstimate {
  // Amount that the user will get deducted from their regional
  // bank account, according to the 'amount_credit' value.
  amount_debit: Amount;
  // Amount that the user will receive in their fiat
  // bank account, according to 'amount_debit'.
  amount_credit: Amount;
}
200 Ok:
Response contains the calculated values
400 Bad request:
Both parameters have been provided and the calculation is not correct, or none of them has been provided.
GET /config

Response:

interface Config {
  // Name of this API, always "circuit".
  name: string;
  // API version in the form $n:$n:$n
  version: string;
  // Contains ratios and fees related to buying
  // and selling the circuit currency.
  ratios_and_fees: RatiosAndFees;
  // Fiat currency.  That is the currency in which
  // cash-out operations ultimately wire money.
  fiat_currency: string;
}
interface RatiosAndFees {
  // Exchange rate to buy the circuit currency from fiat.
  buy_at_ratio: LibeufinNumber;
  // Exchange rate to sell the circuit currency for fiat.
  sell_at_ratio: LibeufinNumber;
  // Fee to subtract after applying the buy ratio.
  buy_in_fee: LibeufinNumber;
  // Fee to subtract after applying the sell ratio.
  sell_out_fee: LibeufinNumber;
}

Example. Given a circuit currency CC, a fiat currency FC, a sell_at_ratio = 0.9 and sell_out_fee = 0.03, selling 10 CC would result in the following FC: (10 * 0.9) - 0.03 = 8.97 FC. On the other hand, given buy_at_ratio = 1.1 and buy_in_fee = 0.01, a user wanting to spend 10 FC to buy the CC would result in the following CC: (10 * 1.1) - 0.01 = 10.99 CC.

Note

the terms ‘sell out’ and ‘cashout’ may be used interchangeably.

GET /cashouts

Returns the list of all the (pending and confirmed) cash-out operations. Ordinary users can only use this endpoint to learn their own cash-out operations.

Request:

Query Parameters
  • accountOptional. Filters the request to only get the cash-out operations related to the account specified in this parameter. Ordinary users must use this option and pass their own username as the value.

Response:

interface Cashouts {
  // Every string represents a cash-out operation UUID.
  cashouts: string[];
}
200 OK:
At least one cash-out operation was found.
204 No Content:
No cash-out operations were found at the bank
403 Forbidden:
A ordinary user invoked this call either without the account parameter or by passing to it someone else’s username.
GET /cashouts/$cashoutId

Informs about the status of the $cashoutId operation. The request is available to the administrator and the original author.

Response:

CashoutStatusResponse

interface CashoutStatusResponse {

  status: CashoutStatus;
  // Amount debited to the circuit bank account.
  amount_debit: Amount;
  // Amount credited to the external bank account.
  amount_credit: Amount;
  // Transaction subject.
  subject: string;
  // Circuit bank account that created the cash-out.
  account: string;
  // Fiat bank account that will receive the cashed out amount.
  cashout_address: string;
  // Ratios and fees related to this cash-out at the time
  // when the operation was created.
  ratios_and_fees: RatiosAndFees;
  // Time when the cash-out was created.
  creation_time: number; // milliseconds since the Unix epoch
  // Time when the cash-out was confirmed via its TAN.
  // Missing or null, when the operation wasn't confirmed yet.
  confirmation_time?: number | null; // milliseconds since the Unix epoch
}
enum CashoutStatus {

  // The payment was initiated after a valid
  // TAN was received by the bank.
  CONFIRMED = "confirmed",

  // The cashout was created and now waits
  // for the TAN by the author.
  PENDING = "pending",
}

Response:

404 Not found:
The cashout operation was not found. That is also the case of $cashoutId being an aborted operation.

Access API

Every endpoint is served under /demobanks/default/access-api. See Taler Bank Access API. This API allows users to access their bank accounts and trigger Taler withdrawals.

Integration API

Every endpoint is served under /demobanks/default/integration-api. See Taler Bank Integration API. This API handles the communication with Taler wallets.

Taler Wire Gateway API

Served under /demobanks/default/taler-wire-gateway. Currently, only the admin/add-incoming endpoint is implemented. This endpoint allows testing, but the rest of this API does never involve the Sandbox.

EBICS API

POST /demobanks/default/ebics/subscribers

Allows (only) the admin user to associate a bank account to a EBICS subscriber. If the latter does not exist, it is created.

Request:

interface SubscriberRequest {

  // hostID
  hostID: string;

  // userID
  userID: string;

  // partnerID
  partnerID: string;

  // systemID, optional.
  systemID: string;

  // Label of the bank account to associate with
  // this subscriber.
  demobankAccountLabel: string;
}

Note

The following endpoints are not served under the /demobank/default segment.

Legacy API

This was the first API offered by Sandbox. It is used in some test cases. One is hosted at the Wallet repository; other MAY as well exist.

Except of the main EBICS handler located at “/ebicsweb”, all the EBICS calls have to authenticate the ‘admin’ user via the HTTP basic auth scheme.

EBICS Hosts

POST /admin/ebics/hosts

Create a new EBICS host.

Request:

interface EbicsHostRequest {

  // Ebics version.
  hostID: string;

  // Name of the host.
  ebicsVersion: string;
}
GET /admin/ebics/hosts

Shows the list of all the hosts in the system.

Response:

interface EbicsHostResponse {

  // shows the host IDs that are active in the system.
  // The Ebics version *is* missing, but it's still available
  // via the HEV message.
  ebicsHosts: string[];
}
POST /admin/ebics/hosts/$hostID/rotate-keys

Overwrite the bank’s Ebics keys with random ones. This is entirely meant for tests (as the Sandbox itself is) and no backup will be produced along this operation.

EBICS Subscribers

POST /admin/ebics/bank-accounts

Associates a new bank account to an existing subscriber.

Note

This call allows to create a bank account without any associated user profile! That makes the basic auth access to the financial data only possible for the admin.

Request:

interface BankAccountRequest {

   // Ebics subscriber
   subscriber: {
     userID: string;
     partnerID: string;
     systemID: string;
   };

   // IBAN
   iban: string;

   // BIC
   bic: string;

   // human name
   name: string;

   // bank account label
   label: string;
 }
GET /admin/ebics/subscribers

Shows the list of all the subscribers in the system.

Response:

interface SubscribersResponse {

  subscribers: Subscriber[]
}
interface Subscriber {

  // userID
  userID: string;

  // partnerID
  partnerID: string;

  // hostID
  hostID: string;

  // Label of the bank account
  // associated with this Ebics subscriber.
  demobankAccountLabel: string;
}
POST /admin/ebics/subscribers

Create a new EBICS subscriber without associating a bank account to it. This call is deprecated. Follow this page for updates over the EBICS management REST design.

Request:

interface SubscriberRequestDeprecated {

  // hostID
  hostID: string;

  // userID
  userID: string;

  // partnerID
  partnerID: string;

  // systemID, optional.
  systemID: string;

}

Bank accounts

The access to a particular bank account is granted either to the owner or to admin, via the HTTP basic auth scheme. A ‘owner’ is a registered customer, who is identified by a username. The registration of customers is offered via the Taler Bank Access API.

Note

The current version allows only one bank account per customer, where the bank account name (also called ‘label’) equals the owner’s username.

GET /admin/bank-accounts

Give summary of all the bank accounts. Only admin allowed.

Response:

interface AdminBankAccount {

  // IBAN
  iban: string;

  // BIC
  bic: string;

  // human name
  name: string;

  // bank account label
  label: string;
}
GET /admin/bank-accounts/$accountLabel

Give information about a bank account.

Response:

interface AdminBankAccountBalance {
  // Balance in the $currency:$amount format.
  balance: Amount;
  // IBAN of the bank account identified by $accountLabel
  iban: string;
  // BIC of the bank account identified by $accountLabel
  bic: string;
  // Mentions $accountLabel
  label: string;
}
POST /admin/bank-accounts/$accountLabel

Create bank account. Existing users without a bank account can request too.

Request: AdminBankAccount

Transactions

GET /admin/bank-accounts/$accountLabel/transactions

Inform about all the transactions of one bank account.

Response:

interface AdminTransactions {
  payments: AdminTransaction[];
}
interface AdminTransaction {

  // Label of the bank account involved in this payment.
  accountLabel: string;

  // Creditor IBAN
  creditorIban: string;

  // Debtor IBAN
  debtorIban: string;

  // UID given by one financial institute to this payment.
  // FIXME: clarify whether that can be also assigned by
  // the other party's institution.
  accountServicerReference: string;

  // ID of the Pain.001 that initiated this payment.
  paymentInformationId: string;

  // Unstructured remittance information.
  subject: string;

  // Date of the payment in the HTTP header format.
  date: string;

  // The number amount as a string.
  amount: string;

  // BIC of the creditor IBAN.
  creditorBic: string;

  // Legal name of the creditor.
  creditorName: string;

  // BIC of the debtor IBAN.
  debtorBic: string;

  // Legal name of the debtor.
  debtorName: string;

  // Payment's currency
  currency: string;

  // Have values 'credit' or 'debit' relative
  // to the requesting user.
  creditDebitIndicator: string;
}
POST /admin/bank-accounts/$accountLabel/generate-transactions

Generate one incoming and one outgoing transaction for the bank account identified by $accountLabel. Only admin allowed.

POST /admin/bank-accounts/$accountLabel/simulate-incoming-transaction

Book one incoming transaction for $accountLabel. The debtor (not required to be in the same bank) information is taken from the request. Only admin allowed.

Request:

interface AdminSimulateTransaction {

  // Debtor IBAN.
  debtorIban: string;

  // Debtor BIC.
  debtorBic: string;

  // Debtor name.
  debtorName: string;

  // Amount number (without currency) as a string.
  amount: string;

  // Payment subject.
  subject: string;
}
POST /admin/payments/camt

Return the last camt.053 document from the requesting account.

Request

interface CamtParams {

  // label of the bank account being queried.
  bankaccount: string;

  // The Camt type to return.  Only '53' is allowed
  // at this moment.
  type: number;
}

Response

The last Camt.053 document related to the bank account mentioned in the request body.

Errors

The JSON type coming along a non 2xx response is the following:

interface SandboxError {
  error: SandboxErrorDetail;
}
interface SandboxErrorDetail {

  // String enum classifying the error.
  type: ErrorType;

  // Human-readable error description.
  description: string;
}
enum ErrorType {
  /**
   * This error can be related to a business operation,
   * a non-existent object requested by the client, or
   * even when the bank itself fails.
   */
  SandboxError = "sandbox-error",

  /**
   * It is the error type thrown by helper functions
   * from the Util library.  Those are used by both
   * Sandbox and Nexus, therefore the actual meaning
   * must be carried by the error 'message' field.
   */
  UtilError = "util-error"
}