- FiatConnect CICO Provider API Specification "RFC"
- 0. Table of Contents
- 1. Introduction
- 2. Lifecycle of a Transfer
- 3. API Specification
- 3.1. Geolocation
- 3.2. Clock Synchronization
- 3.3. Authentication & Authorization
- 3.4. Formal Specification
- 3.4.1. Quote Endpoints
- 3.4.1.1.
POST /quote/in
- 3.4.1.1.1. Parameters
- 3.4.1.1.2. Responses
- 3.4.1.1.2.2. HTTP
400
- 3.4.1.1.3. Semantics
- 3.4.1.1.3.1. Success
- 3.4.1.1.3.2. Failure
- 3.4.1.1.3.2.1.
GeoNotSupported
- 3.4.1.1.3.2.2.
CryptoAmountTooLow
- 3.4.1.1.3.2.3.
CryptoAmountTooHigh
- 3.4.1.1.3.2.4.
FiatAmountTooLow
- 3.4.1.1.3.2.5.
FiatAmountTooHigh
- 3.4.1.1.3.2.6.
CryptoNotSupported
- 3.4.1.1.3.2.7.
FiatNotSupported
- 3.4.1.1.3.2.8.
InvalidParameters
- 3.4.1.2.
POST /quote/out
- 3.4.1.2.1. Parameters
- 3.4.1.2.2. Responses
- 3.4.1.2.3. Semantics
- 3.4.1.2.3.1. Success
- 3.4.1.2.3.2. Failure
- 3.4.1.2.3.2.1.
GeoNotSupported
- 3.4.1.2.3.2.2.
CryptoAmountTooLow
- 3.4.1.2.3.2.3.
CryptoAmountTooHigh
- 3.4.1.2.3.2.4.
FiatAmountTooLow
- 3.4.1.2.3.2.5.
FiatAmountTooHigh
- 3.4.1.2.3.2.6.
CryptoNotSupported
- 3.4.1.2.3.2.7.
FiatNotSupported
- 3.4.1.2.3.2.8.
InvalidParameters
- 3.4.1.1.
- 3.4.2. KYC Endpoints
- 3.4.3. Fiat Account Endpoints
- 3.4.4. Transfer Endpoints
- 3.4.1. Quote Endpoints
- 4. Transfer State Machines
- 5. Webhooks
- 6. Amounts
- 7. AML Considerations
- 8. Sandbox Environment
- 8.5. Transfers
- 9. Definitions
- 10. References
Liquidity between fiat and cryptocurrency assets is a common point of friction for new and growing crypto networks. In particular, cashing in/out to/from crypto assets can be a difficult experience. Oftentimes, there is poor support for geographical regions, and fees and spreads may be excessively high. Additionally, liquidity providers (referred to here as cash-in/cash-out providers, or CICO providers) share no common interface for allowing clients to transfer funds between crypto and fiat. In many cases, CICO providers only provide transfers through their own native (typically web-based) interface. In the few cases where CICO providers do offer an API for clients to integrate against, there is no uniformity in interface design across providers. Because each provider's API is unique, native CICO integrations within clients require completely bespoke integrations. Furthermore, since individual CICO providers may only offer limited geographical support for transfers, a client may have to integrate with many different providers in order to offer acceptably broad coverage to its users, which requires considerable implementation effort. In many cases, this level of implementation effort is not feasible, which can lead to clients which offer poor regional CICO availability. This may lead to users seeking out a variety of different clients in order to access broad regional support. Not only is this a poor user experience, but it is a detriment to both client developers and the network which those clients and CICO providers service. Clients unable to devote considerable engineering effort towards CICO provider integration lose out, and network TVL is hamstrung due to poor user experience.
FiatConnect is a proposed solution to these issues. FiatConnect provides a standardized server-side API specification for CICO providers to provide cash-in/cash-out functionality to clients. In addition to providing a standardized API specification, the FiatConnect ecosystem also offers a set of client-side tooling through the FiatConnect SDK which makes it seamless for client developers to integrate against any CICO provider who operates a FiatConnect API. By integrating with the FiatConnect SDK, clients will be able to implement a single native flow that can be re-used to perform transfers with any provider operating a FiatConnect API; supporting new providers in the client merely requires a small amount of static configuration. We believe that the FiatConnect specification will provide benefits to a multitude of parties:
- Client Developers: Client developers will not have to exert any specific implementation effort in order to integrate with any particular CICO provider. After implementing a single native flow with the FiatConnect SDK, supporting new providers comes for free.
- CICO Providers: CICO providers, particularly smaller ones, face a challenge in gaining initial adoption; they often must rely on the client to spend engineering effort creating an integration. With FiatConnect, this dependency is removed. Once a provider implements a FiatConnect API, they can immediately start providing crypto/fiat liquidity to any client, at no cost to the client developer. Client developers are thus much more likely to integrate with FiatConnect-compliant providers, making FiatConnect a compelling option for providers in order to drive adoption.
- End User: Since FiatConnect will reduce the total amount of engineering work required to integrate providers in clients, end users will have access to a much broader selection of providers than they previously did. Furthermore, with the time saved by wallet/dapp developers by integrating with FiatConnect-compatible providers, they will have more bandwidth to innovate on their core products, further increasing value for the end user.
This document offers a specification for the FiatConnect API for CICO providers; it discusses the client-side FiatConnect SDK only when necessary, and only as it concerns the FiatConnect API design.
This proposal has been designed without multi-chain support in mind; we assume all blockchain-related concepts (e.g., tokens, addresses) exist within a single predetermined chain, agreed upon by all parties. It does not matter so much what this chain is, just that all parties (clients and servers) are in agreement. If a provider wants to support multiple chains, they would have to stand up multiple APIs, with identical interfaces. Likewise, clients would have to maintain per-chain instances of the FiatConnect SDK (discussed later) in order to support transfers from multiple chains.
This specification is explicitly designed for the Celo network. As such, the document will occasionally reference Celo-specific entities; in particular, specific cryptocurrencies that are native to the Celo network.
The value of this specification extends well beyond the Celo network however; it is likely extensible by providing various parts of the specification with
an e.g. chainId
parameter. In the interest of keeping this document as simple as possible, and to avoid introducing premature complexity, we assume
that clients and servers referenced in this document are operating against the Celo network.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.
We first provide a generalized and somewhat informal model of a "transfer" operation, which will inform the FiatConnect API specification later in this document. The model should be general enough to support a wide range of CICO provider's requirements. This transfer lifecycle is based on the author's own experience as a client developer for Valora on the Celo network; it should certainly be reviewed by CICO providers and amended if there are any shortcomings.
For a single provider, we model the lifecycle of a transfer (either in or out) as a number of discrete steps:
-
- Quote: The client requests a quote for a transfer with the user's desired parameters.
-
- KYC: For regulatory and compliance reasons, the user must provide data used for KYC verification purposes to the provider.
- 2.1. Monitor KYC: KYC verification is assumed to be asynchronous; the client should be able to monitor its status.
-
- Fiat Account: The user must be able to provide a source of fiat funds for transfers in, or a destination for transfers out.
-
- Create Transfer: Once a user has provided KYC information and has an appropriate fiat account on file, the client initiates a transfer.
- 4.1. Monitor Transfer: Transfers are assumed to be asynchronous; the client should be able to monitor its status.
Before a transfer is ever initiated, the client should be able to request a quote for a potential transfer based off of the user's desired transfer parameters. We expect that quote generation will depend on the following parameters:
fiatType
: The type of fiat currency used for the transfer.cryptoType
: The type of cryptocurrency used for the transfer.fiatAmount
/cryptoAmount
: The amount of either fiat or cryptocurrency selected for the quote.region
: The user's geographical region.
We will explore the precise syntax and semantics of each of these parameters later. If a provider is unable to service a transfer given specific quote parameters for whatever reason, the provider should return an error code that details why the quote failed. In this way, a client may use the quote endpoints as an initial "gate" to determine whether or not a provider is supported for a particular transfer intent.
For regulatory and compliance reasons, providers may require that users complete a KYC verification process before initiating a transfer. We expect that different providers will have varying KYC requirements depending on a client's geographical region. The FiatConnect specification must not make any assumptions about the particular shape of KYC data that providers require. Rather, the specification should be extensible so as to allow providers to communicate to the client what sort of KYC data is required. The FiatConnect specification allows for a list of standardized KYC schemas from which a provider can select those that it accepts. If a provider's KYC needs are unmet by the specification's recognized schemas, the provider may request that the specification be extended to support a new schema, though a close eye should be kept on the reusability of this schema by other potential providers.
KYC validation is not expected to occur before every transfer. Rather, we expect that KYC validation with a provider will be an infrequent occurrence. A user may have to submit KYC details for a particular region before initiating a transfer there, but subsequent transfers in that region will likely reuse the previous KYC. One might expect that these details become "stale" after a time and will need to be resubmitted by the end user before initiating further transfers in their region. Ultimately, what a provider decides to do with KYC data once it is submitted is an implementation detail and not strictly enforced by the FiatConnect API specification, except for requirements on communicating KYC verification statuses to the client.
Once a client submits a user's KYC details to a provider, we expect that most providers will complete verification quickly and automatically, however the verification process is assumed to be fundamentally asynchronous. The server must provide a means for the client to monitor the status of an ongoing KYC verification, and query the status of previously submitted verifications. The FiatConnect standard specifies a list of common KYC statuses with fixed semantics that a server can communicate to the client. The standard requires that providers offer two means of monitoring KYC verification:
- Via typical endpoints that can be polled by the client
- Via webhooks that can be configured by the client
We will discuss these two options in more depth later in this document.
A user needs to be able to store details about a source or destination for fiat funds with the provider. The FiatConnect specification refers to these as Fiat Accounts, but these can commonly be thought of as, e.g., bank accounts, credit cards, etc. A fiat account refers to any user-owned account that supports withdrawals and deposits of fiat-denominated funds. Similar to KYC details, different providers may support different types of fiat accounts, and require different schemas depending on the user's geographical region. The FiatConnect specification introduces two distinct concepts to account for this; that of a Fiat Account Type, and a Fiat Account Schema. A Fiat Account Type refers to the high-level type of fiat account, e.g., bank account, credit card, debit card, etc. A Fiat Account Schema refers to the precise shape that account data should take when being communicated from client to server; each schema has an associated type. This is a subtle, yet important distinction. As mentioned in section 2.1, providers may use the type of a fiat account for determining whether a transfer is allowed. Different providers, however, may have different requirements on how account data is communicated, even for the same account type, depending on geographical region. Like with KYC schemas, the FiatConnect standard does not make assumptions about fiat account types and schemas; these are designed to be extensible by the community.
A CICO provider must provide functionality for a client to store fiat account details with them; these details must be sufficient in order for the provider to initiate a deposit/withdrawal on the user's behalf. Providers must also allow clients to retrieve and delete stored accounts. Providers may not allow users to submit fiat account details before undergoing KYC verification for the current region, if KYC verification is required. Since providers may have restrictions on what fiat account types are allowed for transfers in a particular region, providers must also provide a means for clients to know what these account types are.
Once a user has completed KYC and has an appropriate fiat account type on file with the provider, the client may initiate a transfer. When creating a transfer, the client must be able to reference a fiat account on store with the provider to use as a source or destination for fiat funds. It is extremely important that transfer requests are idempotent; if a transfer request/server response is lost in transit, the client should be able to safely re-try the request without risk of initiating a duplicate transfer. As such, the FiatConnect specification has a strict requirement that providers support idempotency keys for transfer requests. This is discussed more in-depth later in this document.
Transfers, like KYC verifications, are assumed to be fundamentally asynchronous. We do not expect that all transfers complete quickly (as we do for KYC verifications), which makes it critical for clients to be able to monitor transfer status over time. The server must provide a means for a client to retrieve the status of a transfer once it has been submitted. Much like monitoring KYC statuses, the FiatConnect standard requires that providers offer two means of transfer status monitoring:
- Via typical endpoints that can be polled by the client
- Via webhooks that can be configured by the client
These will be discussed in more depth later in this document.
This section presents the formal specification of the FiatConnect API, including required semantics.
Throughout the specification, many references are made to "geo", "geographical location", "region", etc. These are all meant to refer to the actual geographical location of the client. Having access to geolocation information about an end-user is important, since regulatory constraints may dictate that providers implement different requirements on requests from different geographical regions.
When requesting a quote, a client will explicitly provide its location through request parameters. The server must respond to this request assuming that the client is being honest about its location. We expect that a server's actual source of truth about an end-user's location will come from KYC verification.
As an example, a client using a VPN should still be able to interact with FiatConnect APIs; when requesting a quote, this client would provide their actual country, and not the one implied by their request IP. If their self-reported location is allowed for transfers, and their actual location (as collected through KYC) matches this, their KYC will be accepted and they will be allowed to initiate a transfer. In the case where a user submits KYC details for a geolocation that is not supported by a provider, they will never be able to initiate a transfer, regardless of what location they submit to the quote endpoint, since the KYC verification should be denied.
Various parts of the FiatConnect specification rely on the client and server synchronizing their clocks in order to agree on a common time. In particular, the authentication scheme relies on clock synchronization to choose proper issuance and expiration times for session cookies, and the quote endpoints return timestamps representing the expiry of the requested quote, which requires client-server clock synchronization in order to be interpreted correctly.
Rather than having bespoke clock synchronization solutions for each of these cases, the FiatConnect specification requires API implementors to
include a single /clock
endpoint, which simply returns the server's current timestamp. Servers MUST implement this endpoint. With this information,
the client can determine the offset between their local time and server time, and synchronize on clocks (to an acceptable approximation).
The exact method by which clients determine the clock offset is an implementation detail left to the client. (For example, a relatively
easy and inexpensive method that incorporates round-trip delay is the approach taken by the
Network Time Protocol.)
This endpoint is responsible for reporting the current server time to the client.
This endpoint does not require any parameters.
On success, the server MUST return an HTTP 200
, with the following response body:
{
time: `string`
}
The semantics of this endpoint are extremely simple; the server MUST always return a successful response containing the current server time.
On success, the server MUST return a 200 response. The time
field in the response MUST be the server's current time, formatted as an ISO 8601 datetime string.
The FiatConnect API specifies two types of authentication; the first authenticates the user, and is required for all requests; the second authenticates the client, and is optionally required, depending on whether or not it has been configured by the client with the provider.
FiatConnect-compliant APIs are required to use the Sign-In With Ethereum standard, otherwise known as EIP-4361 in order to identify users and confirm ownership of a Celo blockchain address. At a high level, the Sign-In With Ethereum standard requires the client to use a user's private key to sign a plaintext message containing a number of standardized fields, and send that signed message to the server to prove account ownership. FiatConnect APIs MUST use the SIWE standard to create authenticated sessions for the user, and return session cookies to the client. More detail on exactly how this should be structured is provided below.
SIWE has rich tooling and support across multiple languages; see here for more details, example implementations, and libraries for many popular languages.
At a high level, the client will craft a SIWE-compliant message and sign it with the user's private key. This message will contain various parameters relating
to the origin URL which they're requesting to be authorized against, desired session length, etc. Once the client has crafted and signed the message, the client will send
the signed message to a FiatConnect API at a POST /auth/login
endpoint. The server will validate the signature, and check that all the included fields in the signed message are valid.
If everything checks out, the server will create a session for the user according to the details in the signed message. Once the server has created the session,
it responds to the client with a 200
, and returns session cookies that the client can use in subsequent requests to privileged endpoints on behalf of the signed-in address.
Within the FiatConnect specification, all references to SIWE-compliant messages refer to plaintext messages adhering exactly to the SIWE message format, and additionally to the constraints described below.
${domain} wants you to sign in with your Ethereum account:
${address}
URI: ${uri}
Version: 1
Chain ID: 42220
Nonce: ${nonce}
Issued At: ${issued-at}
Expiration Time: ${expiration-time}
Each field in this template is required to be filled in by the client, and has a unique meaning. As described later, servers MUST reject authorization attempts if any fields are not as expected. The meaning of each of these fields is enumerated below:
The domain
field MUST correspond to the RFC 3986 authority of the API that the client is requesting to be authorized for, e.g. example.com
.
The address
field MUST correspond to the Celo blockchain address that the user is attempting to login/authorize themself as. For externally-owned accounts, this
must conform to the mixed-case checksum encoding specified in EIP-55.
The uri
field MUST correspond to the origin URL of the API to authorize for, with the /auth/login
path appended, e.g. https://example.com/auth/login
.
The nonce
field MUST be a unique, at least 8 characters long, alpha-numeric string not previously seen for any unexpired login request from the given client.
The issued-at
field MUST be the ISO 8601 datetime string of the time at which the client generated the message.
The expiration-time
field MUST be an ISO 8601 datetime string that specifies when this message will no longer valid, and when a session created with this message
will expire. This field MUST NOT be more than four hours (14400 seconds) later than the issued-at
field.
Note that in cases where a client is attempting to authorize themselves as the owner of a contract-owned account (rather than an externally-owned account), the address
field MUST correspond to the address of the on-chain contract account, rather than the externally-owned account (EOA) address. The message itself should be signed in accordance
with the EIP-1271 standard, in such a way that the contract account can validate the signature and message hash with its
implementation of the isValidSignature
method as outlined in the EIP-1271 standard. If desired, CICO providers MAY support authenticating as a contract-owned account,
but it is not required.
Once the client generates a SIWE-compliant plaintext message, it signs it with the user's externally-owned account private key according to EIP-191
personal_sign
format. The client will then send the message and signature to the server at a POST /auth/login
endpoint, described below. If the server
accepts the signed message as valid, it will respond with a session cookie linked that identifies the user's authenticated session within the server. The server
MUST set the SameSite policy on the session cookie to prevent the client
from sending the cookie to other hosts. Ultimately, it is the client's responsibility to manage session cookies between different FiatConnect APIs.
If a client sends a session cookie when calling the POST /auth/login
endpoint (either to refresh their current, valid session, or to generate a new session if their current one is
expired), the server MUST ignore the provided cookie. If the authentication request is valid, the server MUST set a new value on the cookie referencing the client's new, valid session,
and return it to the client.
The POST /auth/login
endpoint is responsible for verifying signed messages sent by clients, creating and authenticating a user's session, and returning session cookies.
Note that the Sign-In With Ethereum standard, and FiatConnect, support authorization for externally owned accounts (EOAs) and contract-owned accounts; The POST /auth/login
endpoint MUST honor login requests for EOAs, and MAY support smart contract-owned accounts as well, as described below.
The request body must contain the following fields:
message
: {string
} [REQUIRED]- The plaintext SIWE message
signature
: {string
} [REQUIRED]
The message
field should be the plaintext message, while the signature should be an string containing the typical secp256k1 signature fields: r
, s
, and v
.
This signature
string should be a hex string, prefixed with 0x
. The hex data should be 65 bytes long (130 hex characters, excluding the leading 0x
). The first
32 bytes (64 hex characters) represent r
, the next 32 bytes (64 hex characters) represent s
, and the final byte (2 hex characters) represent v
. This is the "canonical"
form of the signature as referenced in EIP-2098, rather than the "compact" form.
On success, the server MUST return an HTTP 200
, update the state of the user's session to be authenticated, and return a session cookie.
On failure, the server MUST return an HTTP 401
, with the following response body.
{
error: `ErrorEnum`,
}
This endpoint performs validation on a signed SIWE-compliant message. In addition to checking that the signature itself is valid and corresponds to the included
address, it also performs various safety and security checks on the contents of the message. If all checks pass, the server updates the client's session to be authenticated,
and returns a 200
.
On success, the server MUST respond with a 200
status code. The server MUST only respond successfully after performing the following checks:
-
A server MUST verify that the plaintext
message
adheres to the SIWE grammar/syntax (refer to theABNF Message Format
section within EIP-4361). If it does not, the server MUST return a 401 (InvalidParameters
) -
A server MUST determine whether the address being signed in as corresponds to an EOA or contract account. If a server determines that the address corresponds to a contract-owned account and does not support contract-owned account login, it must return a 401 (
ContractLoginNotSupported
).-
If the address to sign in as corresponds to an EOA, the server MUST verify that the
signature
field was signed by the address attempting to be signed is and is a valid signature for themessage
included in the request. -
If the address to sign in as corresponds to a contract-owned account and the server supports contract-owned account login, the server MUST verify the validity of the included
signature
according to the EIP-1271 standard.
-
-
A server MUST verify that the
nonce
field does not correspond to a nonce that has already been used to authenticate an ongoing, valid session. -
A server MUST verify that the
domain
anduri
fields correspond exactly to the values expected for the server's hostname/origin. -
A server MUST verify that the
Version
andChain ID
lines in the signed message are exactly1
and42220
, respectively. -
If the
issued-at
field in the signed message is after the server's current timestamp, or if theissued-at
field is after theexpiration-time
field, the server MUST fail the request. If the server's current timestamp is past theexpiration-time
field, the server MUST fail the request. If theexpiration-time
field is greater than four hours (14400 seconds) ahead of theissued-at
field, the server MUST fail the request.
If all fields are acceptable, and the message is formatted correctly, the server SHOULD authenticate the user's session, and return an HTTP 200
.
The server SHOULD attach the fields from the signed message to the session internally, in order to access data about the user and their session in other endpoints.
Once a server authenticates a session using a particular nonce, the server MUST NOT allow any other sessions to be created using the same nonce and address, until
expiration-time
of the session created using that nonce has been reached. The implementation of this is up to the server, but will require storing some global
state about nonces in order to check if a client is attempting to sign-in using a nonce that is temporarily forbidden. Careful consideration should be taken to make
sure that nonce management works correctly when scaling server resources.
On failure, the server MUST return an HTTP 401
error, along with an error code that details exactly why the request failed.
If the signature is invalid or doesn't match the included address
field, the server MUST return an InvalidSignature
error. Note that the address used
to sign the message may not actually match the address being authenticated in the case where the client is authenticating as a contract-owned account. In
such cases, InvalidSignature
MUST NOT be returned when the signature is valid, and the address used to sign the message is considered valid by the contract
referenced in the address
field.
If the request body is missing, malformed, or fails to pass any of the required checks, the server MUST respond with an InvalidParameters
error. If another
error type is more descriptive of the actual issue, that error should be used instead.
If the server determines that a client is trying to log in as a contract-owned account but the server does not support contract-owned account logins, the
server MUST respond with a ContractLoginNotSupported
error.
If the nonce included in the message is already attached to a valid session, the server MUST respond with a NonceInUse
error.
If the issued-at
field is after the server's current timestamp, the server MUST respond with an IssuedTooEarly
error.
If the expiration-time
field is more than four hours into the future, the server MUST respond with an ExpirationTooLong
error.
Once a client has established a session with a server, they can use that session (by sending the session cookie to the server) to access privileged endpoints throughout the API. Certain endpoints impose different requirements on the in-use session. There are two levels of endpoints, with respect to their authentication requirements. These levels are explained below.
The first group of endpoints are those that do not require the user to have a logged-in session. These are:
POST /quote/in
POST /quote/out
POST /auth/login
GET /clock
Since it may be a painful user experience to have a user log in to a provider just to get a quote, authentication is not required for these endpoints. Of course,
clients may still access these endpoints when logged in, but a logged-in session (or any session at all) is not required to access these endpoints. POST /auth/login
does not require a user to already be logged in, as described above.
The second, and largest set of endpoints, are the ones that require a user to be logged in. These endpoints are the ones that deal with reading and writing user-specific data.
POST /kyc/:kycSchema
GET /kyc/:kycSchema/status
DELETE /kyc/:kycSchema
POST /accounts
GET /accounts
DELETE /accounts/:fiatAccountId
GET /transfer/:transferId/status
POST /transfer/in
POST /transfer/out
For these endpoints, a user needs to be logged in with an authenticated session. This session MUST have an expiration time of no greater than four hours (the maximum allowed by FiatConnect).
If a client attempts to access any of these endpoints without a session, the server MUST return a 401
status code with an Unauthorized
error.
If a client tries to access any of these endpoints with an expired session, the server MUST respond with an HTTP 401
status code with a SessionExpired
error.
In addition to the SIWE-based auth used to authenticate users and create sessions, the FiatConnect specification also supports a more traditional authentication standard, used to identify specific clients with requests. Recall the webhook-based status monitoring mentioned earlier in this document. In order to support status monitoring via webhooks, individual clients will need to be able to register a URL pointing to an API able to handle webhook updates from the server. Once a client has registered a webhook URL with the provider, the client needs a way to identify itself to the server. To uniquely identify clients in order to know where to send webhook-based status updates, a server may allow clients to register an API key. The exact mechanism by which servers allocate API keys to clients and allow them to register webhook URLs is out of scope of this document.
Once a client has registered an API key with a provider and associated it with a webhook URL, a client MAY provide it to a server through an Authorization
header,
using the Bearer
authorization scheme. For example: Authorization: Bearer <key>
, where <key>
is the client's API key.
A server MUST support API token authentication, and MAY require that clients include an API key in each request. If a server requires
that clients include an API key in each request, it MUST respond to the client with an HTTP 400
error if the API key is missing from the request.
Regardless of whether or not a server requires an API key on every request, it MUST return an HTTP 401
error if an API key is
provided but does not correspond to any registered client; likewise it MUST return an HTTP 400
error if the API key is poorly formed.
API token authentication is completely separate from SIWE-based auth. The privileged endpoints listed earlier always require a user to have a valid session via SIWE authentication. API token authentication MAY be required on all endpoints, or optional. Regardless of whether or not the client provides an API token, if the client attempts to access a privileged endpoint without a valid session, the server MUST reject their request.
All servers implementing a FiatConnect-compliant API MUST use HTTPS, and MUST NOT support unsecured HTTP requests.
This section details the precise syntax and semantics of all the endpoints required by the FiatConnect specification, besides those devoted to authentication. Endpoints are logically grouped, and roughly presented in order of dependency. The logical groupings are as follows:
- Quote Endpoints
- KYC Endpoints
- Fiat Account Endpoints
- Transfer Endpoints
This section references a number of definitions/enums; for an exhaustive list of these, please see later in the document.
We assume that the lifecycle of a transfer begins with an end-user requesting a quote. We predict that the typical transfer experience will involve a user providing their desired transfer parameters (crypto type, fiat type, amount), and the client then proceeding to show them a list of supported providers.
We assume that different quotes represent transfers that may have differing requirements regarding KYC and fiat accounts. To support this, the quote endpoints must return information about KYC and fiat account schemas that are required in order to actually perform a transaction for the requested quote.
Quotes may vary depending on geo, and certain specific transfers may be unavailable in certain regions entirely. As such, we assume that the client will interpret a 200 response from this endpoint as verification that the provider is supported for the particular transfer parameters, and a non-200 as verification that it is not.
Some non-200s may be recovered from by modifying the transfer parameters; others may not be, e.g., for those caused by geos where transfers are completely unsupported.
The POST /quote/in
endpoint is used to retrieve quotes used for transfers in to crypto from fiat currencies. In addition to returning quote information, it also
returns the permissable types of KYC that a user must have on file to initiate the corresponding transfer, as well as the fiat account types that are allowed to be
used for the transfer.
Transfers in are notably different from transfers out, since they require the user to send fiat funds to a provider in exchange for crypto. Certain providers, or certain types of fiat accounts may have restrictions on how these fiat funds are transferred from the user to the provider. While some types of fiat accounts may support debiting the user's fiat account (i.e., initiating the transfer of fiat funds on behalf of the user), other types of fiat accounts may not support this behavior, and require the user to initiate the transfer of fiat funds from their account to the provider's. For example, in the US, transfers of fiat from bank accounts - ACH transfers - may be initiated by the account owner themselves, or initiated by a third party with their bank account number.
For certain types of fiat accounts, both methods of fiat transfer may be possible, either initiated by the provider or the user. For such accounts, providers must choose one of these
methods of transfer when returning the /quote/in
response. Providers may specify that a Fiat Account Schema requires user action by returning an optional userActionType
field in the
quote response. If userActionType
is null or undefined, there MUST be no user action required in order to initaite a transfer in using an account of that kind.
fiatType
: {FiatTypeEnum
} [REQUIRED]- The desired fiat type to use for a transfer in quote; selected from a predefined list of fiat types supported by FiatConnect.
cryptoType
: {CryptoTypeEnum
} [REQUIRED]- The desired crypto type to use for a transfer in quote; selected from a predefined list of crypto types supported by FiatConnect.
fiatAmount
: {string
}- String-ified numerical amount (E.g.
"5.00"
) of the selected fiat type to use for this transfer in quote; if provided, the returned quote will be denominated in the type of crypto specified for the quote.
- String-ified numerical amount (E.g.
cryptoAmount
: {string
}- String-ified numerical amount (E.g.
"1.0"
) of the selected crypto type to use for this transfer in quote; if provided, the returned quote will be denominated in the type of fiat specified for the quote.
- String-ified numerical amount (E.g.
country
: {string
} [REQUIRED]- An ISO 3166-1 alpha-2 country code representing the country where the quote should be requested for.
region
: {string
}- An optional ISO 3166-2 subdivision code representing a region within the provided country.
address
: {string
} [REQUIRED]- An EIP-55 formatted address, representing the Celo address of the user to get the quote for. For contract-owned accounts, this should be the address of the contract itself.
preview
: {boolean
}- An optional boolean flag, that if included, will cause the server not to generate a
quoteId
for the request.
- An optional boolean flag, that if included, will cause the server not to generate a
On success, the server MUST return an HTTP 200
, with the following response body:
{
quote: {
fiatType: `FiatTypeEnum`,
cryptoType: `CryptoTypeEnum`,
fiatAmount: `string`,
cryptoAmount: `string`,
fee?: `string`,
feeType?: `FeeTypeEnum`,
feeFrequency?: `FeeFrequencyEnum`,
quoteId?: `string`,
guaranteedUntil: `string`
transferType: `TransferTypeEnum.TransferIn`
},
kyc: {
kycRequired: `boolean`,
kycSchemas: {
kycSchema: `KycSchemaEnum`,
allowedValues: {
[string]: `string[]`
}
}[]
},
fiatAccount: {
[FiatAccountTypeEnum]: {
fiatAccountSchemas: {
fiatAccountSchema: `FiatAccountSchemaEnum`,
userActionType?: `TransferInUserActionDetailsEnum`
allowedValues: {
[string]: `string[]`
}
}[],
settlementTimeLowerBound?: `string`,
settlementTimeUpperBound?: `string`
}
}
}
On failure, the server MUST return an HTTP 400
, with a response body as follows. Refer to the ErrorEnum
definition for all possible error values.
{
error: `ErrorEnum`,
minimumFiatAmount?: `string`,
maximumFiatAmount?: `string`,
minimumCryptoAmount?: `string`,
maximumCryptoAmount?: `string`
}
All transfer in quotes require the fiatType
, cryptoType
, and exactly one of fiatAmount
or cryptoAmount
. country
is required, and region
is optional.
preview
is also an optional parameter. If these requirements are not met, the server MUST return an HTTP 400
error. If the server responds with an HTTP 200
, the provider MUST support
a transfer in for the requested details. If the requested quote is not supported, the server MUST return an HTTP 400
error.
A successful response indicates that the provider is able to perform a transfer for the requested quote in the specified country/region.
If fiatAmount
is provided, the quote.cryptoAmount
field returned in the success body MUST correspond to the amount of crypto the user should expect to
receive by providing fiatAmount
worth of the fiat currency. If cryptoAmount
is provided, the quote.fiatAmount
field MUST correspond to the amount
of fiat currency required in order to receive the requested amount of crypto.
The quote.fiatType
, quote.cryptoType
, quote.fiatAmount
, and quote.cryptoAmount
fields in the response body MUST correspond to the request body provided to the endpoint.
The quote.guaranteedUntil
field represents the time that the quote is guaranteed until, as an ISO 8601 datetime string. The quote.quoteId
field
is a globally unique identifier for the quote, and is used by the client to initiate a transfer with the parameters associated with the quote including
conversion rate, fee, amount, crypto type, fiat type, etc. A server MUST provide quote.guaranteedUntil
and quote.quoteId
, and MUST honor the
provided conversion rate and fee if the client initiates a transfer with the quoteId
before the time quote.guaranteedUntil
.
The quote.fee
field is an optional return value, used to represent an optional fixed fee as a string-ified numerical
amount (e.g. "1.0"
) for the transfer. Note that fee
is meant for end-user informational purposes only, and fee
MUST be included in the exchange rate (ratio of cryptoAmount
to fiatAmount
) already in the provided quote. For
example, if fiatAmount
is 15 in the quote response for some transfer in of USD for cUSD, and cryptoAmount
is 12,
and fee
is 3, the end user MUST receive 12 cUSD for 15 USD (not 9, which would double-count the fee).
For transfers in, fee
is assumed to be denominated in the selected fiatType
. If fee
is provided, the server MAY
return quote.feeType
and/or quote.feeFrequency
. feeType
represents the type of fee; e.g, if it's for KYC or a
fixed platform fee. feeFrequency
represents the frequency at which the fee is required; e.g., one-time, or on each
transfer.
The quote.settlementTimeLowerBound
and quote.settlementTimeUpperBound
fields are optional return values, representing the lower and upper bounds for transaction settlement time using
a particular FiatAccountType
respectively. A server MAY include these in the response. If included, these MUST be strings representing time deltas in number of seconds. If included, a server
SHOULD try to honor the advertised settlement time bounds when performing a related transfer, but it is not required.
In addition to returning quote data, a successful response must also return information about which KYC schemas are acceptable/required in order to initiate a transfer
with the given quote parameters. If KYC is required, the server MUST set kyc.kycRequired
to true
in the response body. Likewise, if no KYC is required, this value MUST be
false
. If kyc.kycRequired
is true
, the kyc.kycSchemas
field MUST be present. kyc.kycSchemas
is a list of objects. Each object corresponds to a single acceptable KYC schema.
The kycSchema
field within this object represents the type of KYC schema that can be used. The allowedValues
object is an optional mapping from any number of keys in the selected
KYC schema to values that are allowed for that key. For example, if a server wants to limit the selection for certain fields on the schema, allowedValues
can contain lists of selectable
values for those fields. On the client-side, this could be used to render a list of options, for example.
Finally, a successful response must also return information about what fiat account types are allowed to be used for the transfer, and what schemas are allowed to communicate those account details.
If the quote request contains a value of true
for the preview
field, the returned quote SHOULD represent a preview; the provider has no obligation to persist
any information relating to the generated quote on the server. If a quote preview is requested, the provider MUST NOT populate the quoteId
field in the response
body. This prevents clients from actually attempting to use this quote for a transfer. Besides the exclusion of the quoteId
field, all other aspects of a preview
quote response MUST be identical to those of the response had the client requested a non-preview quote.
On success, the server MUST return a mapping from fiat account types to lists of schemas that the client may use to add a new account of that
type. This is expected to vary by geographical region as well as quote details provided by the request body.
Each fiatAccount[FiatAccountTypeEnum]
MUST correspond to a fiat account type that is allowed to be used for the requested quote.
fiatAccount[FiatAccountTypeEnum].fiatAccountSchemas
is a list of objects, where each object represents a fiat account schema that can be used to communicate data
about the corresponding fiat account type. Each object MUST contain a fiatAccountSchema
field representing the actual fiat account schema that can be used, an
optional allowedValues
field, and an optional userActionType
field. The allowedValues
object is an optional mapping from any number of keys in the selected
fiat account schema to values that are allowed for that key. This is identical in purpose and function to the allowedValues
field for KYC schemas, discussed earlier.
userActionType
, if present, denotes that this account schema, if used for the transfer, will require user action in order to transfer fiat funds from the user
to the provider. Different values of TransferInUserActionDetailEnum
denote different semantics with respect to what kind of action the user will have to perform
in order to send fiat funds to the provider. If a fiat account schema requiring user action is used to execute a transfer with a compatible quote, the semantics
of the /transfer/in
endpoint will differ, as discussed later.
On failure, this endpoint MUST return an HTTP 400
error, along with an error code that details exactly why the quote failed.
If a quote is not supported due to the provider not supporting the user's region at all, the server MUST return a GeoNotSupported
error.
If the user provides cryptoAmount
in the request body, and the value is too low, the server MUST return a CryptoAmountTooLow
error. The
server MAY also provide a minimumCryptoAmount
value with the response body.
If the user provides cryptoAmount
in the request body, and the value is too high, the server MUST return a CryptoAmountTooHigh
error. The
server MAY also provide a maximumCryptoAmount
value with the response body.
If the user provides fiatAmount
in the request body, and the value is too low, the server MUST return a FiatAmountTooLow
error. The
server MAY also provide a minimumFiatAmount
value with the response body.
If the user provides fiatAmount
in the request body, and the value is too high, the server MUST return a FiatAmountTooHigh
error. The
server MAY also provide a maximumFiatAmount
value with the response body.
If the provided cryptoType
is unsupported in the user's current region, but the server supports other transfers in their region,
the server MUST return a CryptoNotSupported
error.
If the provided fiatType
is unsupported in the user's current region, but the server supports other transfers in their region,
the server MUST return a FiatNotSupported
error.
If the request is missing any required parameters, or if the parameters are poorly formed, the server MUST respond
with an InvalidParameters
error.
The POST /quote/out
endpoint is used to retrieve quotes used for transfers out from crypto to fiat currencies.
fiatType
: {FiatTypeEnum
} [REQUIRED]- The desired fiat type to use for a transfer out quote; selected from a predefined list of fiat types supported by FiatConnect.
cryptoType
: {CryptoTypeEnum
} [REQUIRED]- The desired crypto type to use for a transfer out quote; selected from a predefined list of crypto types supported by FiatConnect.
fiatAmount
: {string
}- String-ified numerical amount (e.g.
"5.00"
) of the selected fiat type to use for this transfer out quote; if provided, the returned quote will be denominated in the type of crypto specified for the quote.
- String-ified numerical amount (e.g.
cryptoAmount
: {string
}- String-ified numerical amount (e.g.
"1.0"
) of the selected crypto type to use for this transfer out quote; if provided, the returned quote will be denominated in the type of fiat specified for the quote.
- String-ified numerical amount (e.g.
country
: {string
} [REQUIRED]- An ISO 3166-1 alpha-2 country code representing the country where the quote should be requested for.
region
: {string
}- An optional ISO 3166-2 subdivision code representing a region within the provided country.
address
: {string
} [REQUIRED]- An EIP-55 formatted address, representing the Celo address of the user to get the quote for. For contract-owned accounts, this should be the address of the contract itself.
preview
: {boolean
}- An optional boolean flag, that if included, will cause the server not to generate a
quoteId
for the request.
- An optional boolean flag, that if included, will cause the server not to generate a
On success, the server MUST return an HTTP 200
, with the following response body:
{
quote: {
fiatType: `FiatTypeEnum`,
cryptoType: `CryptoTypeEnum`,
fiatAmount: `string`,
cryptoAmount: `string`,
fee?: `string`,
feeType?: `FeeTypeEnum`,
feeFrequency?: `FeeFrequencyEnum`,
quoteId?: `string`,
guaranteedUntil: `string`,
transferType: `TransferTypeEnum.TransferOut`
},
kyc: {
kycRequired: `boolean`,
kycSchemas: {
kycSchema: `KycSchemaEnum`,
allowedValues: {
[string]: `string[]`
}
}[]
},
fiatAccount: {
[FiatAccountTypeEnum]: {
fiatAccountSchemas: {
fiatAccountSchema: `FiatAccountSchemaEnum`,
allowedValues: {
[string]: `string[]`
}
}[],
settlementTimeLowerBound?: `string`,
settlementTimeUpperBound?: `string`
}
}
}
On failure, the MUST return an HTTP 400
, with a response body as follows. Refer to the ErrorEnum
definition for all possible error values.
{
error: `ErrorEnum`,
minimumFiatAmount?: `string`,
maximumFiatAmount?: `string`,
minimumCryptoAmount?: `string`,
maximumCryptoAmount?: `string`
}
All transfer out quotes require the fiatType
, cryptoType
, and exactly one of fiatAmount
or cryptoAmount
. country
is required, and region
is optional.
preview
is also an optional parameter. If these requirements are not met, the server MUST return an HTTP 400
error. If the server responds with an HTTP 200
, the provider MUST support
a transfer out for the requested details. If the requested quote is not supported, the server MUST return an HTTP 400
error.
A successful response indicates that the provider is able to perform a transfer for the requested quote for the specified country/region.
If fiatAmount
is provided, the quote.cryptoAmount
field returned in the success body MUST correspond to the amount of crypto the user must provide
in order to receive fiatAmount
worth of the fiat currency. If cryptoAmount
is provided, the quote.fiatAmount
field MUST correspond to the amount
of fiat currency the user should expect to receive in exchange for cryptoAmount
worth of the cryptocurrency.
The quote.fiatType
, quote.cryptoType
, quote.fiatAmount
, and quote.cryptoAmount
fields in the response body MUST correspond to the request body provided to the endpoint.
The quote.guaranteedUntil
field represents the time that the quote is guaranteed until, as an ISO 8601 datetime string. The quote.quoteId
field
is a globally unique identifier for the quote, and is used by the client to initiate a transfer with the parameters associated with the quote including
conversion rate, fee, amount, crypto type, fiat type, etc. A server MUST provide quote.guaranteedUntil
and quote.quoteId
, and MUST honor the
provided conversion rate and fee if the client initiates a transfer with the quoteId
before the time quote.guaranteedUntil
.
The quote.fee
field is an optional return value, used to represent an optional fixed fee as a string-ified numerical
amount (e.g. "1.0"
) for the transfer. Note that fee
is meant for end-user informational purposes only, and fee
MUST be included in the exchange rate (ratio of fiatAmount
to cryptoAmount
) already in the provided quote. For
example, if cryptoAmount
is 15 in the quote response for some transfer out of cUSD for USD, and fiatAmount
is 12,
and fee
is 3, the end user MUST receive 12 USD for 15 cUSD (not 9, which would double-count the fee).
For transfers out, fee
is assumed to be denominated in the selected cryptoType
. If fee
is provided, the server MAY
return quote.feeType
and/or quote.feeFrequency
. feeType
represents the type of fee; e.g, if it's for KYC or a
fixed platform fee. feeFrequency
represents the frequency at which the fee is required; e.g., one-time, or on each
transfer.
The quote.settlementTimeLowerBound
and quote.settlementTimeUpperBound
fields are optional return values, representing the lower and upper bounds for transaction settlement time using
a particular FiatAccountType
respectively. A server MAY include these in the response. If included, these MUST be strings representing time deltas in number of seconds. If included, a server
SHOULD try to honor the advertised settlement time bounds when performing a related transfer, but it is not required.
In addition to returning quote data, a successful response must also return information about which KYC schemas are acceptable/required in order to initiate a transfer
with the given quote parameters. If KYC is required, the server MUST set kyc.kycRequired
to true
in the response body. Likewise, if no KYC is required, this value MUST be
false
. If kyc.kycRequired
is true
, the kyc.kycSchemas
field MUST be present. kyc.kycSchemas
is a list of objects. Each object corresponds to a single acceptable KYC schema.
The kycSchema
field within this object represents the type of KYC schema that can be used. The allowedValues
object is an optional mapping from any number of keys in the selected
KYC schema to values that are allowed for that key. For example, if a server wants to limit the selection for certain fields on the schema, allowedValues
can contain lists of selectable
values for those fields. On the client-side, this could be used to render a list of options, for example.
If the quote request contains a value of true
for the preview
field, the returned quote SHOULD represent a preview; the provider has no obligation to persist
any information relating to the generated quote on the server. If a quote preview is requested, the provider MUST NOT populate the quoteId
field in the response
body. This prevents clients from actually attempting to use this quote for a transfer. Besides the exclusion of the quoteId
field, all other aspects of a preview
quote response MUST be identical to those of the response had the client requested a non-preview quote.
Finally, a successful response must also return information about what fiat account types are allowed to be used for the transfer, and what schemas are allowed to communicate those account details.
On success, the server MUST return a mapping from fiat account types to lists of schemas that the client may use to add a new account of that
type. This is expected to vary by geographical region as well as quote details provided by the request body.
Each fiatAccount[FiatAccountTypeEnum]
MUST correspond to a fiat account type that is allowed to be used for the requested quote.
fiatAccount[FiatAccountTypeEnum].fiatAccountSchemas
is a list of objects, where each object represents a fiat account schema that can be used to communicate data
about the corresponding fiat account type. Each object MUST contain a fiatAccountSchema
field representing the actual fiat account schema that can be used, and an
optional allowedValues
field. The allowedValues
object is an optional mapping from any number of keys in the selected fiat account schema to values that are allowed for that key.
This is identical in purpose and function to the allowedValues
field for KYC schemas, discussed earlier.
On failure, this endpoint MUST return an HTTP 400
error, along with an error code that details exactly why the quote failed.
If a quote is not supported due to the provider not supporting the user's region at all, the server MUST return a GeoNotSupported
error.
If the user provides cryptoAmount
in the request body, and the value is too low, the server MUST return a CryptoAmountTooLow
error. The
server MAY also provide a minimumCryptoAmount
value with the response body.
If the user provides cryptoAmount
in the request body, and the value is too high, the server MUST return a CryptoAmountTooHigh
error. The
server MAY also provide a maximumCryptoAmount
value with the response body.
If the user provides fiatAmount
in the request body, and the value is too low, the server MUST return a FiatAmountTooLow
error. The
server MAY also provide a minimumFiatAmount
value with the response body.
If the user provides fiatAmount
in the request body, and the value is too high, the server MUST return a FiatAmountTooHigh
error. The
server MAY also provide a maximumFiatAmount
value with the response body.
If the provided cryptoType
is unsupported in the user's current region, but the server supports other transfers in their region,
the server MUST return a CryptoNotSupported
error.
If the provided fiatType
is unsupported in the user's current region, but the server supports other transfers in their region,
the server MUST return a FiatNotSupported
error.
If the request is missing any required parameters, or if the parameters are poorly formed, the server MUST respond
with an InvalidParameters
error.
A CICO provider may wish to require different KYC types for a single user across separate transfers for various reasons. For example, if a user initiates a transfer from the US, the provider may request a particular type of KYC for the transfer. Later on, if the user initiates a transfer from a different geo, the same provider may require a different type of KYC. The provider should store these KYC verifications to re-use later in case the user returns to a geo where they have already provided an appropriate KYC type. Negotiation of what KYC types are allowed for a particular transfer occurs entirely within the quote endpoints.
While validation is typically expected to be completed quickly and automatically, we assume that it is fundamentally an asynchronous process. As such, the FiatConnect specification must support monitoring the status of an ongoing validation. While we require that a CICO provider be able to maintain different types of KYC validations per-user, we require that they only maintain one record of each type for each user at a time. Otherwise, server-side implementation would become significantly more difficult, likely requiring an idempotency key to initiate new KYC verifications.
The POST /kyc/:kycSchema
endpoint allows a client to provide KYC data of a particular schema to the server for verification.
kycSchema
: {KycSchemaEnum
} [REQUIRED]- The KYC schema being used in the request body
The request body schema for this endpoint must match the KYC schema selected in the path parameter.
On success, the server MUST return an HTTP 200
and respond with the following response schema:
{
kycStatus: `KycStatusEnum`
}
On failure, the server MUST respond with the following response schema:
{
error: `ErrorEnum`
}
On conflict, the server MUST respond with the following response schema:
{
error: `ErrorEnum.ResourceExists`
}
This endpoint should accept data that the server will use to verify a user's KYC status. Upon receipt of valid data, the server SHOULD initiate a verification of the provided data, likely using a third party KYC service. If the user's region as reported by the independent KYC verification process does not match their "provided" geo (i.e., the one calculated from the caller's IP), the KYC verification SHOULD likely eventually be denied. Once a user submits KYC data, the server MUST maintain the status of the verification. If a client has enabled webhooks for the API, the server MUST publish these statuses to the webhook endpoint asynchronously.
If the selected schema (via the path parameter) is valid in the user's geo, and the request body exactly matches the selected schema, and the server does not already
have a KYC verification (either pending or complete) on file for the user, the server MUST return an HTTP 200
. The response body MUST contain the current state of the
KYC verification in the kycStatus
field.
The server may fail this request for two primary reasons; if the query is poorly formed, or if the server has already received KYC data for the user in the selected schema.
If the KYC schema selected in the path parameter is not supported for the user's geo, the server MUST return an UnsupportedSchema
error.
If the KYC schema selected in the path parameter is supported for the user's geo, but the request body does not match the selected schema, the server
MUST return an InvalidSchema
error.
If the server already has KYC data on file for the user in the selected schema, the server MUST return a ResourceExists
error.
The GET /kyc/:kycSchema/status
endpoint is used to query the status of an ongoing, completed, or expired KYC verification for a particular KYC schema type.
Note that these statuses MUST also be made available via webhook, if configured by the client.
kycSchema
: {KycSchemaEnum
} [REQUIRED]- The KYC schema used for the verification whose status is being requested
On success, the server MUST return an HTTP 200
status code, with the following response body:
{
kycStatus: `KycStatusEnum`
}
If the resource does not exist, the server MUST return an HTTP 404
status code, with the following response body:
{
error: `ErrorEnum.ResourceNotFound`
}
If KYC data has been submitted for a particular schema type, this endpoint MUST return the current status of the KYC verification for that schema.
On success, the server MUST return the status of the KYC verification.
This endpoint may fail if there is no record of KYC data being submitted for the requested schema.
If the server has no data on file for a KYC verification of the given schema, the server MUST return a ResourceNotFound
error.
The DELETE /kyc/:kycSchema
endpoint is used to delete a KYC record for a particular KYC schema.
kycSchema
: {KycSchemaEnum
} [REQUIRED]- The KYC schema to delete from the server
On success, the server MUST return an HTTP 200
status code, with an empty response body.
If the resource does not exist, the server MUST return an HTTP 404
status code, with the following response body:
{
error: `ErrorEnum.ResourceNotFound`
}
If KYC data has been submitted for a particular schema type, this endpoint MUST delete all record of the data from the server. If a user wishes to engage in a transfer that requires this KYC later, the user must resubmit the appropriate KYC information.
On successful deletion, the server MUST return an HTTP 200
status code.
This endpoint may fail if there is no record of KYC data for the requested schema.
If the server has no data on file for a KYC verification of the given schema, the server MUST return a ResourceNotFound
error.
FiatConnect's Fiat Account model consists of two primary concepts; the notion of an "account schema", and an "account type". Account schemas refer to the precise
representation of an account; i.e., the exact structure and fields required when communicating Fiat Account objects from client to server. All account schemas share
a set of base fields that are common to all schemas, such as "name", "institution", etc. Each account must have an "account type" associated with it, selected from
one of the account types supported by FiatConnect (see FiatAccountTypeEnum
for an initial list). For example, one can imagine two different account schemas,
CheckingAccountSchema1
and CheckingAccountSchema2
. These two schemas both would be associated with the, e.g., FiatAccountTypeEnum.CheckingAccount
account type,
but might require different data; it might be the case that different geos have different representations for communicating checking account information.
Similar to KYC schemas, providers may require different sorts of fiat (i.e., bank) accounts depending on both the geo of the client, and the particular details of the transfer. CICO providers MUST be able to maintain multiple fiat accounts on record for each user. Negotiation of what types of fiat accounts are allowed for a transfer, and what schemas to use to communicate the account details, occurs entirely within the quote endpoints.
In order for clients to uniquely select accounts on file when communicating with the server, the server MUST instrument their account records with unique identifiers (i.e. a UUID) that the client can use to select an account. These identifiers MAY be globally unique across all users, but need not be. A server MUST NOT allow a user to interact with fiat account identifiers (i.e., selecting one for a transfer) for accounts not owned by that user.
The POST /accounts
endpoint is used to store a new fiat account on file with the server.
The request body must contain the following fields:
fiatAccountSchema
: {FiatAccountSchemaEnum
} [REQUIRED]- The fiat account schema to use to add the fiat account
data
: {object
} [REQUIRED]- An object containing the data required by the Fiat Account schema chosen in the
fiatAccountSchema
field above.
- An object containing the data required by the Fiat Account schema chosen in the
On success, the server MUST respond with an HTTP 200
status code, along with a response body with the following schema:
{
fiatAccountId: `string`,
accountName: `string`,
institutionName: `string`,
fiatAccountType: `FiatAccountTypeEnum`,
fiatAccountSchema: `FiatAccountSchemaEnum`
}
On failure, the server MUST return an HTTP 400
error code, along with a response body with the following schema:
{
error: `ErrorEnum`
}
On conflict, the server MUST return an HTTP 409
error code, along with a response body with the following schema:
{
error: `ErrorEnum.ResourceExists`
}
This endpoint is used to add a new fiat account for a user.
On success, the server MUST return metadata associated with the account. The server MUST also generate a unique fiatAccountId
that the
client can later use to reference the account on file.
This endpoint may fail for two main reasons; if there's an issue with the selected schema/payload, or if the account already exists for the user. The server MAY implement checks to ensure that multiple accounts whose "unique fields" are the same cannot be added for a user. The semantics of these checks may be different for each fiat account schema/fiat account type, and are up to the server to implement. For example, a user may not be allowed to add two debit cards with the same numbers.
If the schema selected in the path parameter is not supported by the server, the server MUST return an UnsupportedSchema
error.
If the schema of the request body does not match the schema selected by the path parameter, the server MUST return an InvalidSchema
error.
If the server determines that the user is trying to add an account that is fundamentally identical to one that the user has already added, the
server MUST return a ResourceExists
error.
The GET /accounts
endpoint is used to return a list of all fiat accounts on file for a user.
This endpoint requires no parameters.
On success, the server MUST respond with an HTTP 200
status code, along with a response body with the following schema. Informally,
this is a mapping from fiat account types that the user has on file to metadata about those accounts.
{
[FiatAccountTypeEnum]: [{
fiatAccountId: `string`,
accountName: `string`,
institutionName: `string`,
fiatAccountType: `FiatAccountTypeEnum`,
fiatAccountSchema: `FiatAccountSchemaEnum`
}]
}
This endpoint is simply used to return a list of metadata about all of the fiat accounts that a user has on file with a provider.
On success, this endpoint MUST return a mapping from fiat account types to metadata about fiat accounts that the user has on file.
The DELETE /accounts/:fiatAccountId
endpoint is used to delete a user's fiat account from the server.
fiatAccountId
: {string
} [REQUIRED]- The internal fiat account ID to delete
On success, the server MUST return an HTTP 200
status code, with an empty response body.
If the resource does not exist, the server MUST return an HTTP 404
error code, along with the following response body.
{
error: `ErrorEnum.ResourceNotFound`
}
This endpoint allows a user to delete a fiat account record from the server. The user MUST only be able to delete accounts which they have
added themselves. In other words, if a server receives a fiatAccountId
that corresponds to an account added by a user other than the one
calling the endpoint, the server MUST return an HTTP 404
error code.
On successful deletion of the fiat account record, the server MUST return an HTTP 200
status code.
The server may fail if there is no fiat account on file with the provided fiatAccountId
for the current user.
If no fiat account is on file with the provided fiatAccountId
for the current user, the server MUST return a ResourceNotFound
error.
Once a user has created a fiat account with a CICO provider, they may begin a transfer. CICO providers MUST reject a transfer request if a fiat account identifier is provided that refers to an account type that is not allowed in the user's geo. A CICO provider MUST allow concurrent transfers for a single user. (This allows a user to try a new transfer in case one is hanging for whatever reason.)
Since transfers are asynchronous and may be concurrent, this specification requires that CICO providers accept an idempotency key in case the request/response is lost in transit and the client never receives a response. An idempotency key will allow the client to safely attempt to re-initiate a transfer without running the risk of creating a duplicate transaction. Idempotency keys MUST be implemented as request headers according to IETF's NWG's draft proposal.
The POST /transfer/in
endpoint is used to initiate a new transfer in from fiat to crypto. Since the transfer of fiat funds from a user to a provider
may be user or provider-initiated, the semantics of the /transfer/in
response differ depending on whether or not the fiat account schema used to execute
the given quote requires user action, as specified in the initial /quote/in
response associated with the provided quoteId
.
Idempotency-Key: <idempotency-key>
- An idempotency key, generated by the client
The request body must contain the following fields:
fiatAccountId
: {string
} [REQUIRED]- The fiat account ID to use for the transfer.
quoteId
: {string
} [REQUIRED]- Identifier of the quote to use for the transfer.
On a successfully initiated transfer in request, the server MUST respond with an HTTP 200
status code, along with the following response body:
{
transferId: `string`,
transferStatus: `TransferStatusEnum`,
transferAddress: `string`,
userActionDetails?: `TransferInUserActionDetailsEnum`
}
If the transfer parameters are invalid, or if the transfer is not possible for reasons other than the fiat account not existing, the server MUST
return an HTTP 400
error code, along with the following response body:
{
error: `ErrorEnum`
}
If the selected fiat account does not exist, the server MUST respond with an HTTP 404
error code, along with the following response body:
{
error: `ErrorEnum.ResourceNotFound`
}
The server MUST follow the semantics outlined in this draft proposal with respect to idempotency key errors.
This endpoint allows a user to initiate a new transfer in request. The server MUST support idempotency keys, and MUST NOT accept any requests which lack them.
If a user provides a fiatAccountId
that refers to an account they have on file that is allowed for the transfer, and the transfer parameters are acceptable,
and the user has non-expired KYC on file, and a quote with a matching quoteId
exists for the user and has not expired, the server MUST respond with an HTTP 200
and initiate the transfer.
For the transfer, the quote with quoteId
MUST be honored, meaning the same exchange rate and fees that were issued with the original quote MUST be used, as
well as the amount, fiat type, and crypto type.
When a new transfer is initiated, the server MUST
generate a transfer ID that the client can use to monitor the progress of the transfer. If the client has enabled webhooks, and the server supports them, the server
MUST call the user-specified webhook before returning an HTTP 200
. The response body MUST contain a transferAddress
, indicating the address that the provider will
use to send funds to the user's address from.
If the fiatAccountId
used to initiate the transfer in has a Fiat Account schema that was denoted in the initial /quote/in
response as requiring user action,
the response body MUST contain a userActionDetails
object containing the details required for the user to complete the desired action in order to send
fiat funds to the provider. The userActionDetails
object returned MUST be one of the schemas specified in TransferInUserActionDetailsEnum
, and correspond to the
TransferInUserActionDetailsEnum
value specified for the Fiat Account schema used for this transfer. If the fiatAccountId
selected for this transfer does not have a
Fiat Account schema that was specified as requiring user action in the initial /quote/in
response associated with the given quoteId
, the response body
MUST NOT contain a userActionDetails
field.
On success, the server MUST return the transferId
associated with the pending transfer, as well as the initial status of the transfer and the address that funds will be
sent from. If the selected Fiat Account requires user action in order to send fiat funds to the provider, the response body MUST contain a userActionDetails
field, as discussed
above, otherwise it MUST NOT. If supported and configured, the endpoint MUST also report the initial status of the transfer to the client-specified webhook.
This endpoint may fail for a number of reasons; the error code returned by the endpoint will vary depending on failure reason.
If a user's KYC has expired for their current geo, the server MUST reject the transfer and return a KycExpired
error.
If a transfer is not allowed for a generic reason (such as unacceptable transfer parameters) the server MUST reject the transfer and return a TransferNotAllowed
error.
If the quote associated with quoteId
is expired, or if no quote is found for the user with a matching quoteId
, or if the provided quote is for the
wrong type of transfer, the server MUST reject the transfer and return an InvalidQuote
error.
If the selected fiatAccountId
is not found for the current user, the server MUST reject the transfer and return a ResourceNotFound
error.
If the selected fiatAccountId
corresponds to an existing fiat account for the user, but that fiat account is not valid for use with this transfer, the server MUST
return an InvalidFiatAccount
error
The POST /transfer/out
endpoint is used to initiate a new transfer out from crypto to fiat.
Idempotency-Key: <idempotency-key>
- An idempotency key, generated by the client
The request body must contain the following fields:
fiatAccountId
: {string
} [REQUIRED]- The fiat account ID to use for the transfer.
quoteId
: {string
} [REQUIRED]- Identifier of the quote to use for the transfer.
On a successfully initiated transfer out request, the server MUST respond with an HTTP 200
status code, along with the following response body:
{
transferId: `string`,
transferStatus: `TransferStatusEnum`,
transferAddress: `string`
}
If the transfer parameters are invalid, or if the transfer is not possible for reasons other than the fiat account not existing, the server MUST
return an HTTP 400
error code, along with the following response body:
{
error: `ErrorEnum`
}
If the selected fiat account does not exist, the server MUST respond with an HTTP 404
error code, along with the following response body:
{
error: `ErrorEnum.ResourceNotFound`
}
The server MUST follow the semantics outlined in this draft proposal with respect to idempotency key errors.
This endpoint allows a user to initiate a new transfer out request. The server MUST support idempotency keys, and MUST NOT accept any requests which lack them.
If a user provides a fiatAccountId
that refers to an account they have on file that is allowed for the transfer, and the transfer parameters are acceptable,
and the user has non-expired KYC on file, and a quote with a matching quoteId
exists for the user and has not expired, the server MUST respond with an HTTP 200
and initiate the transfer.
For the transfer, the quote with quoteId
MUST be honored, meaning the same exchange rate and fees that were issued with the quote MUST be used, as
well as the amount, fiat type, and crypto type.
When a new transfer is initiated, the server MUST
generate a transfer ID that the client can use to monitor the progress of the transfer. If the client has enabled webhooks the server
MUST call the user-specified webhook before returning an HTTP 200
. The server MUST also return a transferAddress
representing the address that the user must send
funds to in order to complete the transfer.
On success, the server MUST return the transferId
associated with the pending transfer, as well as the initial status of the transfer and the address to send
funds to for the transfer. If supported and configured, the endpoint MUST also report the initial status of the transfer to the client-specified webhook.
This endpoint may fail for a number of reasons; the error code returned by the endpoint will vary depending on failure reason.
If a user's KYC has expired for their current geo, the server MUST reject the transfer and return a KycExpired
error.
If a transfer is not allowed for a generic reason (such as unacceptable transfer parameters) the server MUST reject the transfer and return a TransferNotAllowed
error.
If the quote associated with quoteId
is expired, or if no quote is found for the user with a matching quoteId
, or if the provided quote is for the
wrong type of transfer, the server MUST reject the transfer and return an InvalidQuote
error.
If the selected fiatAccountId
is not found for the current user, the server MUST reject the transfer and return a ResourceNotFound
error.
If the selected fiatAccountId
corresponds to an existing fiat account for the user, but that fiat account is not valid for use with this transfer, the server MUST
return an InvalidFiatAccount
error
The GET /transfer/:transferId/status
endpoint is used to get the status of an ongoing or completed transfer, as well as metadata about
the transfer.
transferId
: {string
} [REQUIRED]- The transfer ID for the transfer whose status to return.
On success, the server MUST return an HTTP 200
status code, along with a response body with the following schema:
{
status: `TransferStatusEnum`,
transferType: `TransferTypeEnum`,
fiatType: `FiatTypeEnum`,
cryptoType: `CryptoTypeEnum`,
amountProvided: `string`,
amountReceived: `string`,
fee?: `string`,
fiatAccountId: `string`,
transferId: `string`,
transferAddress: `string`,
txHash?: `string`,
userActionDetails?: `TransferInUserActionDetailsEnum`
}
If the user has no transfer on file with the provided transferId
, the server MUST return an HTTP 200
status code,
along with a response body with the following schema:
{
error: `ErrorEnum.ResourceNotFound`
}
This endpoint is meant to be used by clients to monitor the status of an ongoing or completed transfer request, as well as retrieve details about the transfer. Note that servers MUST also make this status information available via webhooks, if the client has configured them.
If the user has a transfer on file with the corresponding transferId
, the server MUST respond with an HTTP
200
status code. The fields in the success response body correspond to the details of the submitted transfer. In particular,
amountProvided
refers to the amount of fiat or crypto that the user has provided for the transfer. amountReceived
refers to the amount
of crypto or fiat that the server will be crediting to the user. fee
, if present refers to the fee, if any, associated with the transfer,
denominated in fiat or crypto, depending on the transfer type. fee
is given as a string-ified numerical amount (e.g. "1.0"
).
If the queried transfer represents a transfer in, and if the transfer has progressed to the point where the provider has sent crypto funds to the user,
the txHash
field MUST be present, and represent the hash of the transaction in which the provider sent the user crypto funds.
The txHash
MUST correspond to a valid transaction hash on the Celo blockchain, and its syntax must match the following regex: /^0x([A-Fa-f0-9]{64})$/
;
namely, it must be exactly the string 0x
followed by 64 hexadecimal characters.
Further, for queried transfers that represent transfers in, if the transfer required user action, the response body MUST contain userActionDetails
.
userActionDetails
, if present MUST be identical to the userActionDetails
object returned from the original /transfer/in
response
that initiated the transfer. If the requested transfer does not represent a transfer in requiring user action, userActionDetails
MUST NOT be present in the response.
This endpoint MUST fail when the user has no transfer on file with the provided transferId
.
If the user has no transfer on file with the provided transferId
, the server MUST return a ResourceNotFound
error.
The FiatConnect specification requires that both transfers in and out progress through a particular series of states that are made
available to the client both by polling the GET /transfer/:transferId/status
endpoint and via webhooks. Statuses for transfers in and
transfers out must progress according to a specific state machine. This state machine is different for both transfers in and transfers out.
Statuses for transfers in must progress through the state machine shown below, ultimately ending at one of the two terminal states:
TransferFailed
or TransferComplete
. A server SHOULD make a best effort to reach a terminal transfer state within the settlementTimeUpperBound
specified for the quote associated with the transfer, if one exists. Note that since a transfer in may fail after the server has received fiat funds
from the user's fiat account but before crypto funds are sent to the user, the server MUST return any fiat funds to the user's fiat account in case
of such a failure.
Since the transfer of fiat funds to the provider may require user action, or be provider-initiated, there are two unique paths
that a transfer can take to reach the terminal states. Transfers in requiring user action MUST NOT enter the TransferFiatFundsDebited
state, and transfers in requiring no user action (provider-initiated transfers) MUST NOT enter the TransferWaitingForUserAction
state.
graph LR
TransferStarted---->TransferFiatFundsDebited
TransferFiatFundsDebited---->TransferReceivedFiatFunds
TransferReceivedFiatFunds---->TransferSendingCryptoFunds
TransferSendingCryptoFunds---->TransferComplete
TransferStarted---->TransferFailed
TransferFiatFundsDebited---->TransferFailed
TransferWaitingForUserAction---->TransferFailed
TransferReceivedFiatFunds---->TransferFailed
TransferSendingCryptoFunds---->TransferFailed
TransferStarted---->TransferWaitingForUserAction
TransferWaitingForUserAction---->TransferReceivedFiatFunds
Statuses for transfers out must progress through the state machine shown below, ultimately ending at one of the three terminal states:
TransferFailed
, TransferAmlFailed
, or TransferComplete
. Note that at the beginning of a transfer out, a server may perform AML checks.
If these AML checks fail, the server MUST progress the state machine to TransferAmlFailed
state. A server SHOULD
make a best effort to reach a terminal transfer state within the settlementTimeUpperBound
specified for the quote associated with the transfer,
if one exists.
Transfers out are fundamentally different from transfer in in that they require user interaction partway through the state machine. Once a
transfer out's state moves to TransferReadyForUserToSendCryptoFunds
, the server should wait for the user to send the proper amount of crypto
funds to the address specified for the transfer. The server MUST wait until the quote's expiration to receive the user's crypto funds before
moving to the TransferFailed
state, though it MAY wait longer. Once the user has sent the proper amount of funds, the server MUST progress the state to
TransferReceivedCryptoFunds
, and continue with the transfer out.
Like with transfers in, a transfer out may fail after the server has received the user's crypto funds, but before the server has credited their fiat account with fiat funds. In case of such a failure, the server MUST return any crypto funds sent by the user back to the user's crypto address.
graph LR
TransferStarted---->TransferReadyForUserToSendCryptoFunds
TransferReadyForUserToSendCryptoFunds---->TransferReceivedCryptoFunds
TransferReceivedCryptoFunds---->TransferComplete
TransferStarted---->TransferFailed
TransferStarted---->TransferAmlFailed
TransferReadyForUserToSendCryptoFunds---->TransferFailed
TransferReceivedCryptoFunds---->TransferFailed
As mentioned throughout this document, CICO providers MUST support sending status updates to webhooks, whose URL is configurable by the client. CICO providers may generate an API token that the client can use to authenticate against the FiatConnect API, allowing the server to know where to send status updates to. This section outlines the details of webhook request syntax, as well as how the server must sign them to guarantee the client that the requests are genuine.
Webhook requests made from the server MUST be in a standard format, outlined below:
{
eventType: `WebhookEventTypeEnum`,
provider: `string`,
eventId: `string,
timestamp: `string`,
address: `string`,
payload: {WebhookKycStatusEventSchema | WebhookTransferInStatusEventSchema | WebhookTransferOutStatusEventSchema}
}
The eventType
field corresponds to the type of event being sent to the webhook. The provider
field corresponds to the name of the provider
that this webhook event is coming from. This is important to include, since the client's webhook handler may receive events from multiple
different providers. The eventId
field is a unique event ID generated by the server. The client may use this field as an idempotency key to
ensure that events are not processed more than once. The timestamp
field represents the exact time this event was initially generated, represented
as time since Epoch in seconds. The payload
field contains the actual event data, and will vary depending on the selected eventType
. address
is the Celo address of the user this webhook event is referencing.
Servers MUST implement retries with exponential backoff when sending payloads to a client's webhook. Servers MUST retry at least 4 times if the original
request does not return an HTTP 200
status code, but MAY retry more. The waiting period between the first 4 retries SHOULD be roughly 3, 66, 731, 4098 seconds respectively,
though these are rough guidelines. Once a server has received an HTTP 200
status code from the client's webhook, it MUST stop retrying.
WebhookKycStatusEventSchema
is the schema that defines webhook payloads for KYC events:
{
kycSchema: `KycSchemaEnum`,
kycStatus: `KycStatusEnum`
}
WebhookTransferInStatusEventSchema
is the schema that defines webhook payloads for transfer in events. Note that this schema is identical to the
one returned from the GET /transfer/:transferId/status
endpoint. The semantics for fields and their inclusion are also identical to those of the
GET /transfer/:transferId/status
endpoint.
{
status: `TransferStatusEnum`,
transferType: `TransferTypeEnum`,
fiatType: `FiatTypeEnum`,
cryptoType: `CryptoTypeEnum`,
amountProvided: `string`,
amountReceived: `string`,
fee?: `string`,
fiatAccountId: `string`,
transferId: `string`,
transferAddress: `string`,
txHash?: `string`,
userActionDetails?: `TransferInUserActionDetailsEnum`
}
WebhookTransferOutStatusEventSchema
is the schema that defines webhook payloads for transfer out events. Note that this schema is nearly identical to the
one returned from the GET /transfer/:transferId/status
endpoint; it lacks the optional txHash
, userActionType
and userActionDetails
field,
since these are not relevant to transfers out. The semantics for fields and their inclusion are otherwise identical to those of the GET /transfer/:transferId/status
endpoint.
{
status: `TransferStatusEnum`,
transferType: `TransferTypeEnum`,
fiatType: `FiatTypeEnum`,
cryptoType: `CryptoTypeEnum`,
amountProvided: `string`,
amountReceived: `string`,
fee?: `string`,
fiatAccountId: `string`,
transferId: `string`,
transferAddress: `string`
}
Requests made to a client's webhook handler MUST be signed, in order to prevent non-genuine webhook requests sent by malicious actors from being interpreted as genuine. To facilitate this, the CICO provider MUST generate a secret key that it shares with the client developer that can be used to recompute and verify the signature provided by the server.
Server requests to webhooks MUST contain a FiatConnect-Signature
header containing an HMAC. Using the private key that the CICO provider shares
with the client developer, a webhook can verify that a request is genuine by recomputing the digest and comparing it against the one provided by
the CICO provider.
The FiatConnect-Signature
header MUST contain two comma-separated key-value pairs. The first is of the form t=<unix_timestamp>
, and represents
the UNIX timestamp that the request was sent. The second will be of the form v1=<signature>
, and contain the signature itself. The signature
MUST be computed from the shared webhook private key and a dot-separated string consisting of the UNIX timestamp joined with the request body.
This signature verification design is based off of Persona's webhook documentation.
In the interest of preventing rounding errors, floating point errors, or similar errors, numerical amounts MUST be communicated as strings for every request parameter and every response field throughout the FiatConnect API.
Clients and servers alike SHOULD NOT cast fiat or crypto amounts to floating point numbers or integers, but should instead consider libraries such as bignumber.js for handling large numbers and decimals to an arbitrary number of decimal places.
Except where otherwise noted, amounts of cryptocurrencies MUST be given in the units of that currency, e.g. 1.5
Celo means
one and a half Celo (not 1.5e-18
Celo, which would be the case if amounts were instead given in
wei).
Amounts of fiat currencies MUST be given in the units of that currency, e.g. 5.00
USD means five USD.
Given that amounts are communicated in divisible units-- such as US Dollars, which are divisible into 100
cents, or Celo, which
is divisible into 10^18
wei-- it is expected that the client or server will sometimes need to communicate non-integer amounts.
All amounts MUST match the following regex: /^[0-9]+\.?[0-9]*$/
. Put in words, the required format is:
- one or more digits 0-9
- optionally, a period (used as a decimal mark for non-integer amounts)
- zero or more digits 0-9
FiatConnect-compliant API's MUST observe the maximum number of decimal places of precision for every cryptocurrency in CryptoTypeEnum
, and 2 decimal places of
precision for all fiat currencies. (Most, but not all, ERC-20 tokens offer up to 18 decimal places of precision.)
Note that this does NOT mean a CICO provider is required to support transfers of extremely small amounts; see here.
Note also that client and server MUST include all significant figures
of an amount, but MAY omit trailing zeroes. For example, "half a US Dollar" could be given as "0.5"
or "0.50"
,
and "one and one-tenth Celo" could be given as "1.1"
.
Anti-Money Laundering (AML) checks are an important part of regulatory compliance in the financial services industry. This proposal, however, does not require CICO providers to implement such checks. Providers MAY implement their own AML checks if they wish, according to local law. It is likely that by requiring KYC verification, CICO providers will be able to collect enough relevant information on the user in order to facilitate AML checks, if required.
In order to facilitate ease of client integration and testing against FiatConnect-compliant APIs, each FiatConnect API MUST have a corresponding sandbox API available. This sandbox API should be identical in behavior to the production FiatConnect API in every way, except for a number of key differences.
FiatConnect sandbox API implementations MUST operate against the Celo Alfajores network, rather than Mainnet. The Alfajores network operates with tokens with no monetary value, which allows testing of transfers without transacting real-world value.
Sandbox servers MUST recognize a different set of client API keys than the production API, in order to allow clients to register a different set of webhook URLs than those recognized by the production API.
All KYC submissions in the sandbox environment MUST eventually result in the KycStatusEnum.KycApproved
status. Sandbox environments MUST still send status updates
by webhook throughout the process, but verifications will always end in approval.
Sandbox APIs MUST never internally connect to a provided Fiat Account or perform any sort of validation that user-submitted Fiat Account details are "valid". Sandbox APIs MUST never actually interact with a user's personal fiat accounts.
Transfers in sandbox APIs will be much like ones in production APIs, but they will transfer tokens on the Alfajores network, which have no actual value. Sandbox APIs MUST never debit/credit actual fiat accounts, but they SHOULD receive/send crypto from/to the user's address depending on the type of transfer requested.
This document references a number of definitions, all of which are enumerated in their entirety below. There are two "types" of definitions; those which are static, and not subject to change upon this proposal's acceptance, and those that are dynamic, and meant to be extended by the community.
An enum listing KYC verification statuses.
[
`KycNotCreated`,
`KycPending`,
`KycApproved`,
`KycDenied`,
`KycExpired`
]
An enum listing the error types used by various endpoints.
[
`InvalidSignature`
`GeoNotSupported`,
`CryptoAmountTooLow`,
`CryptoAmountTooHigh`,
`FiatAmountTooLow`,
`FiatAmountTooHigh`,
`CryptoNotSupported`,
`FiatNotSupported`,
`UnsupportedSchema`,
`InvalidSchema`,
`ResourceExists`,
`ResourceNotFound`,
`TransferNotAllowed`,
`KycExpired`,
`Unauthorized`,
`SessionExpired`,
`InvalidParameters`,
`ContractLoginNotSupported`,
`NonceInUse`,
`IssuedTooEarly`,
`ExpirationTooLong`,
`InvalidFiatAccount`,
`InvalidQuote`
]
An enum listing transfer types.
[
`TransferIn`,
`TransferOut`
]
An enum listing payload types for webhook status updates.
[
`WebhookKycStatusEvent`
`WebhookTransferInStatusEvent`,
`WebhookTransferOutStatusEvent`
]
An enum listing the types of transfer statuses recognized by FiatConnect.
[
`TransferStarted`,
`TransferFiatFundsDebited`,
`TransferWaitingForUserAction`,
`TransferSendingCryptoFunds`,
`TransferAmlFailed`,
`TransferReadyForUserToSendCryptoFunds`,
`TransferReceivedCryptoFunds`,
`TransferReceivedFiatFunds`,
`TransferComplete`,
`TransferFailed`
]
An enum listing the types of fiat currencies supported by FiatConnect. All values should be ISO 4217 Currency Codes
[
`USD`,
`EUR`,
`BRL`,
`GNF`,
`INR`,
`NGN`,
`GHS`,
`KES`,
`ZAR`,
`PHP`,
`UGX`,
`GBP`,
`XOF`,
`RWF`,
`CNY`,
`XAF`,
`ARS`,
`BOB`,
`CLP`,
`COP`,
`FKP`,
`GYD`,
`PYG`,
`PEN`,
`SRD`,
`UYU`,
`VES`,
`MXN`,
`PAB`,
]
An enum listing the types of crypto tokens supported by FiatConnect.
[
`cUSD`,
`cEUR`,
`cREAL`,
`CELO`
]
An enum listing the KYC schema types recognized by the FiatConnect specification.
[
`PersonalDataAndDocuments`,
`PersonalDataAndDocumentsDetailed`
]
An enum listing the types of Fiat Accounts recognized by the FiatConnect specification. A Fiat Account Type is a property of each Fiat Account Schema, and represents what kind of account that schema represents.
[
`BankAccount`,
`MobileMoney`,
`DuniaWallet`
]
An enum listing the types of fees that providers may require on transfers.
[
`KycFee`,
`PlatformFee`
]
An enum listing the frequency, or how often, a particular fee needs to be paid.
[
`OneTime`,
`Recurring`
]
An enum lsiting the types of supported Fiat Account schemas.
[
`AccountNumber`,
`MobileMoney`,
`DuniaWallet`,
`IBANNumber`,
`IFSCAccount`,
`PIXAccount`,
]
An enum listing the types of User Action Detail Schemas for transfers in.
[
`PIXUserAction`,
`IBANUserAction`,
`PSEUserAction`,
`URLUserAction`
]
A KYC schema containing personal data about a user, as well as documents such as an ID photo and selfie.
{
firstName: `string`,
middleName?: `string`,
lastName: `string`,
dateOfBirth: {
day: `string`,
month: `string`,
year: `string`
},
address: {
address1: `string`,
address2?: `string`,
isoCountryCode: `string`,
isoRegionCode: `string`,
city: `string`,
postalCode?: `string`
},
phoneNumber: `string`,
selfieDocument: `string`,
identificationDocument: `string`
}
The selfieDocument
and identificationDocument
fields should be base64 encoded binary blobs representing images.
PersonalDataAndDocumentsDetailed
is a more detailed version of the PersonalDataAndDocuments
KYC Schema,
allowing the user to provide more information for CICO providers which require it.
{
firstName: `string`,
middleName?: `string`,
lastName: `string`,
dateOfBirth: {
day: `string`,
month: `string`,
year: `string`
},
address: {
address1: `string`,
address2?: `string`,
isoCountryCode: `string`,
isoRegionCode: `string`,
city: `string`,
postalCode?: `string`
},
phoneNumber: `string`,
email: `string`,
selfieDocument: `string`,
identificationDocumentType: `IdentificationDocumentTypeEnum`,
identificationDocumentFront: `string`,
identificationDocumentBack?: `string`
}
The phoneNumber
field is REQUIRED and MUST follow the formating of the E.164 international standard. It MUST contain all parts of the phone number including the Country Code.
The email
field is REQUIRED and MUST be a valid email.
The identificationDocumentType
field MUST be selected from IdentificationDocumentTypeEnum
, and is used to represent the kind of document being submitted.
The selfieDocument
, identificationDocumentFront
and identificationDocumentBack
fields MUST be base64 encoded binary blobs representing images.
The identificationDocumentBack
field is REQUIRED if the identificationDocumentType
is IDC
or DL
. Otherwise, it is OPTIONAL.
IdentificationDocumentType
is used to represent the type of document being submitted with a PersonalDataAndDocumentsDetailed
KYC schema.
[
`IDC`,
`PAS`,
`DL`
]
The enum values represent the following forms of identification:
IDC
: State-issued identity cardPAS
: PassportDL
: Driver's Licenes
All Fiat Account Schemas supported by FiatConnect MUST contain the accountName
, institutionName
, and fiatAccountType
fields. accountName
is a friendly, user-definable name for the account.
institutionName
is a user-friendly name representing the financial institution/organization the account is with, and fiatAccountType
is a FiatAccountTypeEnum
value,
representing the type of fiat account this schema represents. The institutionName
and accountName
fields are required in order for the API to return obfuscated but distinguishable
account information from the GET /accounts
endpoint.
AccountNumber
is a Fiat Account schema that represents accounts where the only identifying information required is some accountNumber
string.
{
accountName: `string`,
institutionName: `string`,
accountNumber: `string`,
country: `string`,
fiatAccountType: `FiatAccountTypeEnum.BankAccount`
}
The country
field should be a ISO 3166-1 alpha-2 country code. The field is for providers to specify which country this data is meant for. For example, if a provider is expecting an AccountNumber
schema for Nigeria, they can set the allowedValues
field for country
in the quote endpoint response to ['NG']
. With this information, the client will know
to prompt the user for a Nigeria-specific account number.
Depending on the allowedValues
field for country
, the client SHOULD impose restrictions on the type of data the user can provide for the accountNumber
field. A non-exhaustive list
is below:
'NG'
: The account number should be exactly 10 digits long, and only include the numbers 0-9.
MobileMoney
is a fiat account schema pertaining to virtual wallets provided by telecommunications companies in Africa.
The operator
field represents the name of the mobile operator, and mobile
is the phone number of the end user.
The property mobile
MUST follow the International format E.164 from ITU-T
with a plus sign prefix (i.e., +14155552671 for US). Finally, the country
field MUST be a ISO 3166-1 alpha-2 country code.
{
accountName: `string`,
institutionName: `string`,
mobile: `string`,
country: `string`,
operator: `string`,
fiatAccountType: `FiatAccountTypeEnum.MobileMoney`
}
The Dunia wallet is a proprietary wallet for people that have an account on the Dunia platform. So, any account on Dunia
platform can be used to consume Fiat Connect services by providing their mobile
as identifier. The property mobile
should follow the International format E.164 from ITU-T (i.e., +14155552671 for US).
{
accountName: `string`,
institutionName: `string`,
mobile: `string`,
fiatAccountType: `FiatAccountTypeEnum.DuniaWallet`
}
IBANNumber
is a representation of a bank account that is agnostic to the user's home country. The primary identifying field is iban
, which represents an
International Bank Account Number. See here for more context.
{
accountName: `string`,
institutionName: `string`,
iban: `string`,
country: `string`,
fiatAccountType: `FiatAccountTypeEnum.BankAccount`
}
The country
field MUST be a ISO 3166-1 alpha-2 country code. The syntax of the iban
field MUST correspond to that of a valid International
Bank Account Number.
IFSCAccount
is an account schema that represents transfers for INR
currency.
{
accountName: `string`,
institutionName: `string`,
ifsc: `string`,
accountNumber: `string`,
country: `string`,
fiatAccountType: `FiatAccountTypeEnum.BankAccount`
}
The country
field is a ISO 3166-1 alpha-2 country code.
The ifsc
or Indian Financial System Code is an 11-digit alpha-numeric code that is unique for bank branches that offer online money transfer options.
The accountNumber
field is unique between individuals and no two banks or account holders can have the same account number.
Banks use different starting codes for their branches for differentiation. (In India, bank account numbers usually contain 7 to 21 digits.)
PIXAccount
is a fiat account type for PIX, a Brazilian instant payment method created by the Brazilian Central Bank in which funds are transferred between accounts in a few seconds, at any time or day. Pix transfers can be carried out from a checking account, savings account or prepaid payment account.
{
accountName: `string`,
institutionName: `string`,
keyType: `PIXKeyTypeEnum`,
key: `string`,
fiatAccountType: `FiatAccountTypeEnum.BankAccount`
}
keyType
MUST be one of the following values:
[
`EMAIL`,
`CPF`,
`PHONE`,
`RANDOM`
]
If keyType
is EMAIL
, key
MUST be a valid email.
If keyType
is CPF, key
MUST be a CPF number.
If keyType
is PHONE
, key
MUST be an 11-digit Brazilian mobile phone number. Area code MUST be included. The string MUST match the regex /[0-9]{11}/
(a series of 11 digits, 0 through 9).
Otherwise, if keyType
is RANDOM
, key
MUST be UUID. If keyType
is RANDOM
, key
represents a random key generated by the user's bank.
All User Action Details Schemas supported by FiatConnect MUST contain the userActionType
field. The userActionType
field denotes which TransferInUserActionDetailsEnum
this schema is associated with; its value MUST be one of the values in TransferInUserAction
, and it MUST be unique across all supported User Action Details Schemas.
PIXUserAction
is a User Action Details Schema for transfers in requiring user action which use the PIXAccount
Fiat Account Schema.
{
userActionType: `TransferInUserActionDetailsEnum.PIXUserAction`,
pixString: `string`
}
The pixString
field is a copy-and-pastable code that the user can enter into the Pix payment system to initiate the transfer of fiat funds to the provider.
IBANUserAction
is a User Action Details Schema for transfers in requiring user action which use the IBANAccount
Fiat Account Schema.
{
userActionType: `TransferInUserActionDetailsEnum.IBANUserAction`,
iban: `string`,
bic: `string`
}
The iban
field represents the IBAN number for the provider-controlled bank account that the user should send funds to. The bic
field represents the
Bank Identifier Code associated with the institution with which the provider-controlled bank account is registered.
Deprecated: Note that PSEUserAction
is deprecated and its use is discouraged, in favor of the more general URLUserAction
schema. Consider migrating
existing implementations over to using this schema instead of PSEUserAction
.
PSEUserAction
is a User Action Details Schema for transfers in requiring use of the Colombian PSE payment system.
{
userActionType: `TransferInUserActionDetailsEnum.PSEUserAction`,
url: `string`,
}
The url
field contains a uniquely generated URL which the user can follow in order to complete their transfer of fiat funds to the provider using the PSE payment system.
URLUserAction
is a User Action Details Schema for transfers in which require the user to follow a URL to complete the transfer of fiat funds. The URL itself, and
action required by the user after navigating to the URL are unspecified by this schema; no particular semantics are required or imparted by use of the URLUserAction
schema.
{
userActionType: `TransferInUserActionDetailsEnum.URLUserAction`,
url: `string`,
}
The url
field contains a URL which the user can follow in order to complete thier transfer of fiat funds to the provider.
AccountNumberUserAction
is a User Action Details Schema for transfers in which require the user to send funds to
an account identified by some number to complete the transfer of fiat funds.
{
userActionType: `TransferInUserActionDetailsEnum.AccountNumberUserAction`,
institutionName: `string`,
accountName: `string`,
accountNumber: `string`,
transactionReference?: `string`,
deadline?: `string`
}
The institutionName
, accountName
, and accountNumber
fields describe a provider-controlled
account that the user should send funds to. The transactionReference
field is a string that is intended to help providers
determine which FiatConnect transfer is associated with an incoming fiat transfer. If included, the user MUST include
the transactionReference
string in the transaction details when sending funds to the provider's account. The deadline
field, if included, MUST be an ISO 8601 datetime string, and represents the time by which the user needs to send funds
to the provider's fiat account. If the user sends fiat funds after this time, the provider may choose whether to complete the
transfer or return the funds and mark the transfer as failed.
Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, March 1997, https://www.rfc-editor.org/info/rfc2119.
Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, May 2017, https://www.rfc-editor.org/info/rfc8174.
M. Jones, "JSON Web Token", RFC7519, May 2015, https://datatracker.ietf.org/doc/html/rfc7519.
EIP-4361: Sign-In with Ethereum, https://eips.ethereum.org/EIPS/eip-4361
EIP-55: Mixed-case checksum address encoding, https://eips.ethereum.org/EIPS/eip-55
EIP-191: Signed Data Standard, https://eips.ethereum.org/EIPS/eip-191
EIP-1271: Standard Signature Validation Method for Contracts, https://eips.ethereum.org/EIPS/eip-1271
EIP-2098: Compact Signature Representation, https://eips.ethereum.org/EIPS/eip-2098
Persona, "Best Practices", https://docs.withpersona.com/docs/best-practices.
J. Jena, The Idempotency-Key HTTP Header Field, July 2021, https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header-00
ISO 3166-1 https://en.wikipedia.org/wiki/ISO_3166-1
ISO 3166-2 https://en.wikipedia.org/wiki/ISO_3166-2