UMA Deposits and Withdrawals Integration Guide

Deposits and Withdrawals using the UMA (LNURL-p) standard on the Lightning Network including generating and sending to invoices.

Universal Money Address (UMA) is an open source project designed to make improvements over standard BOLT11 Lightning Network transactions.

These configurations will typically result in addresses taking one of the following formats:

  • $[email protected] [Default]
  • $satoshi@[platform].zerohash.id
  • $satoshi@uma.[platform].com
  • $satoshi@[platform].com

Note: CERT uses Testnet for non-UMA Lightning Network transactions and REGTEST for UMA transactions. UMA transactions will migrate to Testnet in CERT targeting mid-November 2024.


Transaction Limits and Controls (UMA)

The below tables lists the transaction limits in place for UMA Lightning Network transactions.

TransactionControl DescriptionControl
WithdrawalsTransaction Daily Maximum AmountN/A
WithdrawalsMinimum Transaction Amount1 satoshi
DepositsInvoice Maximum AmountN/A
DepositsInvoice Maximum FrequencyN/A

UMA Deposits

Register UMA Address

An invoice must be generated in order to receive a deposit over the Lightning Network. The customer can choose the username and the domain it is registered can take one of the forms listed above.

Once your UMA server is setup, a user must first be registered by your server and then sent to Zero Hash via POST /deposits/uma

{  
    "username": {UMA_ADDRESS}, // $username, see <https://uma.me> documentation  
    "participant_code": "{{participant_code}}"  
}

Confirm Creation of UMA Address

The UMA address creation can be confirmed by calling GET /deposits/uma
This call will return a list of created addresses.


Update or Change UMA Address

An UMA address can be changed after creation, such as in the case of a typo, by calling PATCH /deposits/uma

Request:

{  
    "new_username": "$scdgcr2",  
    "previous_username":"$scdgcr",  
    "participant_code": "3QX004"  
}

Success: 200

Error:
HTTP CODE 409 - conflict  
 {"message":"previous username does not match"}

Generate UMA Invoice

Once successfully registered, that platform can then generate an invoice by calling a LNURL request using a different Zero Hash URL depending on the environment (below example payloads use the CERT URL for illustration purposes):

  • CERT: uma.cert.zerohash.com/.well-known/lnurlp
  • PROD: uma.zerohash.com/.well-known/lnurlp

Note: The LNURL and PAYREQ must be sent from the onboarded Zero Hash platform. If the originating VASP is not the onboarded entity then these requests must be proxied to pass IP address / domain validation rules or else will be automatically rejected by default.

The LNURL request needs to follow the UMA documentation found here. A sample payload could look like the following:

GET uma.zerohash.com/.well-known/lnurlp/$bob?umaVersion=1.0&nonce=1234&vaspDomain=vasp1.com&signature=abcd&isSubjectToTravelRule=true×tamp=12345678

Note: The error message "UMA recipient not found" is returned if attempting to generate an invoice without registering the participant first.

The step following a successful LNURL request is to send the PayReq request with the generated invoice ID “lnurlp_callback”. The payreq call will look like the following:

POST uma.zerohash.com/api/payreq/<participant_code>

{  
   "amount":"1000.USD",  
   "payerData":{  
      "identifier":"$[[email protected]](mailto:[email protected])",  
      "email":"[[email protected]](mailto:[email protected])",  
      "name":"payer",  
      "compliance":{  
         "kycStatus":"VERIFIED",  
         "utxos":\[
           
            ],
     "nodePubKey":"",
     "signature":"",
     "signatureNonce":"",
     "signatureTimestamp":,
     "utxoCallback":""
  }
 },  
   "payeeData":{  
      "compliance":{  
         "mandatory":true  
      },  
      "identifier":{  
         "mandatory":true  
      },  
      "email":{  
         "mandatory":false  
      },  
      "name":{  
         "mandatory":false  
      }  
   },  
   "convert":"USD"  
}

The invoice is then returned and ready to be paid.


USD Funds

