Guides

Non-Custodial Payout Orchestration

This tutorial walks you through how to orchestrate payouts using non-custodial organizations and the non-custodial payout flow. For more information on Mural's custody models, check out the Custody Models documentation.

Prerequisites

Before going through this tutorial, you should have general understanding of how to interface with the Mural API (see integration guide here)

To enable non-custodial payments, we provide an SDK that can be integrated into your app, allowing users to sign transactions securely and in a non-custodial way.

It can be installed via npm:

npm install @muralpay/browser-sdk

Overview

This guide will walk you through the full non-custodial payout flow using the NonCustodialSDK and the Mural API. We will cover:

  1. Creation of a Non-Custodial Organization
  2. Using the NonCustodialSDKin combination with the existing Mural API to orchestrate and execute a Payout from a Mural Account for your user


1. Create a Non-Custodial Organization for your users

You must first create an Organization for your users. To create an Organization that supports non-custodial execution of Payouts Requests, use the nonCustodialIndividual or nonCustodialBusiness type when creating the Organization.

📘

Note

Only Approvers provided during the Organization creation process are able to approve Payout Requests

curl --request POST \
     --url https://api.muralpay.com/api/organizations \
     --header 'accept: application/json' \
     --header 'authorization: Bearer $API_KEY' \
     --header 'content-type: application/json' \
     --data '
{
  "type": "nonCustodialBusiness",
  "approvers": [
    {
      "name": "John Doe",
      "email": "[email protected]"
    }
  ],
  "businessName": "Sun Tree Capital, LLC",
  "email": "[email protected]"
}
'

This non-custodial org that is being created will be used for the on-behalf-of of the next steps.


2. Create the SDK Instance

Mount an empty iframe in your app. Note, this iFrame will not be visible to your user. Initialize the SDK by calling createInstance.

Configuration

FieldTypeDescription
iframeContainerIdstringThe ID of the HTML element where the iframe will be mounted
iframeElementHTMLElementThe actual HTML element (must match the ID above)

HTML:

<div id="mural-auth-iframe-container-id"></div>

JavaScript:

const sdk = await NonCustodialSDK.createInstance({
  iframeContainerId: 'mural-auth-iframe-container-id',
  iframeElement: document.getElementById('mural-auth-iframe-container-id')
});

3. Trigger Authentication Challenge

Trigger an authentication challenge for the user by calling the Initiate Challenge endpoint. Note, this will send an email with a special code to the Approver.

curl --request POST \
     --url https://api.muralpay.com/api/approvers/non-custodial/initiate-challenge \
     --header 'accept: application/json' \
     --header 'on-behalf-of: $ORG_ID' \
     --header 'authorization: Bearer $API_KEY' \
     --header 'content-type: application/json' \
     --data '
{
  "publicKey": "04a82bfb0edb13d...",
  "approverId": "user123"
}
'

Payload:

  • publicKey — Use sdk.getPublicKey()
  • approverId — The approvers's ID
const publicKey = sdk.getPublicKey();

Response: Contains authenticatorId for use in next step.


4. Start Session with Verification Code

Once the user receives their email verification code, collect it and call startSession:

await sdk.startSession({
  code: $code-from-user-email
  authenticatorId: $authenticator-id-from-initiate-challenge
});

This creates a secure, authenticated session. You don’t need to call it again for each signing operation.
The SDK provides two helper functions, sdk.isSessionActive() and sdk.getClientSessionExpiry(), to check the session status and expiration.


5. Sign the Payout

Retrieve the payload to sign by calling the Get a Payout Request Body endpoint:

curl --request GET \
     --url https://api.muralpay.com/api/payouts/payout/non-custodial/body-to-sign/:id \
     --header 'accept: application/json' \
     --header 'on-behalf-of: $ORG_ID' \
     --header 'authorization: Bearer $API_KEY'
  • :id is the payout request ID

Then sign it using the SDK:

const signature = await sdk.signPayoutPayload(body); // use the exact body as returned

📘

Important

Do not alter the body returned by the API. Modifications will invalidate the signature.


5. Execute the Payout Request

Send the signature to the Execute the Payout endpoint.

curl --request POST \
     --url https://api.muralpay.com/api/payouts/payout/non-custodial/execute/:id \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --header 'on-behalf-of: $ORG_ID' \
     --header 'authorization: Bearer $API_KEY' \
     --data '
{
  "signature": "<signature-from-sdk>"
}
'