SEP: 0031
Title: Direct Payments
Author: SDF
Status: Active
Created: 2020-04-07
Updated: 2020-12-15
Version 1.2.0
This SEP defines a protocol for enabling direct fiat to fiat payments between two financial accounts that exist outside of the Stellar network. The payments are facilitated by two anchors.
This proposal facilitates the ability for anchors to build a rail between two regions, allowing end users to send fiat from one bank account directly into another end user's bank account. In this flow, neither user needs to deal with the Stellar network as the two anchors take care of everything for them.
Alice in Nigeria wants to send money to Bob in Europe. Alice signs up with NigeriaPay to make this payment to send money directly into Bob’s bank account. Bob doesn’t need to do anything, or know anything about this payment, besides letting Alice know what his bank account information is. Alice only needs to deal with her anchor (NigeriaPay).
NigeriaPay will utilize its European rail, enabled with EuroPay Anchor service, to move the money to EuroPay in order to deposit it into Bob’s bank account.
- An anchor must define the location of their
DIRECT_PAYMENT_SERVER
in theirstellar.toml
. Anchors will find each other's servers by pulling this TOML file from their home domains. - Anchors will create bi-lateral agreements to interoperate with each other. This differs from other protocols in that there is no concept of 'discoverability'. Anchors should keep a mapping of their partnerships in different regions to their home domains.
- Each anchor registers a Stellar key with each counterparty anchor they interact with in order to identify themselves via SEP-10 Web Authentication.
Anchors should support SEP-10 web authentication to ensure the counterparty they're interoperating with is actually who they say they are. Clients must submit the JWT obtained via the SEP-10 authentication flow to all API endpoints except /info
.
The JWT should be included as a request header:
Authorization: Bearer <JWT>
Any API request that fails to meet proper authentication should return a 403 Forbidden response.
This protocol involves the transfer of value, and so HTTPS is required for all endpoints for security. Anchors should refuse to interact with any insecure HTTP endpoints.
All endpoints accept in requests the following Content-Type
s:
application/json
All endpoints respond with content type:
application/json
- Sending Client: The end user who is initiating a payment via the sending anchor.
- Sending Anchor: The business offering outbound payment services. Takes fiat in from the sending client, and has a business relationship with the receiving anchor.
- Receiving Anchor: The business offering inbound payment processing. Deposits fiat in the receiving client's bank account, and has a business relationship with the sending anchor.
- Receiving Client: The owner of the destination bank account.
- To create a rail, find a counterparty who implements this SEP in the region you wish to provide access to and agrees to do business with you.
- Trade public keys with each other in order to identify and securely interoperate with each other.
- Keep a mapping of region to home_domain, using home_domain as your initial entry point to interoperating.
- The Sending Client (user) iniates a direct payment to the Receiving Client.
- The Sending Anchor identifies the Receiving Anchor it will use for the payment based on the receipient's location and desired currency.
- The Sending Anchor makes a request to the Receiving Anchor's
/info
endpoint to collect asset information and thetransaction.fields
describing the pieces of information required by the Receiving Anchor. - If the Receiving Anchor has a
sender_sep12_type
and/or areceiver_sep12_type
attribute in the/info
endpoint response,- The Sending Anchor must collect the Receiving Anchor's
KYC_SERVER
URI from the Receiving Anchor's SEP-1 stellar.toml file. - The Sending Anchor must make a
GET KYC_SERVER/customer
request for each_sep12_type
value listed in the/info
response. Each/customer
response contains SEP-9 fields required by the Receiving Anchor for that user.
- The Sending Anchor must collect the Receiving Anchor's
- The Sending Anchor collects all required information from the Sending Client. This includes the custom fields listed in the Receiving Anchor's
/info
response as well as the KYC fields described in the/customer
responses for both the Sending and Recieving Clients. How the Sending Anchor collects this information from the Sending Client is out of the scope of this document, but the Receving Client should not be required to take any action in order for the Sending Client to make the payment to the Receiving Anchor. - The Sending Anchor makes
PUT KYC_SERVER/customer
requests containing the SEP-9 values listed in theGET KYC_SERVER/customer
responses to register the Clients with the Receiving Anchor. - On successful registration (
202
HTTP status), the Sending Anchor makes aPOST DIRECT_PAYMENT_SERVER/transactions
request to create apending_sender
transaction with the Receiving Anchor.- Note that this request contains the
id
s returned by thePUT /customer
requests, thetransaction.fields
values collected from the Sending Client, as well as the transaction amount and asset information.
- Note that this request contains the
- Once the transaction is included in the
GET DIRECT_PAYMENT_SERVER/transactions
response and the transaction'sstatus
ispending_sender
, the Sending Anchor submits the path payment transaction to Stellar.- The destination account of the transaction must be the
stellar_account_id
returned in thePOST /transactions
response. - The memo of the transaction must match the
stellar_memo
andstellar_memo_type
returned in thePOST /transactions
response.
- The destination account of the transaction must be the
- The Sending Anchor polls the Receiving Anchor's
GET DIRECT_PAYMENT_SERVER/transactions
endpoint until the transaction'sstatus
changes tocompleted
,error
,pending_customer_info_update
, orpending_transaction_info_update
. - If
completed
, the job is done and the Sending Anchor should notify the Sending Client. - If
error
, the Receiving Anchor should be contacted to resolve the situation. - If
pending_transaction_info_update
, thetransaction.fields
values collected from the Sending Client were invalid and must be corrected by the Sending Client- This requires the Sending Anchor to detect which fields were invalid from the
required_info_updates
object on the/transactions
record. - Then the Sending Anchor must reach out the Sending Client again, collect valid values, and make a
PATCH DIRECT_PAYMENT_SERVER/transactions
request to the Receiving Anchor
- This requires the Sending Anchor to detect which fields were invalid from the
- If
pending_customer_info_update
, the SEP-9 KYC values collected were invalid and must be corrected by the Sending Client- This requires the Sending Anchor to make a
GET KYC_SERVER/customer
request for each customer associated with the transaction to determine which fields need to be updated - The Sending Anchor then reaches out to the Sending Client again, collects valid values for the invalid fields, and makes a
PUT KYC_SERVER/customer
request to the Receiving Anchor
- This requires the Sending Anchor to make a
- After providing the Receiving Anchor with updated values, the status should ultimately change to
completed
- The Sending Anchor makes a request to the Receiving Anchor's
DIRECT_PAYMENT_SERVER/info
endpoint. - The Sending Anchor makes a
GET KYC_SERVER/customer
request for each_sep12_type
attribute included in the response. - The Sending Anchor makes a
PUT KYC_SERVER/customer
request for each_sep12_type
attribute.- The Receiving Anchor must validate the KYC data provided and reject the request with useful error messages if invalid. This is critical for limiting the number of times a transaction ends up in a
pending_customer_info_update
status
.
- The Receiving Anchor must validate the KYC data provided and reject the request with useful error messages if invalid. This is critical for limiting the number of times a transaction ends up in a
- The Sending Anchor makes a
POST DIRECT_PAYMENT_SERVER/transactions
request.- The Receiving Anchor must validate the asset, amount, transaction fields, and customers.
- The Recieving Anchor must create a transaction record in their database and expose it via
/transactions
. - Transactions should initially be
pending_sender
. If any preprocessing is required before receiving a payment, mark the transaction aspending_receiving
until ready.
- The Receiving Anchor then waits to receive the payment identified by the
stellar_memo
returned in thePOST /transactions
response. - Once the Stellar payment has been received and matched with the internal transaction record, the Receiving Anchor must attempt to transfer an equivalent amount of the asset (minus fees) off-chain to the Receiving Client using the KYC and rails data collected by the Sending Anchor
- If the off-chain payment succeeds, the transaction's status should be updated to
completed
- If the off-chain payment cannot be received by the Recieving Client almost immediately, the transaction's status should be updated to
pending_external
until received. Then,completed
. - If the off-chain payment fails, the Recieving Anchor must determine why, which is outside the scope of this document. Once determined, the Reciving Anchor must either correct it themselves (internal error) or receive updated values from the Sending Anchor for the fields that were discovered to be invalid.
- If the invalid values were described in
/info
'stransaction.fields
object, the transaction's status should be updated topending_transaction_info_update
andrequired_info_updates
should contain an object describing the errors. - If the invalid values were described in
GET /customer
responses, the transaction's status should be updated topending_customer_info_update
and the invalid field names should be returned in the nextGET /customer?id=
request for each Client. - The Sending Client will detect transaction's status and invalid fields, collect the info from the Sending Client, and make requests to the Recieving Anchor containing the updated information.
- Once the passed information is validated, the Receiving Anchor should update the transaction's status to
pending_receiver
and retry the off-chain transfer. This loop of attempting the transfer and waiting for updated information should continue until the transfer is successful.
- If the invalid values were described in
GET DIRECT_PAYMENT_SERVER/info
Allows an anchor to communicate basic info about what currencies their DIRECT_PAYMENT_SERVER
supports receiving from partner anchors.
Request parameters:
Name | Type | Description |
---|---|---|
lang |
string | (optional) Defaults to en . Language code specified using ISO 639-1. description fields in the response should be in this language. |
The response should be a JSON object like:
{
"receive":{
"USD":{
"enabled":true,
"fee_fixed":5,
"fee_percent":1,
"min_amount":0.1,
"max_amount":1000,
"sep12": {
"sender": {
"types": {
"sep31-sender": {
"description": "U.S. citizens limited to sending payments of less than $10,000 in value"
},
"sep31-large-sender": {
"description": "U.S. citizens that do not have sending limits"
},
"sep31-foreign-sender": {
"description": "non-U.S. citizens sending payments of less than $10,000 in value"
}
}
},
"receiver": {
"types": {
"sep31-receiver": {
"description": "U.S. citizens receiving USD"
},
"sep31-foreign-receiver": {
"description": "non-U.S. citizens receiving USD"
}
}
}
},
"fields":{
"transaction":{
"receiver_routing_number":{
"description": "routing number of the destination bank account"
},
"receiver_account_number":{
"description": "bank account number of the destination"
},
"type":{
"description": "type of deposit to make",
"choices":[
"SEPA",
"SWIFT"
]
}
}
}
}
}
}
The JSON object contains an entry for each asset that the anchor supports for receiving and completing a direct payment.
sep12
: an object containingsender
andreceiver
keys.sender
: an object containing atypes
key if KYC information is required for the sending client, empty otherwise.types
: (optional) an object containing the accepted values for thetype
parameter in SEP-12 requests for sending clients. Each key should map to an object value with a human-readabledescription
. This field can be omitted if no KYC is necessary for the sending client.
receiver
: an object containing atypes
key if KYC information is required for the receiving client, empty otherwise.types
: (optional) an object containing the accepted values for thetype
parameter in SEP-12 requests for receiving clients. Each key should map to an object value with a human-readabledescription
. This field can be omitted if no KYC is necessary for the receiving client.
min_amount
: (optional) minimum amount. No limit if not specified.max_amount
: (optional) maximum amount. No limit if not specified.fee_fixed
: (optional) fixed (flat) fee for deposit. In units of the received asset. Leave blank if there is no fee or the fee schedule is complex.fee_percent
: (optional) percentage fee for deposit. In percentage points. Leave blank if there is no fee or the fee schedule is complex.sender_sep12_type
: (deprecated, optional) value of thetype
parameter the sending anchor should use for aSEP-12 GET /customer
request. This field can be omitted if no KYC is necessary. Use the values insep12.sender.types
instead if it is present.receiver_sep12_type
: (deprecated, optional) value of thetype
parameter the sending anchor should use for aSEP-12 GET /customer
request. This field can be omitted if no KYC is necessary. Use the values insep12.receiver.types
instead if it is present.fields
: as explained below.
The fields
object allows an anchor to describe fields that must be passed into POST /transactions
. Only fields related to the transaction should be described in the fields
object. In the example above, the receiving anchor requires the account and routing number of the receiving client's bank account.
Each fields
sub-object contains a key for each field name and an object with the following fields as the value:
description
: (required) description of field to show to user.choices
: (optional) list of possible values for the field.optional
: (optional) false if not specified.
If KYC is required for a sending or receiving client in some cases but not others, it is recommended to provide values in the respective types
object for all cases and return an empty fields
object from SEP-12 GET /customer for the cases where no KYC is necessary.
POST DIRECT_PAYMENT_SERVER/transactions
Content-Type: application/json
{
"amount": 100,
"asset_code": "USD",
"asset_issuer": "GDRHDSTZ4PK6VI3WL224XBJFEB6CUXQESTQPXYIB3KGITRLL7XVE4NWV",
"sender_id": "d2bd1412-e2f6-4047-ad70-a1a2f133b25c",
"receiver_id": "137938d4-43a7-4252-a452-842adcee474c",
"fields": {
"transaction": {
"receiver_routing_number": "442928834",
"receiver_account_number": "0029483242",
"type": "SEPA"
}
}
}
This post requests attempts to initiate a payment through this anchor. It should provide the amount and all the required fields (specified in the /info
endpoint). The values for sender_id
and receiver_id
are from the receiving anchor's SEP-12 PUT /customer
responses.
If the request describes a valid transaction that this anchor can fulfill, we return a success response with details on what to send. If the request is not valid, or we need more info, we can return with an error response and expect the sending anchor to try again with updated values.
Name | Type | Description |
---|---|---|
amount |
number | Amount of payment in destination currency. |
asset_code |
string | Code of the asset the sending anchor intends to send. This must match one of the entries listed in the receiving anchor's /info endpoint. |
asset_issuer |
string | (optional) The issuer of the Stellar asset the sending anchor intends to send. If not specified, the asset sent must be issued by the receiving anchor. |
sender_id |
string |
(optional) The ID included in the SEP-12 PUT /customer response for the sending client. If sender_sep12_type was included in the /info response, this field is required. |
receiver_id |
string |
(optional) The ID included in the SEP-12 PUT /customer response for the receiving client. If receiver_sep12_type was included in the /info response, this field is required. |
fields |
object | A key-pair object containing the values requested by the receiving anchor in their /info endpoint containing a single "transaction" object. |
lang |
string | (optional) Defaults to en . Language code specified using ISO 639-1. Any human-readable error codes or field descriptions will be returned in this language. |
This is the successful case where a receiving anchor confirms that they can fulfill this payment as described. The response body should be a JSON object with the following values
Name | Type | Description |
---|---|---|
id |
string | Persistent identifier to check the status of this payment |
stellar_account_id |
string | Stellar account to send payment to |
stellar_memo_type |
string | Type of memo to attach to the Stellar payment (text , hash , or id ) |
stellar_memo |
string | The memo to attach to the Stellar payment |
In the case where the sending anchor didn't provide all the KYC information requested in GET /customer
, or where the receiver requires additional KYC information after learning of the transaction amount
, the request should fail with a 400 status code and the following body in JSON format. The sender should then retry both the GET /customer
request to collect the additional fields and the PUT /customer
request including all fields described in the GET /customer
response.
Name | Type | Description |
---|---|---|
error |
string | customer_info_needed |
type |
string | (optional) A string for the type URL argument the sending anchor should use when making the GET /customer request. The value should be included in the sender or receiver types response object from /info . |
In the case where the sending anchor didn't provide all the information requested in /info
, or if the transaction requires extra information, the request should fail with a 400 status code and the following body in JSON format. The sender should then retry the entire request including all the previously sent fields plus the fields described in the response.
Name | Type | Description |
---|---|---|
error |
string | transaction_info_needed |
fields |
object | A key-value pair of missing fields in the same format as fields described in /info . |
In the case where the transaction just cannot be completed, return an error response with a JSON object containing an error
key describing the error in human-readable format in the language indicated in the request.
{
'error': "The amount was above the maximum limit"
}
{
'error': "That bank account is restricted via AML laws"
}
The transaction endpoint enables senders to query/validate a specific transaction at a receiving anchor.
GET DIRECT_PAYMENT_SERVER/transactions/:id
Request parameters:
Name | Type | Description |
---|---|---|
id |
string | The id of the transaction. |
On success the endpoint should return 200 OK
HTTP status code and a JSON object with the following fields:
Name | Type | Description |
---|---|---|
transaction |
object | The transaction that was requested by the client. |
The transaction
object should be of the following schema.
Name | Type | Description |
---|---|---|
id |
string | Unique, anchor-generated id for the deposit/withdrawal. |
status |
string | Processing status of deposit/withdrawal. |
status_eta |
number | (optional) Estimated number of seconds until a status change is expected |
amount_in |
string | (optional) Amount received by anchor at start of transaction as a string with up to 7 decimals. Excludes any fees charged before the anchor received the funds. |
amount_out |
string | (optional) Amount sent by anchor to user at end of transaction as a string with up to 7 decimals. |
amount_fee |
string | (optional) Amount of fee charged by anchor. |
stellar_account_id |
string | Stellar account to send payment to. |
stellar_memo_type |
string | Type of memo to attach to the Stellar payment: text , hash , or id . |
stellar_memo |
string | The memo to attach to the Stellar payment. |
started_at |
UTC ISO 8601 string | (optional) Start date and time of transaction. |
completed_at |
UTC ISO 8601 string | (optional) Completion date and time of transaction. |
stellar_transaction_id |
string | (optional) transaction_id on Stellar network of the transfer that initiated the payment. |
external_transaction_id |
string | (optional) ID of transaction on external network that completes the payment into the receivers account. |
refunded |
boolean | (optional) Should be true if the transaction was refunded. Not including this field means the transaction was not refunded. |
required_info_message |
string | (optional) A human-readable message indicating any errors that require updated information from the sender. |
required_info_updates |
object | (optional) A set of fields that require update from the sender, in the same format as described in /info. This field is only relevant when status is pending_transaction_info_update . |
status
should be one of:
pending_sender
-- awaiting payment to be initiated by sending anchor.pending_stellar
-- transaction has been submitted to Stellar network, but is not yet confirmed.pending_customer_info_update
-- certain pieces of information need to be updated by the sending anchor. See pending customer info update sectionpending_transaction_info_update
-- certain pieces of information need to be updated by the sending anchor. See pending transaction info update sectionpending_receiver
-- payment is being processed by the receiving anchorpending_external
-- payment has been submitted to external network, but is not yet confirmed.completed
-- deposit/withdrawal fully completed.error
-- catch-all for any error not enumerated above.
Example response:
{
"transaction": {
"id": "82fhs729f63dh0v4",
"status": "pending_external",
"status_eta": 3600,
"external_transaction_id": "ABCDEFG1234567890",
"amount_in": "18.34",
"amount_out": "18.24",
"amount_fee": "0.1",
"started_at": "2017-03-20T17:05:32Z"
}
}
{
"transaction": {
"id": "82fhs729f63dh0v4",
"status": "pending_info_update",
"status_eta": 3600,
"external_transaction_id": "ABCDEFG1234567890",
"amount_in": "18.34",
"amount_out": "18.24",
"amount_fee": "0.1",
"started_at": "2017-03-20T17:05:32Z",
"required_info_message": "The bank reported an incorrect account number for the receiver, please ensure the account matches legal documents",
"required_info_updates": {
"transaction": {
"receiver_account_number": {
"description": "The receiver's bank account number"
}
}
}
}
}
If the transaction cannot be found, the endpoint should return a 404 NOT FOUND
result.
In certain cases the receiver might need to request updated information. For example, if the bank tells the anchor that the provided receiver's name is incorrect or missing a middle initial. Since this information was sent via SEP-12, the transaction should go into the pending_customer_info_update
state until the sender makes another PUT /customer
request to update. The sending anchor can check which fields need to be updated by making a GET /customer
request including the id
or account
& memo
parameters. The receiving anchor should respond with a NEEDS_INFO
status and last_name
included in the fields described.
Another possibility is that the bank tells the receiving anchor that the provided account or routing number is incorrect. Since this information was sent via a POST /transactions
request, the transaction should go into the pending_transaction_info_update
state until the sender makes a request to the endpoint outlined below.
This endpoint should only be used when the receiver requests more info via the pending_transaction_info_update
status. The required_info_updates
transaction field should contain the fields required for the update. If the sender tries to update at a time when no info is requested the receiver should fail with an error response.
PATCH DIRECT_PAYMENT_SERVER/transactions/:id
Request parameters:
Name | Type | Description |
---|---|---|
id |
string | The id of the transaction. |
fields |
object | A key-pair object containing the values requested to be updated by the receiving anchor in the same format as /transactions . |
PATCH DIRECT_PAYMENT_SERVER/transactions/82fhs729f63dh0v4
{
"fields": {
"transaction": {
"receiver_bank_account": "12345678901234",
"receiver_routing_number": "021000021"
}
}
}
If the information was successfully updated, respond with a 200 status code, and return the transaction JSON in the body. The transaction should return to pending_receiver
, though it is possible that the information could still need to be updated again.
If the transaction specified by "id"
does not exist, return a 404 response.
If the information was malformed, or if the sender tried to update data that isn't updatable, return a 400 with an object containing an error message.
{
"error": "Supplied fields do not allow updates, please only try to updates the fields requested"
}