Payreq Request

The next request in the protocol is the payreq request, which corresponds to the second request of LUD-06, but with some minor changes. This request exchanges compliance data and tells the receiving VASP to generate an invoice for a specified amount on behalf of the receiving user.
First, the sending VASP will need to retrieve the receiving VASP's public keys as shown in the “Fetching Public Keys and Verifying Signatures” section. The encryption public key will be used to encrypt Travel Rule information, if the sending VASP is sending that information.
vasp2_pubkeys = uma.fetch_public_key_for_vasp(initial_request_data.vasp_domain, pubkey_cache)
Then, the sending VASP needs to get the payer info corresponding to the required payer data specified in the LnurlpResponse received from the receiving VASP.
payer_data_options = lnurlp_response.required_payer_data

# NOTE: In a real application, you'd want to use the authentication context to
# pull out this information.

# It's not actually always Alice sending the money ;-).

# The identifier is the sender's UMA.

sender_identifier = "$alice@vasp1.com"

sender_name = "Alice FakeName" if payer_data_options.name_required else None

sender_email = "alicefakename123@vasp1.com" if payer_data_options.email_required else None

if payer_data_options["compliance"]["mandatory"]:
  # The travel rule info should be a json-encoded string. Its contents depend
  # on your geographic region and legal requirements. If this transaction is not
  # subject to the travel rule, this can be null or an empty string. This
  # function is implementation-specific and need to be written by you.
  if lnurl_response.compliance.is_subject_to_travel_rule: 
    travel_rule_info = generate_travel_rule_info(sender_identifier, receiver_address)
  else:
    travel_rule_info = None

  # If you are using a standardized travel rule format, you can set this to
  # something like "IVMS@101.2023".
  travel_rule_format = None

  # The sender's utxos might be used by the receiver to pre-screen the transaction
  # with their compliance provider. This function is node-implementation-specific
  # and needs to be written by you.
  sender_utxos = get_node_utxos(sender_identifier)

  # If known, the public key of the sender's node. If supported by the receiving
  # VASP's compliance provider, this will be used to pre-screen the sender's
  # UTXOs for compliance purposes. This function is implementation-specific and
  # needs to be written by you.
  payer_node_pubkey = get_node_pubkey(sender_identifier)

  # This callback is the URL that the receiver will call to send UTXOs of the
  # channel that the receiver used to receive the payment once it completes. See
  # the "Sending the Payment & Post-transaction Hooks" section for more info. The
  # transactionId here is an optional example in case you might generate if you
  # want to track the transaction which has completed.
  utxo_callback := "https://vasp1.com/api/uma/utxocallback?txid=" + transaction_id

  sender_compliance_data = uma.create_compliance_payer_data(
    receiver_encryption_pubkey=vasp2_encryption_pubkey,
    signing_private_key=signing_private_key,
    payer_identifier=sender_identifier,
    travel_rule_info=travel_rule_info,
    travel_rule_format=travel_rule_format,
    payer_kyc_status=uma.KycStatus.VERIFIED,
    payer_utxos=sender_utxos,
    payer_node_pubkey=sender_node_pubkey,
    utxo_callback=utxo_callback,
  )
else:
  sender_compliance_data = None

Now let's build the request object:
# If you'd instead like to lock the amount to msats so that the sending amount
# is fixed, set this to False and specify the amount in msats rather than the
# receiving currency.
is_amount_in_receiving_currency = True

# If the lnurlpResponse agreed on a different UMA version, you can parse it here
# and use it to create the pay request.
uma_version = initial_request_data.lnurlp_response.uma_version
if uma_version is not None:
    uma_version = ParsedVersion.load(uma_version).major

requested_payee_data = create_counterparty_data_options(
    {
        "compliance": True,
        "identifier": True,
        "email": False,
        "name": False,
    }
)

pay_request = uma.create_pay_request(
  receiving_currency_code=receiving_currency_code,
  is_amount_in_receiving_currency=is_amount_in_receiving_currency,
  amount=amount,
  payer_identifier=sender_identifier,
  payer_name=sender_name,
  payer_email=sender_email,
  payer_compliance=payer_compliance,
  requested_payee_data=requested_payee_data,
  uma_major_version=uma_version if uma_version is not None else 1,
)
The travelRuleInfo passed here will be encrypted using the receiving VASP's encryptionPubKey. That way, the receiving VASP can store the encrypted travel rule info blob and only decrypt it when needed.
Retrieving the nodePubKey and sendingChannelUtxos is node-implementation-specific. See your node or Lightning provider's documentation for more information on how to retrieve these values.
We've now created a signed PayRequest object that can be serialized to JSON and sent as the request body to the URL specified in the callback field of the LnurlpResponse previously received from the receiving VASP.
import requests