If your platform is configured to automatically offramp the received BTC to USD, the funds will sit in the participant’s account until they are debited by the Real-Time Payment process outlined in the next section.


The following sections review calls that can be made to confirm each step of the process.


Confirming the Deposit

The following command will return the deposits to a participant code

GET /deposits?participant_code={{participant_code}}

{  
    "message": [  
        {  
            "settle_timestamp": 1718813440365,  
            "account_id": "31206b3e-4305-4b74-bf23-3d1eaec3ae00",  
            "participant_code": "7D1QCQ",  
            "account_group": "UKYI3K",  
            "asset": "BTC",  
            "amount": "0.00001558",  
            "reference_id": "Invoice:01903143-4368-52d6-0000-07acc1700390",  
            "source": "[email protected]",  
            "received_address": "$testaddress",  
            "account_label": "general",  
            "movement_id": "93f778b2-3620-493e-8c44-4468ab3483e3", ‘movement_id will be set to the payment_hash of the invoice which originated that deposit.  
            "run_id": "3848133"  
        }  
    ],  
    "page": 1,  
    "page_size": 200,  
    "total_pages": 1,  
    "count": 1  
}

Confirming Trade Details (BTC to USD)

The following command will return the trades for a participant code

GET /trades?participant_code={{participant_code}}


Confirm Participant Balance Increase

The following command will return the trades for a participant code

GET /accounts?account_owner={{participant_code}}

{  
    "message": [  
        {  
            "asset": "USD",  
            "account_owner": "7D1QCQ",  
            "account_type": "available",  
            "account_group": "UKYI3K",  
            "account_label": "general",  
            "balance": "1",  
            "account_id": "1058112a-b1e4-4142-9fe2-47cd2284bb99",  
            "last_update": 1718813461541  
        }  
    ],  
    "page": 1,  
    "total_pages": 1  
}

Confirm Participant Transaction Movements

The following command will return the movement of both digital assets and fiat balances for a given participant code

GET /accounts/{{account_id}}/movements

{  
    "message": [  
        {  
            "asset": "USD",  
            "account_owner": "7D1QCQ",  
            "account_type": "available",  
            "account_group": "UKYI3K",  
            "account_label": "general",  
            "balance": "1",  
            "account_id": "1058112a-b1e4-4142-9fe2-47cd2284bb99",  
            "last_update": 1718813461541  
        }  
    ],  
    "page": 1,  
    "total_pages": 1  
}

Transaction Limits and Controls (UMA)

UMA deposits and withdrawals are only available with VASPs that have completed Zero Hash’s Compliance UMA Due Diligence and have been added to the allowlist as part of that process. UMA transactions to/from external VASPs will be blocked until they are part of the allowlist.

The minimum allowed transaction amount using UMA is 1 satoshi and millisatoshis are not recognized because these are below the minimum precision amount.


ACH and Real-Time Payments (RTP)

Please see the separate guide for ACH and Real-time Payments that will cover steps to withdraw fiat funds from the participant account to an external bank account.


Error Codes & States

Please see the following table of error states and courses of action

ErrorZero Hash ActionPlatform Action
API to initiate offramp failsFail every htlc composing the invoice, funds revert to sender.
No error is currently returned in this case.
Sending VASP would need to retry transaction due to canceled invoice
API to initiate offramp succeeded but later payment status webhook comes back as payment failedZH notifies the platform via webhook of the failure. There is no automated retry of this step.Platform to retry POST /payments
When UMA address is incorrect → valid format $[email protected] uma-server responds with bad request response and a proper error message.
Resubmit with proper format
When UMA address is not found (non-existent)
uma-server responds with bad request response and a proper error message.
When any of the following query params is not sent:

