The VASP UMA Auth OpenAPI Schema

The VASP UMA Auth API schema is a RESTful API that the NWC image uses to communicate to the main VASP server. It uses the same sort of functionality that the VASP has already implemented for the UMA standard, so it's fairly easy to implement once you have UMA built.
You can find the VASP UMA Auth OpenAPI schema in the uma-auth-api GitHub repository. This same github repository also contains generated model libraries for Python, Go, Kotlin/Java, and TypeScript. Alternatively, can also generate your own libraries for whatever framework or language you're using from the schema itself. You can see the full list of OpenAPI generators here.
You'll need to implement the specified API endpoints in your VASP server to handle the NWC image requests. Technically, you do not need to support all of the endpoints in the schema, but the more you support, the more functionality you can provide to your users. At a minimum, your existing UMA VASP implementation will give you a simple starting point for implementing:
  • GET /receiver/lud16/{receiver_address}: This is equivalent to the lnurlp request in UMA.
  • GET /quote/lud16: This is equivalent to the payreq request in UMA.
  • POST /quote/{payment_hash}: This actually pays a quote that was previously generated.
  • POST /payments/lud16: This is all 3 of the above combined into one request. Note that with this function, you can only lock the sending side, not receiving side.
You also must support the following endpoints for the NWC image to work:
  • GET /info: Get information about the user's wallet connection, default currencies, etc.
  • GET /budget_estimate: Used to get a cost estimate in the user's default currency if they are making a payment in a different currency.
If you have BTC support and can send/recieve via Lightning for a user, you should also implement these to provide functionality for SATs-only NWC applications:
  • POST /invoice: Create a BOLT-11 invoice for a payment.
  • POST /payments/bolt11: Pay a BOLT-11 invoice.
  • POST /payments/keysend: Pay via keysend. This is slightly less critical to support than the other two.
These functions let applications query the user's balance and transactions. These are sensitive operations that require explicit permission from the user. They are not strictly necessary for you to support, but they are useful for applications that want to show the user's full balance or transaction history:
  • GET /balance: Get the user's balance in the specified currency (or default currency).
  • GET /invoices/{payment_hash}: Fetch an invoice by payment hash. Includes the payment status.
  • GET /transactions: Fetch the user's transaction history.
In general, supporting these endpoints usually just entails mapping the API request data type to your existing UMA implementation, and then mapping it back to the API response data type. The NWC image will handle the communication with the client application via NWC.
Here's a simple example of what the GET /receiver/lud16/{receiver_address} endpoint might look like in Python:
from flask import Flask, request, jsonify
# If importing the generated models libary (pip install uma_auth), you would import them like this:
from uma_auth.models.lookup_user_response import LookupUserResponse

app = Flask(__name__)

@app.route('/receiver/lud16/<receiver_address>', methods=['GET'])
def lookup_user(receiver_address):
    # Assuming you have a `sending_vasp` object that can handles the UMA lnurlp lookup:
    lookup_response = sending_vasp.handle_uma_lookup(receiver_uma)
    currencies = lookup_response.get("receiverCurrencies") or []
    return LookupUserResponse(
        currencies=[
            CurrencyPreference(
                currency=Currency(
                  code=currency.get("code"),
                  symbol=currency.get("symbol"),
                  decimals=currency.get("decimals"),
                  name=currency.get("name"),
                ),
                multiplier=currency.get("multiplier"),
                min=(
                    currency.get("convertible").get("min")
                    if "convertible" in currency
                    else currency.get("minSendable")
                ),
                max=(
                    currency.get("convertible").get("max")
                    if "convertible" in currency
                    else currency.get("maxSendable")
                ),
            )
            for currency in currencies
        ]
    ).to_dict()
