End-User Custodial Payout Orchestration

This tutorial walks you through how to orchestrate payouts using end-user custodial organizations and the end-user 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 end-user 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

You can also follow this Demo we built, which covers the full flow from account creation to payment execution and can be used as a reference.

Overview

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

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


1. Create an End-User Custodial Organization for your users

You must first create an Organization for your users. To create an Organization that supports end-user custodial execution of Payouts Requests, use the endUserCustodialIndividual or endUserCustodialBusiness 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": "endUserCustodialBusiness",
  "approvers": [
    {
      "name": "John Doe",
      "email": "[email protected]"
    }
  ],
  "businessName": "Sun Tree Capital, LLC",
  "email": "[email protected]"
}
'

This end-user 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 EndUserCustodialSDK.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/end-user-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/end-user-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/end-user-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>"
}
'