- umaVersion * only version 1.0 will be supported for now
- nonce
- vaspDomain
- signature
- isSubjectToTravelRule
- Timestamp
uma-server responds with a bad request response and a proper error message.
When request goes OKuma-server responds with StatusOK and the following data
preferred currency
estimated exchange rate
compliance data
vasp status: (should always be verified)
When request sends a version other than 1.0 (not supported)uma-server responds with a bad request response and a proper error message.
Requesting the callback url multiple timesShould not be valid, resulting in bad request the second time the same callback url is used.
Sending payReq with not mandatory payer infoShould result in invalid request
receivingCurrencyCode is set to any value other than USD.When sender-vasp sends a payreq with receivingCurrencyCode set to MSat, it should result in a rejection
Conversion is OK according to payreq response.The conversion rate of the payreq response should be OK. Example:
Request: {
SendingCurrency: USD
ReceivingCurrency: USD
Amount: 10000 // means a rfq by total of $100 USD Notional
Response:
invoice with amount in MSats.
Check if the value actually corresponds to 100 USD given the conversion rate exposed in the response
Verify responseOnly USD, BTC and SAT should be supported currencies


UMA Withdrawals


See supported currencies

To see what currencies the destination UMA address supports, use the GET /withdrawals/umalookup/{address}.

Response

{
  "currencies":[
    {
      "code":"USD",
      "symbol":"$",
      "name":"US Dollars",
      "multiplier":"1.6129",
      "decimals":2,
      "min":"1",
      "max":"1000"
    },
    {
      "code":"SAT",
      "symbol":"B",
      "name":"Satoshis",
      "multiplier":"1",
      "decimals":0,
      "min":"1",
      "max":"100000000"
    }
  ]
}

Initiate an LNURL and PayReq

Use the POST /withdrawals/uma endpoint to initiate the LNURL and PayReq requests with the Receiving VASP. If successful, returns a BOLT11 invoice that can be used through existing /withdrawals/requests and /convert_withdraw endpoints to execute the payment.

POST /withdrawals/uma

{  
    "participant_code":"D3SCYB",
    "recipient_uma_address":"[email protected]",
    "amount_sats":"1102", 
    "receiving_currency":"USD",
    "amount_receiving_currency":"",
    "client_withdrawal_request_id":""
}

Note that either amount_sats needs to be provided, or both receiving_currency and amount_receiving_currency. All 3 parameters cannot be provided together.


Response

{  
    "participant_code":"D3SCYB",
    "platform_code": "JDNQGG",
    "sender_uma_address":"[email protected]",
    "recipient_uma_address":"[email protected]",
    "receiving_currency":"USD",
    "amount_sats":"1102",
    "sender_fee_sats":"179",
    "amount_receiving_currency":"551",
    "receiving_currency_multiplier":"2.000000000",
    "receiver_exchange_fee_sats":"100",
    "decimals":2,
    "invoice":"lnbcrt...nvrx9"
}

Note in the above response that receiver_exchange_fee_sats is the number of sats charged by the receiving VASP.


The provided Lightning Network invoice (shortened in the above example) can then be used to perform a withdrawal or convert withdrawal operation (see BOLT11 withdrawals guide).



Fund an UMA Withdrawal using ACH/RTP

If your platform is approved for ACH/RTP flows, then the above UMA steps to create a BOLT11 invoice need to be performed. For more information on ACH/RTP capabilities more generally please refer to this guide.


Initiate the ACH on demand

At the conclusion of these steps, USD will be onramped to BTC and will appear in the participant account.

  • POST /payments/rfq
    • Make sure to have a quote_expiry of at least 30s.
    • This is necessary because we perform a balance check on the user's bank account, and that operation can take some time to complete
  • POST /payments/execute

Generate the UMA invoice
Use the UMA withdrawal endpoints to generate an invoice

  • Call GET /withdrawals/umalookup/{pcode}
  • Call POST /withdrawals/uma

Calculate the withdrawal fee

  • Call GET /withdrawals/locked_network_fee and pass the invoice as withdrawal_address
    • Use max_amount = true instead of passing an amount. This will send the max BTC after adjusting for the Lightning Network fee.
  • A withdrawal_quote_id will be generated

Execute the withdrawal

  • Call POST /withdrawals/execute with the withdrawal_quote_id from the prior step

After the ACH settles

  • The platform float will be credited once the ACH funds settle