The NWC docker image handles setting and tracking connection budgets for you, and will ensure that a given app connection does not exceed the budget set by the user. However, there is one aspect that needs to be handled by your main VASP server: currency conversion rates. When a user creates a new connection, their budget will be set in their default currency. If they are making a payment in a different currency, the NWC image will query your main VASP server for the current conversion rate so that the NWC docker image can deduct the right amount from the budget. For example, say a user's default currency is USD and their budget for a connection is $10 USD per month. If an app calls pay_invoice with a payment request for 0.001 BTC, the NWC image needs your main VASP server to tell it how much to deduct from the user's budget, since it's the one setting the exchange rate.
There are two places where you'll need to consider currency conversion for budgets - putting a hold on a budget amount before executing a payment, and then when the user actually makes the payment successfully.
Before a payment, if the budget currency is different from the payment currency, the NWC image will call the GET /budget_estimate endpoint to get a cost estimate in the user's default currency. This endpoint simply takes a sending currency code, an amount, and a budget currency code. You'll just need to return the estimated cost in the budget currency code. The NWC image will then deduct this amount (plus a configurable buffer) from the user's budget. Note that this is just an estimate, so it doesn't need to be exact. The exact amount will be deducted when the payment succeeds. If the payment fails, the budget will be refunded.
The second place you'll need to consider currency conversion is when the user actually completes a payment. At that point, if the budget currency is different from the payment currency, you will need to populate the total_budget_currency_amount field in the response to the NWC image. This field is present in the following calls:
  • POST /payments/lud16 (pay_to_address)
  • POST /quote/{payment_hash}(execute_quote)
  • POST /payments/bolt11 (pay_invoice)
  • POST /payments/keysend (pay_keysend)
For each of these calls, make sure that if the budget currency is different from the payment currency you populate the total_budget_currency_amount field with the amount that should be deducted from the user's budget. For each of these requests, the budget currency code for the given connection will be provided in the budget_currency_code field of the request.
All requests to the UMA Auth API should be authenticated using a JWT as described by the VASP Token Exchange guide. The NWC image will use the long-lived token associated with an NWC connection to authenticate to the API. The only exception is that it will occasionally use the short-lived token to authenticate when querying GET /info to get the currency list when showing the permission approval screen. You can add scopes to the JWTs to differentiate between these two tokens. You can even add scopes for each command permission that the user has approved if you want to get really granular with your permissions when checking the JWT in each API request. The JWT's sub claim will be your user's ID in your system, which you can use to authenticate them.
import jwt

def user_id_from_jwt(request):
    auth_header = request.headers.get("Authorization")
    if not auth_header:
        abort_with_error(401, "Unauthorized")
    jwt_token = auth_header.split("Bearer ")[-1]
    jwt_public_key = current_app.config.get("NWC_JWT_PUBKEY")
    if not jwt_public_key:
        abort_with_error(500, "JWT public key not configured")
    try:
        decoded = jwt.decode(
            jwt_token,
            key=jwt_public_key,
            algorithms=["ES256"],
            issuer="coolvasp.net",
            audience="coolvasp.net",
        )
        # Optionally check scopes here.
        return decoded.get("sub")
    except jwt.exceptions.InvalidTokenError as e:
        print("Invalid token error", e)
        abort_with_error(401, "Unauthorized")
Optionally, if you're hosting the NWC image in the same VPC as your VASP server, you can limit access to the API to only requests internal to your network. Only the NWC image should be making requests to this API, so there's no need to expose it to the public internet, unless you want to for debugging, or you want to allow general HTTP access to the API for other reasons.
Permissions in NWC and UMA Auth are tied explicitly to the commands that an application is allowed to execute on a wallet (e.g. pay_invoice, get_balance, etc.). When a user approves a connection, they are approving a set of commands that the application can execute. The NWC image will enforce these permissions when an application makes a request to the VASP server. It also enforces the budget set by the user for the connection, so you don't need to worry about that in your main VASP server.
However, for your convenience, the NWC image will include the approved commands when exchanging a short-lived token for a long-lived token. This way, you can add those commands as scopes in the JWT if you'd like, and then check those scopes in your API requests to ensure that the user has approved the command that the application is trying to execute. Again, this is optional, but will give you some piece of mind that tokens you create can only be used for the commands that the user has approved.
Here is the full mapping of NWC commands to UMA Auth API methods:
  • pay_invoice -> POST /payments/bolt11
  • pay_keysend -> POST /payments/keysend
  • make_invoice -> POST /invoice
  • get_balance -> GET /balance
  • get_invoices -> GET /invoices/{payment_hash}
  • get_transactions -> GET /transactions
  • lookup_user -> GET /receiver/lud16/{receiver_address}
  • fetch_quote -> GET /quote/lud16
  • execute_quote -> POST /quote/{payment_hash}
  • pay_to_address -> POST /payments/lud16
get_info and get_budget_estimate should generally always be allowed for any connection.