SEP-31 is a bilateral payment standard for one anchor’s user to make payments to another anchor’s user. Where SEP-6 and SEP-24 allow users to deposit and withdraw their funds on and off the Stellar network, users of SEP-31 anchors may not even know they are using Stellar. A user can simply send fiat or crypto to the sending anchor and have them send the same amount (minus fees) to another anchor who can deposit the off-chain funds into the receiving user’s account.

An anchor can use the integrations outlined below to implement a fully functional SEP-31 anchor.


Add the SEP to POLARIS_ACTIVE_SEPS in in your settings file.

POLARIS_ACTIVE_SEPS = ["sep-1", "sep-10", "sep-31", ...]


Where SEP-6 and SEP-24 use DepositIntegration and WithdrawalIntegration, SEP-31 uses SEP31ReceiverIntegration and RailsIntegration. Note that in future releases, some SEP-6 and SEP-24 functions related to payment rails may be moved from DepositIntegration or WithdrawalIntegration to RailsIntegration.

SEP-31 Endpoints, asset: polaris.models.Asset, lang: str = None) → Dict[KT, VT]

Return a dictionary containing the “fields” object as described in the info response for the given asset. If your anchor requires KYC information about the sender or receiver, return the “receiver_sep12_type” or “sender_sep12_type” key-value pairs as well. Polaris will provide the rest of the fields documented in the info response.

Descriptions should be in the lang passed if supported.