pay_request_response = requests.post(
  url=lnurlp_response.callback,
  json=pay_request.to_dict(),
  timeout=20,
)
IMPORTANT NOTE: This request is a POST request rather than a GET like in the LNURL specification. UMA requires this to be a POST to allow for a longer request body than what is supported with query parameters. Specifically, the Travel Rule data might be of arbitrary length.
The receiving VASP can parse the incoming payreq request by handling a route for POST requests at whatever callback URL was chosen and returned in the LnurlpResponse. Normally, the callback URL will include either a receiving user ID or a transaction ID so that the receiving VASP can construct the response. To parse the request object from the raw request body:
pay_request = uma.parse_pay_request(http_request_body)
The parsed PayRequest object has the structure:
@dataclass
class PayRequest:
    sending_amount_currency_code: Optional[str]
    """
    The currency code of the `amount` field. `None` indicates that `amount` is in
    millisatoshis as in LNURL without LUD-21. If this is not `None`, then `amount`
    is in the smallest unit of the specified currency (e.g. cents for USD). This
    currency code can be any currency which the receiver can quote. However, there
    are two most common scenarios for UMA:

    1. If the sender wants the receiver wants to receive a specific amount in their
    receiving currency, then this field should be the same as
    `receiving_currency_code`. This is useful for cases where the sender wants to
    ensure that the receiver receives a specific amount in that destination currency,
    regardless of the exchange rate, for example, when paying for some goods or
    services in a foreign currency.

    2. If the sender has a specific amount in their own currency that they would
    like to send, then this field should be left as `None` to indicate that the
    amount is in millisatoshis. This will lock the sent amount on the sender side,
    and the receiver will receive the equivalent amount in their receiving currency.
    NOTE: In this scenario, the sending VASP *should not* pass the sending currency
    code here, as it is not relevant to the receiver. Rather, by specifying an
    invoice amount in msats, the sending VASP can ensure that their user will be
    sending a fixed amount, regardless of the exchange rate on the receiving side.
    """

    receiving_currency_code: Optional[str]
    """
    The currency code for the currency that the receiver will receive for this
    payment.
    """

    amount: int
    """
    The amount of the payment in the currency specified by `currency_code`.
    This amount is in the smallest unit of the specified currency
    (e.g. cents for USD).
    """

    payer_data: Optional[PayerData]
    """
    The data about the payer that the sending VASP must provide in order to
    send a payment. This was requested by the receiver in the lnulp response.
    See LUD-18.
    """

    requested_payee_data: Optional[CounterpartyDataOptions]
    """
    The data about the receiver that the sending VASP would like to know from
    the receiver. See LUD-22.
    """

    comment: Optional[str] = None
    """
    A comment that the sender would like to include with the payment. This can
    only be included if the receiver included the `commentAllowed` field in the
    lnurlp response. The length of the comment must be less than or equal to the
    value of `commentAllowed`.
    """

    uma_major_version: Optional[int] = MAJOR_VERSION
    """
    The major version of the UMA protocol that this currency adheres to. This is
    not serialized to JSON.
    """
PayerData includes sender metadata, along with the compliance info provided by the sending VASP (KYC status, encrypted Travel Rule info, utxo data, the sending node public key, and a signature). Note that the “KYC status” is meant to permit an UMA Participant that offers services that may not require KYC for all customers to reveal that its user has a KYC credential (KycStatus=VERIFIED) or not (KycStatus=NOT_VERIFIED). For VASPs offering custodial services only, this field may not always be relevant.
You can then fetch the sending VASP's public key from the cache and verify the signature on the request:
sending_vasp_domain = uma.get_vasp_domain_from_uma_address(pay_request.payer_data.identifier)
if pay_request.is_uma_request():
    pubkey = uma.fetch_public_key_for_vasp(sending_vasp_domain, pubkey_cache)
    uma.verify_pay_request_signature(pay_request, sending_vasp_pubkey_response, nonce_cache)
    # Successfully verified the signature!
Next, the receiving VASP will construct and respond with the payreq response.