return {
    "receiver_sep12_type": "sep31-receiver",
    "sender_sep12_type": "sep31-sender",
    "fields": {
              "description": "routing number of the destination bank account"
              "description": "bank account number of the destination"
              "description": "type of deposit to make",
  • asset – the Asset object for the field values returned
  • lang – the ISO 639-1 language code of the user
polaris.integrations.SEP31ReceiverIntegration.process_post_request(self, params: Dict[KT, VT], transaction: polaris.models.Transaction) → Optional[Dict[KT, VT]]

Use the params passed in the request to do any processing of the user and requested transaction necessary to facilitate the payment to the receiving user. If the arguments are valid, save transaction and link it to your other models. If the transaction is saved but an error response is returned Polaris will return a 500 response to the user.

Polaris validates that the request includes all the required fields returned by but cannot validate the values. Return None if the params passed are valid, otherwise return one of the error dictionaries outlined below.

If the sender_id or receiver_id values are invalid or the information collected for these users is not sufficient to process this request, return a dictionary matching the customer-info-needed response schema.

return {
    "error": "customer_info_needed",
    "type": "sep31-large-amount-sender"

For example, the above response could be used if the anchor requires additional information on the sender when the amount is large. The type key specifies the appropriate type value the client should use for the sender’s SEP-12 GET /customer request, and is optional.

If some optional fields from info() are missing but needed for this transaction, return a dictionary matching the schema described in the transaction-info-needed response.

return {
    "error": "transaction_info_needed",
    "fields": {
        "transaction": {
            "sender_bank_account": {
                "description": (
                    "The bank account number used by the sender. "
                    "Only required for large transactions."
                "optional": True

If some parameters passed are simply not acceptable, return a dictionary containing a single "error" key-value pair.

return {
    "error": "invalid 'sender_bank_account' format"
  • params – The parameters included in the /transaction request
  • transaction – the Transaction object representing the transaction being processed
polaris.integrations.SEP31ReceiverIntegration.process_patch_request(self, params: Dict[KT, VT], transaction: polaris.models.Transaction)

Use the params passed in the request to update transaction or any related data.

Polaris validates that every field listed in Transaction.required_info_updates is present in params but cannot validate the values. If a ValueError is raised, Polaris will return a 400 response containing the exception message in the body.

If no exception is raised, Polaris assumes the update was successful and will update the transaction’s status back to pending_receiver as well as clear the required_info_updates and required_info_message fields.

Once the transaction enters the pending_receiver status, the execute_outgoing_transactions process will attempt to send the payment to the receiving user. See the RailsIntegration.execute_outgoing_transaction function for more information on the lifecycle of a transaction.

  • params – the request body of the PATCH /transaction request
  • transaction – the Transaction object that should be updated
polaris.integrations.SEP31ReceiverIntegration.valid_sending_anchor(self, public_key: str) → bool

Return True if public_key is a known anchor’s stellar account address, and False otherwise. This function ensures that only registered sending anchors can make requests to protected endpoints.

Parameters:public_key – the public key of the sending anchor’s stellar account

Payment Rails

polaris.integrations.RailsIntegration.execute_outgoing_transaction(self, transaction: polaris.models.Transaction)

Send the amount of the off-chain asset specified by transaction minus fees to the user associated with transaction. This function is used for SEP-6 & SEP-24 withdraws as well as SEP-31 payments.

When this function is called, transaction.amount_in is the amount sent to the anchor, not the amount specified in a SEP-24 or SEP-31 API call. This matters because the amount actually sent to the anchor may differ from the amount specified in an API call. That is why you should always validate transaction.amount_in and calculate transaction.amount_fee here.

If the amount is invalid in some way, the anchor must choose how to handle it. If you choose to refund the payment in its entirety, change transaction.status to “error”, assign an appropriate message to transaction.status_message, and update transaction.refunded to True.

You could also refund a portion of the amount and continue processing the remaining amount. In this case, the transaction.status column should be assigned one of the expected statuses for this function, mentioned below, and the amount_in field should be reassigned the value the anchor is accepted.

If the funds transferred to the user become available in the user’s off-chain account immediately, update Transaction.status to Transaction.STATUS.completed. If the transfer was simply initiated and is pending external systems, update the status to Transaction.STATUS.pending_external.

If an exception is raised, the transaction will be left in its current status and may be used again as a parameter to this function. To ensure the exception isn’t repeatedly re-raised, change the problematic transaction’s status to Transaction.STATUS.error.

If transaction.protocol == Transaction.PROTOCOL.sep31 and more information is required from the sending anchor or user to complete the transaction, update the status to Transaction.STATUS.pending_transaction_info_update and save a JSON-serialized string containing the fields that need updating to the Transaction.required_info_update column. The JSON string should be in the format returned from You can also optionally save a human-readable message to Transaction.required_info_message. Both fields will included in the /transaction response requested by the sending anchor.

If the SEP-31 transaction is waiting for an update, the sending anchor will eventually make a request to the PATCH /transaction endpoint with the information specified in Transaction.required_info_update. Once updated, this function will be called again with the updated transaction.

Parameters:transaction – the Transaction object associated with the payment this function should make
polaris.integrations.RailsIntegration.poll_outgoing_transactions(self, transactions: django.db.models.query.QuerySet) → List[polaris.models.Transaction]

Check the transactions that are still in a pending_external status and return the ones that are completed, meaning the user has received the funds.

Polaris will update the transactions returned to Transaction.STATUS.completed.

transactions is passed as a Django QuerySet in case there are many pending transactions. You may want to query batches of Transaction objects to avoid consuming large amounts of memory.

Parameters:transactions – a QuerySet of Transaction objects

Running the Service

In addition to the web server, SEP-31 requires three additional processes to be run in order to work. See the CLI Commands for more information on all Polaris commands.

Watch Transactions

Polaris’ watch_transactions command line tool streams transactions from every anchored asset’s distribution account and attempts to match every incoming stellar payment with a Transaction object created by the sending anchor’s POST /transaction request.

If it finds a match, it will update the transaction’s status to pending_receiver and update the Transaction.amount_in field with the amount actually sent in the stellar transaction.

Run the process like so:

python watch_transactions

Executing Outgoing Transactions

The execute_outgoing_transactions CLI tool polls the database for transactions in the pending_receiver status and passes them to the RailsIntegration.execute_outgoing_transaction() function for the anchor to initiate the payment to the receiving user. See the integration function’s documentation for more information about this step.

You can run the service like so:

python execute_outgoing_transactions --loop --interval 10

This process will continue indefinitely, calling the associated integration function, sleeping for 10 seconds, and then calling it again.

Poll Outgoing Transactions

And finally, once a payment to the user has been initiated by the anchor, this CLI tool periodically calls RailsIntegration.poll_outgoing_transactions() so the anchor can return the transactions that have have completed, meaning the user has received the funds.

If your banking or payment rails do not provide the necessary information to check if the user has received funds, do not run this process and simply mark each transaction as Transaction.STATUS.completed after initiating the payment in RailsIntegration.execute_outgoing_transaction().

Run the process like so:

python poll_outgoing_transactions --loop --interval 60