Repository: XRPLF/xrpl-dev-portal
Pull request #3030: Verify credential tutorial (Python)
Author: login: mDuo13
Labels: name: new topic
description: Describes a detailed document to be added or expanded in detail
name: needs japanese translation
description: PRs with English changes that need to be added/updated in the Japanese translation of the site
Created: 2025-03-08T00:19:38Z | Merged: 2025-03-21T17:45:38Z

Companion tutorial to #2960 based on feedback. Demonstrates how to verify that a credential exists and is valid.

**Preview link:** https://xrpl-dev-portal--verify-cred.preview.redocly.app/docs/tutorials/python/compliance/verify-credential

These steps are _not_ necessary for DepositAuth or many of other forthcoming the use cases that use Credentials as part of Permissioned Domains to grant access to things like Permissioned DEXes, MPTs, etc—because, in those cases, the ledger performs credential verification automatically. However, there are other use cases for Credentials in which you might want to verify them like this.

If you want to create additional credentials for testing with this, you can run the code from #2960.

I expect (someone) to make a separate JavaScript version with a web interface (possibly as a Dev Tool also) eventually. To make the review process easier, I did not try to include both languages in one PR.


Diff:
diff --git a/_code-samples/verify-credential/README.md b/_code-samples/verify-credential/README.md
new file mode 100644
index 00000000000..f539da000d0
--- /dev/null
+++ b/_code-samples/verify-credential/README.md
@@ -0,0 +1,3 @@
+# Verify Credential
+
+Check whether a specific account holds a specific credential, and the credential is currently valid.
diff --git a/_code-samples/verify-credential/py/README.md b/_code-samples/verify-credential/py/README.md
new file mode 100644
index 00000000000..3312e9727d8
--- /dev/null
+++ b/_code-samples/verify-credential/py/README.md
@@ -0,0 +1,33 @@
+# Verify Credential - Python sample code
+
+Verifies that a specific credential exists on the XRPL and is valid.
+
+Quick install & usage:
+
+```sh
+python -m venv .venv
+source .venv/bin/activate
+pip install -r requirements.txt
+./verify_credential.py
+```
+
+`verify_credential.py` can also be used as a commandline utility. Full usage statement:
+
+```sh
+$ ./verify_credential.py -h
+usage: verify_credential.py [-h] [-b] [-n {devnet,testnet,mainnet}]
+                            [issuer] [subject] [credential_type]
+
+Verify an XRPL credential
+
+positional arguments:
+  issuer                Credential issuer address as base58.
+  subject               Credential subject (holder) address as base58.
+  credential_type       Credential type as string
+
+options:
+  -h, --help            show this help message and exit
+  -b, --binary          Use binary (hexadecimal) for credential_type
+  -n, --network {devnet,testnet,mainnet}
+                        Use the specified network for lookup
+```
diff --git a/_code-samples/verify-credential/py/requirements.txt b/_code-samples/verify-credential/py/requirements.txt
new file mode 100644
index 00000000000..e18375ef2ea
--- /dev/null
+++ b/_code-samples/verify-credential/py/requirements.txt
@@ -0,0 +1 @@
+xrpl-py==4.0.0
diff --git a/_code-samples/verify-credential/py/verify_credential.py b/_code-samples/verify-credential/py/verify_credential.py
new file mode 100755
index 00000000000..bca48ce411b
--- /dev/null
+++ b/_code-samples/verify-credential/py/verify_credential.py
@@ -0,0 +1,167 @@
+#!/usr/bin/env python
+
+import argparse
+import logging
+import sys
+from binascii import hexlify
+from re import match
+
+from xrpl.clients import JsonRpcClient
+from xrpl.models.requests import LedgerEntry, Ledger
+from xrpl.utils import ripple_time_to_datetime
+
+# Set up logging --------------------------------------------------------------
+# Use WARNING by default in case verify_credential is called from elsewhere.
+logger = logging.getLogger("verify_credential")
+logger.setLevel(logging.WARNING)
+logger.addHandler(logging.StreamHandler(sys.stderr))
+
+# Define an error to throw when XRPL lookup fails unexpectedly ----------------
+class XRPLLookupError(Exception):
+    def __init__(self, xrpl_response):
+        self.body = xrpl_response.result
+
+# Main function ---------------------------------------------------------------
+def verify_credential(client:JsonRpcClient, 
+                      issuer:str, 
+                      subject:str, 
+                      credential_type:str="", 
+                      credential_type_hex:str=""):
+    """
+    Check whether an XRPL account holds a specified credential,
+    as of the most recently validated ledger.
+
+    Paramters:
+        client - JsonRpcClient for the XRPL network to use.
+        issuer - Address of the credential issuer, in base58
+        subject - Address of the credential holder/subject, in base58
+        credential_type - Credential type to check for as a string,
+                          which will be encoded as UTF-8 (1-64 bytes long).
+        credential_type_hex - Credential type (binary) as hexadecimal.
+        verbose - If true, print details to stdout during lookup.
+    You must provide either credential_type or credential_type_hex.
+
+    Returns True if the account holds the specified, valid credential.
+    Returns False if the credential is missing, expired, or not accepted.
+    """
+    # Handle function inputs --------------------------------------------------
+    if not (credential_type or credential_type_hex):
+        raise ValueError("Provide a non-empty credential_type or " +
+                         "credential_type_hex")
+    if credential_type and credential_type_hex:
+        raise ValueError("Provide either credential_type or " +
+                         "credential_type_hex, but not both")
+    
+    # Encode credential_type as uppercase hex, if needed
+    if credential_type:
+        credential_type_hex = hexlify(credential_type.encode("utf-8")
+                                      ).decode("ascii")
+        logger.info("Encoded credential_type as hex: "+credential_type_hex.upper())
+    credential_type_hex = credential_type_hex.upper()
+
+    if len(credential_type_hex) % 2 or \
+            not match(r"[0-9A-F]{2,128}", credential_type_hex):
+        # Hexadecimal is always 2 chars per byte, so an odd length is invalid.
+        raise ValueError("credential_type_hex must be 1-64 bytes as hexadecimal.")
+
+    # Perform XRPL lookup of Credential ledger entry --------------------------
+    ledger_entry_request = LedgerEntry(
+        credential={
+            "subject": subject,
+            "issuer": issuer,
+            "credential_type": credential_type_hex
+        },
+        ledger_index="validated"
+    )
+    logger.info("Looking up credential...")
+    logger.info(ledger_entry_request.to_dict())
+    xrpl_response = client.request(ledger_entry_request)
+
+    if xrpl_response.status != "success":
+        if xrpl_response.result["error"] == "entryNotFound":
+            logger.info("Credential was not found")
+            return False
+        # Other errors, for example invalidly-specified addresses.
+        raise XRPLLookupError(xrpl_response)
+
+    credential = xrpl_response.result["node"]
+    logger.info("Found credential:")
+    logger.info(credential)
+
+    # Confirm that the credential has been accepted ---------------------------
+    lsfAccepted = 0x00010000
+    if not credential["Flags"] & lsfAccepted:
+        logger.info("Credential is not accepted.")
+        return False
+    
+    # Confirm that the credential is not expired ------------------------------
+    if credential.get("Expiration"):
+        expiration_time = ripple_time_to_datetime(credential["Expiration"])
+        logger.info("Credential has expiration: "+expiration_time.isoformat())
+        logger.info("Looking up validated ledger to check for expiration.")
+
+        ledger_response = client.request(Ledger(ledger_index="validated"))
+        if ledger_response.status != "success":
+            raise XRPLLookupError(ledger_response)
+        close_time = ripple_time_to_datetime(
+                ledger_response.result["ledger"]["close_time"]
+        )
+        logger.info("Most recent validated ledger is: "+close_time.isoformat())
+
+        if close_time > expiration_time:
+            logger.info("Credential is expired.")
+            return False
+
+    # Credential has passed all checks. ---------------------------------------
+    logger.info("Credential is valid.")
+    return True
+
+# Commandline usage -----------------------------------------------------------
+if __name__=="__main__":
+    NETWORKS = {
+        # JSON-RPC URLs of public servers
+        "devnet": "https://s.devnet.rippletest.net:51234/",
+        "testnet": "https://s.altnet.rippletest.net:51234/",
+        "mainnet": "https://xrplcluster.com/"
+    }
+
+    # Parse arguments ---------------------------------------------------------
+    parser = argparse.ArgumentParser(description="Verify an XRPL credential")
+    parser.add_argument("issuer", type=str, nargs="?",
+                        help="Credential issuer address as base58.",
+                        default="rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS")
+    parser.add_argument("subject", type=str, nargs="?",
+                        help="Credential subject (holder) address as base58.",
+                        default="rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA")
+    parser.add_argument("credential_type", type=str, nargs="?",
+                        help="Credential type as string", 
+                        default="my_credential")
+    parser.add_argument("-b", "--binary", action="store_true",
+                        help="Use binary (hexadecimal) for credential_type")
+    parser.add_argument("-n", "--network", choices=NETWORKS.keys(),
+                        help="Use the specified network for lookup",
+                        default="devnet")
+    parser.add_argument("-q", "--quiet", action="store_true",
+                        help="Don't print log messages.")
+    args = parser.parse_args()
+
+    # Call verify_credential with appropriate args ----------------------------
+    client = JsonRpcClient(NETWORKS[args.network])
+    if not args.quiet:
+        # Use INFO level by default when called from the commandline.
+        logger.setLevel(logging.INFO)
+
+    if args.binary:
+        result = verify_credential(client,
+                          issuer=args.issuer,
+                          subject=args.subject,
+                          credential_type_hex=args.credential_type)
+    else:
+        result = verify_credential(client,
+                          issuer=args.issuer,
+                          subject=args.subject,
+                          credential_type=args.credential_type)
+    
+    # Return a nonzero exit code if credential verification failed. -----------
+    if not result:
+        exit(1)
diff --git a/docs/tutorials/python/build-apps/credential-issuing-service.md b/docs/tutorials/python/build-apps/credential-issuing-service.md
index 9815e835302..37c02dd725b 100644
--- a/docs/tutorials/python/build-apps/credential-issuing-service.md
+++ b/docs/tutorials/python/build-apps/credential-issuing-service.md
@@ -335,4 +335,16 @@ Finally, the `CredentialRequest` class inherits from the `Credential` class but
 
 {% code-snippet file="/_code-samples/issue-credentials/py/credential_model.py" language="py" from="class CredentialRequest(Credential):" /%}
 
+## Next Steps
+
+Using this service as a base, you can extend the service with more features, such as:
+
+- Security/authentication to protect API methods from unauthorized use.
+- Actually checking user documents to decide if you should issue a credential.
+
+Alternatively, you can use credentials to for various purposes, such as:
+
+- Define a [Permissioned Domain](/docs/concepts/tokens/decentralized-exchange/permissioned-domains) that uses your credentials to grant access to features on the XRP Ledger.
+- [Verify credentials](../compliance/verify-credential.md) manually to grant access to services that exist off-ledger.
+
 {% raw-partial file="/docs/_snippets/common-links.md" /%}
diff --git a/docs/tutorials/python/compliance/index.md b/docs/tutorials/python/compliance/index.md
new file mode 100644
index 00000000000..8a558e973ba
--- /dev/null
+++ b/docs/tutorials/python/compliance/index.md
@@ -0,0 +1,13 @@
+---
+metadata:
+  indexPage: true
+seo:
+    description: Transact with confidence using the XRP Ledger's suite of compliance features for following government regulations and security practices.
+---
+# Transact with Confidence Using Compliance Features
+
+The XRP Ledger has a rich suite of features designed to help financial institutions of all sizes engage with DeFi technology while complying with government regulations domestically and internationally.
+
+See the following tutorials for examples of how to put these features to work:
+
+{% child-pages /%}
diff --git a/docs/tutorials/python/compliance/verify-credential.md b/docs/tutorials/python/compliance/verify-credential.md
new file mode 100644
index 00000000000..721492d6df4
--- /dev/null
+++ b/docs/tutorials/python/compliance/verify-credential.md
@@ -0,0 +1,221 @@
+---
+seo:
+    description: Verify that an account holds a valid credential on the XRP Ledger.
+labels:
+    - Credentials
+---
+# Verify Credentials in Python
+
+This tutorial describes how to verify that an account holds a valid [credential](/docs/concepts/decentralized-storage/credentials) on the XRP Ledger, which has different use cases depending on the type of credential and the meaning behind it. A few possible reasons to verify a credential include:
+
+- Confirming that a recipient has passed a background check before sending a payment.
+- Checking a person's professional certifications, after verifying their identity with a [DID](/docs/concepts/decentralized-storage/decentralized-identifiers).
+- Displaying a player's achievements in a blockchain-connected game.
+
+This tutorial uses sample code in Python using the [xrpl-py library](../index.md).
+
+## Prerequisites
+
+- You must have Python installed and know how to run Python code from the command line. Python 3.8 or later is required for xrpl-py.
+- You should have a basic understanding of the XRP Ledger.
+- The credential you want to verify should exist in the ledger already, and you should know the addresses of both the issuer and the holder, as well as the official credential type you want to check.
+     - For sample code showing how to create credentials, see [Build a Credential Issuing Service](../build-apps/credential-issuing-service.md).
+
+## Setup
+
+First, download the complete sample code for this tutorial from GitHub:
+
+- {% repo-link path="_code-samples/verify-credential/py/" %}Verify Credential sample code{% /repo-link %}
+
+Then, in the appropriate directory, set up a virtual environment and install dependencies:
+
+```sh
+python -m venv .venv
+source .venv/bin/activate
+pip install -r requirements.txt
+```
+
+This installs the appropriate version of the `xrpl-py` library. There are no other dependencies for this tutorial outside of the Python standard library.
+
+## Overview
+
+The Verify Credential sample code consists of one file, `verify_credential.py`, and contains two main parts:
+
+1. A function, `verify_credential(...)` which can be called with appropriate arguments to verify that a credential exists and is valid. This function can be imported into other code to be used as part of a larger application.
+2. A commandline utility that can be used to verify any credential.
+
+## Usage
+
+To test that you have the code installed and working properly, you can run the commandline utility with no arguments to check the existence of a default credential on Devnet, such as:
+
+```sh
+python verify_credential.py
+```
+
+If all goes well, you should see output such as the following:
+
+```text
+Encoded credential_type as hex: 6D795F63726564656E7469616C
+Looking up credential...
+{'ledger_index': 'validated', 'method': 'ledger_entry', 'api_version': 2, 'credential': {'subject': 'rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA', 'issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'credential_type': '6D795F63726564656E7469616C'}, 'binary': False}
+Found credential:
+{'CredentialType': '6D795F63726564656E7469616C', 'Flags': 65536, 'Issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'IssuerNode': '0', 'LedgerEntryType': 'Credential', 'PreviousTxnID': '7D1257779E2D298C07C7E0C73CD446534B143FBD1F13DB268A878E40FD153B9A', 'PreviousTxnLgrSeq': 803275, 'Subject': 'rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA', 'SubjectNode': '0', 'index': '9603F0E204A8B1C61823625682EB0ECE98A4ECF22FF46CD4845FA9BFA3606B24'}
+Credential is valid.
+```
+
+If the code reports that the credential was not found when called with no arguments, it's possible that the example credential has been deleted or the Devnet has been reset. If you have another credential you can verify, you can provide the details as commandline arguments. For example:
+
+```sh
+python verify_credential.py rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA my_credential
+```
+
+A full usage statement is available with the `-h` flag.
+
+### Interactive Shell
+
+You can open an interactive python shell and import the `verify_credential` function, as in the following example:
+
+```py
+>>> from verify_credential import verify_credential
+>>> from xrpl.clients import JsonRpcClient
+>>> client = JsonRpcClient("https://s.devnet.rippletest.net:51234/")
+>>> verify_credential(client, issuer="rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS", subject="rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA", credential_type="my_credential")
+True
+```
+
+You can import the `verify_credential(...)` function into other scripts and use it the same way.
+
+### Other Examples
+
+The following examples show other possible scenarios. The data for these examples may or may not still be present in Devnet. For example, anyone can delete an expired credential.
+
+{% tabs %}
+{% tab label="Valid with Expiration" %}
+```text
+$ ./verify_credential.py rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S long_lasting_credential
+
+Encoded credential_type as hex: 6C6F6E675F6C617374696E675F63726564656E7469616C
+Looking up credential...
+{'ledger_index': 'validated', 'method': 'ledger_entry', 'api_version': 2, 'credential': {'subject': 'rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S', 'issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'credential_type': '6C6F6E675F6C617374696E675F63726564656E7469616C'}, 'binary': False}
+Found credential:
+{'CredentialType': '6C6F6E675F6C617374696E675F63726564656E7469616C', 'Expiration': 1167724800, 'Flags': 65536, 'Issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'IssuerNode': '0', 'LedgerEntryType': 'Credential', 'PreviousTxnID': 'C65794B7C322F028DB0D2DD72C9FF69D53A676B1608B77ADEF22311AFB22BFF7', 'PreviousTxnLgrSeq': 996934, 'Subject': 'rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S', 'SubjectNode': '0', 'index': 'FC4BB495DAE7C9F4615174188B3C5F2E337680017BA90E1F126DE08CAD15FD66'}
+Credential has expiration: 2037-01-01T08:00:00+00:00
+Looking up validated ledger to check for expiration.
+Most recent validated ledger is: 2025-03-11T20:01:51+00:00
+Credential is valid.
+```
+{% /tab %}
+
+{% tab label="Expired" %}
+```text
+$ ./verify_credential.py rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S expiring_credential
+
+Encoded credential_type as hex: 6578706972696E675F63726564656E7469616C
+Looking up credential...
+{'ledger_index': 'validated', 'method': 'ledger_entry', 'api_version': 2, 'credential': {'subject': 'rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S', 'issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'credential_type': '6578706972696E675F63726564656E7469616C'}, 'binary': False}
+Found credential:
+{'CredentialType': '6578706972696E675F63726564656E7469616C', 'Expiration': 795038400, 'Flags': 65536, 'Issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'IssuerNode': '0', 'LedgerEntryType': 'Credential', 'PreviousTxnID': 'E497F1EFE2E198EDED0D94ADDEE4CEFACDDC3B1674133A0123C765F8061B9600', 'PreviousTxnLgrSeq': 997087, 'Subject': 'rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S', 'SubjectNode': '0', 'index': 'F3A9475871E7BA994E257732D0C7CB0B91CACBBB9F840BDFA6ABABD6F71454CD'}
+Credential has expiration: 2025-03-11T20:00:00+00:00
+Looking up validated ledger to check for expiration.
+Most recent validated ledger is: 2025-03-11T20:02:03+00:00
+Credential is expired.
+```
+{% /tab %}
+
+{% tab label="Unaccepted" %}
+```text
+$ ./verify_credential.py rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S unaccepted_credential
+
+Encoded credential_type as hex: 756E61636365707465645F63726564656E7469616C
+Looking up credential...
+{'ledger_index': 'validated', 'method': 'ledger_entry', 'api_version': 2, 'credential': {'subject': 'rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S', 'issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'credential_type': '756E61636365707465645F63726564656E7469616C'}, 'binary': False}
+Found credential:
+{'CredentialType': '756E61636365707465645F63726564656E7469616C', 'Flags': 0, 'Issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'IssuerNode': '0', 'LedgerEntryType': 'Credential', 'PreviousTxnID': '59DB4B17E5552AB1CA1E2A89F5C03E51C2ACD0D293955FA701AE4A1801E94C96', 'PreviousTxnLgrSeq': 997107, 'Subject': 'rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S', 'SubjectNode': '0', 'index': '8E5AD9444D566BE5C6F87C94D696139CEEE43ACB9A96137A59C003B48DF565C6'}
+Credential is not accepted.
+```
+{% /tab %}
+{% tab label="Hexadecimal Credential Type" %}
+```text
+$ ./verify_credential.py rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA 6D795F63726564656E7469616C -b
+
+Looking up credential...
+{'ledger_index': 'validated', 'method': 'ledger_entry', 'api_version': 2, 'credential': {'subject': 'rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA', 'issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'credential_type': '6D795F63726564656E7469616C'}, 'binary': False}
+Found credential:
+{'CredentialType': '6D795F63726564656E7469616C', 'Flags': 65536, 'Issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'IssuerNode': '0', 'LedgerEntryType': 'Credential', 'PreviousTxnID': '7D1257779E2D298C07C7E0C73CD446534B143FBD1F13DB268A878E40FD153B9A', 'PreviousTxnLgrSeq': 803275, 'Subject': 'rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA', 'SubjectNode': '0', 'index': '9603F0E204A8B1C61823625682EB0ECE98A4ECF22FF46CD4845FA9BFA3606B24'}
+Credential is valid.
+```
+{% /tab %}
+{% /tabs %}
+
+
+## Code Walkthrough
+
+### 1. Initial setup
+
+The `verify_credential.py` file implements the code for this tutorial. 
+This file can be run as a commandline tool, so it starts with a [shebang](https://en.wikipedia.org/wiki/Shebang_(Unix)). Then it imports dependencies, with standard lib first and then specific parts of the `xrpl-py` library:
+
+{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" before="# Set up logging" /%}
+
+The next section of the code sets the default log level for messages that might be written to the console through the utility:
+
+{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Set up logging" before="# Define an error to throw" /%}
+
+Then it defines a type of exception to throw if something goes wrong when connecting to the XRP Ledger:
+
+{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Define an error to throw" before="# Main function" /%}
+
+### 2. Main function
+
+The `verify_credential(...)` function performs the main work for this tutorial. The function definition and docstring define the parameters:
+
+{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Main function" before="# Handle function inputs" /%}
+
+The first thing the function does is verify that the user provided a credential type in either the `credential_type` or `credential_type_hex` parameter. The XRP Ledger APIs require the credential type to be hexadecimal, so it converts the user input if necessary:
+
+{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Handle function inputs" before="# Perform XRPL lookup" /%}
+
+Next, it calls the [ledger_entry method](/docs/references/http-websocket-apis/public-api-methods/ledger-methods/ledger_entry#get-credential-entry) to look up the requested Credential ledger entry:
+
+{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Perform XRPL lookup" before="# Confirm that the credential has been accepted" /%}
+
+If it succeeds in finding the credential, the function continues by checking that the credential has been accepted by its holder. Since anyone can issue a credential to anyone else, a credential is only considered valid if its subject has accepted it.
+
+{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Confirm that the credential has been accepted" before="# Confirm that the credential is not expired" /%}
+
+Then, if the credential has an expiration time, the function checks that the credential is not expired. If the credential has no expiration, this step can be skipped. A credential is officially considered expired if its expiration time is before the [official close time](/docs/concepts/ledgers/ledger-close-times) of the most recently validated ledger. This is more universal than comparing the expiration to your own local clock. Thus, the code uses the [ledger method][] to look up the most recently validated ledger:
+
+{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Confirm that the credential is not expired" before="# Credential has passed all checks" /%}
+
+If none of the checks up to this point have returned a `False` value, then the credential must be valid. This concludes the `verify_credential(...)` main function:
+
+{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Credential has passed all checks" before="# Commandline usage" /%}
+
+### 3. Commandline interface
+
+This file also implements a commandline utility which runs when the file is executed directly as a Python script. Some variables, such as the set of available networks, are only needed for this mode:
+
+{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Commandline usage" before="# Parse arguments" /%}
+
+Then it uses the [argparse library](https://docs.python.org/3/library/argparse.html) to define and parse the arguments that the user can pass from the commandline:
+
+{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Parse arguments" before="# Call verify_credential" /%}
+
+After parsing the commandline args, it sets the appropriate values and passes them to `verify_credential(...)` to perform the credential verification:
+
+{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Call verify_credential" before="# Return a nonzero exit code" /%}
+
+Finally, it returns a nonzero exit code if this credential was not verified. This can be useful for shell scripts:
+
+{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Call verify_credential" before="# Return a nonzero exit code" /%}
+
+Otherwise, the code exits as normal, which returns a successful exit code of `0` to the shell.
+
+## Next Steps
+
+Now that you know how to use `xrpl-py` to verify credentials, you can try building this or related steps together into a bigger project. For example:
+
+- Incorporate credential verification into a [wallet application](../build-apps/build-a-desktop-wallet-in-python.md).
+- Issue your own credentials with a [credential issuing service](../build-apps/credential-issuing-service.md).
+
+{% raw-partial file="/docs/_snippets/common-links.md" /%}
diff --git a/sidebars.yaml b/sidebars.yaml
index ee066c18abd..cc9a6496279 100644
--- a/sidebars.yaml
+++ b/sidebars.yaml
@@ -230,6 +230,9 @@
                 - page: docs/tutorials/python/build-apps/get-started.md
                 - page: docs/tutorials/python/build-apps/build-a-desktop-wallet-in-python.md
                 - page: docs/tutorials/python/build-apps/credential-issuing-service.md
+            - page: docs/tutorials/python/compliance/index.md
+              items:
+                - page: docs/tutorials/python/compliance/verify-credential.md
         - page: docs/tutorials/java/index.md
           expanded: false
           items:


Reviews:
user: login: maria-robobug
body: LGTM, just added a few minor comments.

FYI, I encountered an issue when running the [credential issuer code](https://github.com/XRPLF/xrpl-dev-portal/pull/2960/files) on my Mac (nothing to do with the code itself, just Flask and macOS issues). Basically it looks like port 5000 is already used by macOS Sequoia (and probably previous versions) for the `AirPlay Receiver`, if it's enabled. We should suggest that Mac users try a different port from the default Flask port if they have this enabled:

```sh
flask --app issuer_service run --port 8000 
```

More info here:
https://stackoverflow.com/questions/72795799/how-to-solve-403-error-with-flask-in-python
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T18:31:52Z
user: login: mDuo13
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-20T23:40:55Z
user: login: mDuo13
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-20T23:49:25Z
user: login: maria-robobug
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T11:01:31Z

Review comments:
diff_hunk: @@ -0,0 +1,214 @@
+---
+seo:
+    description: Verify that an account holds a valid credential on the XRP Ledger.
+labels:
+    - Credentials
+---
+# Verify Credentials in Python
+
+This tutorial describes how to verify that an account holds a valid [credential](/docs/concepts/decentralized-storage/credentials) on the XRP Ledger, which has different use cases depending on the type of credential and the meaning behind it. A few possible reasons to verify a credential include:
+
+- Confirming that a recipient has passed a background check before sending a payment.
+- Checking a person's professional certifications, after verifying their identity with a [DID](/docs/concepts/decentralized-storage/decentralized-identifiers).
+- Displaying a player's achievements in a blockchain-connected game.
+
+This tutorial uses sample code in Python using the [xrpl-py library](../index.md).
+
+## Prerequisites
+
+- You must have Python installed and know how to run Python code from the command line. Python 3.8 or later is required for xrpl-py.
+- You should have a basic understanding of the XRP Ledger
+- The credential you want to verify should exist in the ledger already, and you should know the addresses of both the issuer and the holder, as well as the official credential type you want to check.
+<!-- Uncomment when both are merged:
+     - For sample code showing how to create credentials, see [Build a Credential Issuing Service](../build-apps/credential-issuing-service.md).
+-->
+
+## Setup
+
+First, download the complete sample code for this tutorial from GitHub:
+
+- {% repo-link path="_code-samples/verify-credential/py/" %}Verify Credential sample code{% /repo-link %}
+
+Then, in the appropriate directory, set up a virtual environment and install dependencies:
+
+```sh
+python -m venv .venv
+source .venv/bin/activate
+pip install -r requirements.txt
+```
+
+This should install an appropriate version of the `xrpl-py` library. There are no other dependencies for this tutorial outside of the Python standard library.
+
+## Overview
+
+The Verify Credential sample code consists of one file, `verify_credential.py`, containing two main parts:
+
+1. A function, `verify_credential(...)` which can be called with appropriate arguments to verify that a credential exists and is valid. This function can be imported into other code to be used as part of a larger application.
+2. A commandline utility that can be used to verify any credential.
+
+## Usage
+
+To test that you have the code installed and working properly, you can run the commandline utility with no arguments to check the existence of a default credential on Devnet, such as:
+
+```sh
+python verify_credential.py
+```
+
+If all goes well, you should see output such as the following:
+
+```text
+Encoded credential_type as hex: 6D795F63726564656E7469616C
+Looking up credential...
+{'ledger_index': 'validated', 'method': 'ledger_entry', 'api_version': 2, 'credential': {'subject': 'rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA', 'issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'credential_type': '6D795F63726564656E7469616C'}, 'binary': False}
+Found credential:
+{'CredentialType': '6D795F63726564656E7469616C', 'Flags': 65536, 'Issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'IssuerNode': '0', 'LedgerEntryType': 'Credential', 'PreviousTxnID': '7D1257779E2D298C07C7E0C73CD446534B143FBD1F13DB268A878E40FD153B9A', 'PreviousTxnLgrSeq': 803275, 'Subject': 'rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA', 'SubjectNode': '0', 'index': '9603F0E204A8B1C61823625682EB0ECE98A4ECF22FF46CD4845FA9BFA3606B24'}
+Credential is valid.
+```
+
+If the code instead reports that the credential was not found when called with no arguments, it's possible that the example credential has been deleted or the Devnet has been reset. If you have another credential you can verify, you can provide the details as commandline arguments. For example:
path: docs/tutorials/python/compliance/verify-credential.md
user: login: maria-robobug
body: ```suggestion
If the code reports that the credential was not found when called with no arguments, it's possible that the example credential has been deleted or the Devnet has been reset. If you have another credential you can verify, you can provide the details as commandline arguments. For example:
```
created_at: 2025-03-13T11:51:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,214 @@
+---
+seo:
+    description: Verify that an account holds a valid credential on the XRP Ledger.
+labels:
+    - Credentials
+---
+# Verify Credentials in Python
+
+This tutorial describes how to verify that an account holds a valid [credential](/docs/concepts/decentralized-storage/credentials) on the XRP Ledger, which has different use cases depending on the type of credential and the meaning behind it. A few possible reasons to verify a credential include:
+
+- Confirming that a recipient has passed a background check before sending a payment.
+- Checking a person's professional certifications, after verifying their identity with a [DID](/docs/concepts/decentralized-storage/decentralized-identifiers).
+- Displaying a player's achievements in a blockchain-connected game.
+
+This tutorial uses sample code in Python using the [xrpl-py library](../index.md).
+
+## Prerequisites
+
+- You must have Python installed and know how to run Python code from the command line. Python 3.8 or later is required for xrpl-py.
+- You should have a basic understanding of the XRP Ledger
+- The credential you want to verify should exist in the ledger already, and you should know the addresses of both the issuer and the holder, as well as the official credential type you want to check.
+<!-- Uncomment when both are merged:
+     - For sample code showing how to create credentials, see [Build a Credential Issuing Service](../build-apps/credential-issuing-service.md).
+-->
+
+## Setup
+
+First, download the complete sample code for this tutorial from GitHub:
+
+- {% repo-link path="_code-samples/verify-credential/py/" %}Verify Credential sample code{% /repo-link %}
+
+Then, in the appropriate directory, set up a virtual environment and install dependencies:
+
+```sh
+python -m venv .venv
+source .venv/bin/activate
+pip install -r requirements.txt
+```
+
+This should install an appropriate version of the `xrpl-py` library. There are no other dependencies for this tutorial outside of the Python standard library.
+
+## Overview
+
+The Verify Credential sample code consists of one file, `verify_credential.py`, containing two main parts:
+
+1. A function, `verify_credential(...)` which can be called with appropriate arguments to verify that a credential exists and is valid. This function can be imported into other code to be used as part of a larger application.
+2. A commandline utility that can be used to verify any credential.
+
+## Usage
+
+To test that you have the code installed and working properly, you can run the commandline utility with no arguments to check the existence of a default credential on Devnet, such as:
+
+```sh
+python verify_credential.py
+```
+
+If all goes well, you should see output such as the following:
+
+```text
+Encoded credential_type as hex: 6D795F63726564656E7469616C
+Looking up credential...
+{'ledger_index': 'validated', 'method': 'ledger_entry', 'api_version': 2, 'credential': {'subject': 'rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA', 'issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'credential_type': '6D795F63726564656E7469616C'}, 'binary': False}
+Found credential:
+{'CredentialType': '6D795F63726564656E7469616C', 'Flags': 65536, 'Issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'IssuerNode': '0', 'LedgerEntryType': 'Credential', 'PreviousTxnID': '7D1257779E2D298C07C7E0C73CD446534B143FBD1F13DB268A878E40FD153B9A', 'PreviousTxnLgrSeq': 803275, 'Subject': 'rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA', 'SubjectNode': '0', 'index': '9603F0E204A8B1C61823625682EB0ECE98A4ECF22FF46CD4845FA9BFA3606B24'}
+Credential is valid.
+```
+
+If the code instead reports that the credential was not found when called with no arguments, it's possible that the example credential has been deleted or the Devnet has been reset. If you have another credential you can verify, you can provide the details as commandline arguments. For example:
+
+```sh
+python verify_credential.py rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA my_credential
+```
+
+A full usage statement is available with the `-h` flag.
+
+### Interactive Shell
+
+You can open an interactive python shell and import the `verify_credential` function, as in the following example:
+
+```py
+>>> from verify_credential import verify_credential
+>>> from xrpl.clients import JsonRpcClient
+>>> client = JsonRpcClient("https://s.devnet.rippletest.net:51234/")
+>>> verify_credential(client, issuer="rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS", subject="rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA", credential_type="my_credential")
+True
+```
+
+You can import the `verify_credential(...)` function into other scripts and use it the same way.
+
+### Other Examples
+
+The following examples show other possible scenarios. The data for these examples may or may not still be present in Devnet—for example, anyone can delete an expired credential.
+
+{% tabs %}
+{% tab label="Valid with Expiration" %}
+```text
+$ ./verify_credential.py rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S long_lasting_credential
+
+Encoded credential_type as hex: 6C6F6E675F6C617374696E675F63726564656E7469616C
+Looking up credential...
+{'ledger_index': 'validated', 'method': 'ledger_entry', 'api_version': 2, 'credential': {'subject': 'rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S', 'issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'credential_type': '6C6F6E675F6C617374696E675F63726564656E7469616C'}, 'binary': False}
+Found credential:
+{'CredentialType': '6C6F6E675F6C617374696E675F63726564656E7469616C', 'Expiration': 1167724800, 'Flags': 65536, 'Issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'IssuerNode': '0', 'LedgerEntryType': 'Credential', 'PreviousTxnID': 'C65794B7C322F028DB0D2DD72C9FF69D53A676B1608B77ADEF22311AFB22BFF7', 'PreviousTxnLgrSeq': 996934, 'Subject': 'rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S', 'SubjectNode': '0', 'index': 'FC4BB495DAE7C9F4615174188B3C5F2E337680017BA90E1F126DE08CAD15FD66'}
+Credential has expiration: 2037-01-01T08:00:00+00:00
+Looking up validated ledger to check for expiration.
+Most recent validated ledger is: 2025-03-11T20:01:51+00:00
+Credential is valid.
+```
+{% /tab %}
+
+{% tab label="Expired" %}
+```text
+$ ./verify_credential.py rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S expiring_credential
+
+Encoded credential_type as hex: 6578706972696E675F63726564656E7469616C
+Looking up credential...
+{'ledger_index': 'validated', 'method': 'ledger_entry', 'api_version': 2, 'credential': {'subject': 'rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S', 'issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'credential_type': '6578706972696E675F63726564656E7469616C'}, 'binary': False}
+Found credential:
+{'CredentialType': '6578706972696E675F63726564656E7469616C', 'Expiration': 795038400, 'Flags': 65536, 'Issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'IssuerNode': '0', 'LedgerEntryType': 'Credential', 'PreviousTxnID': 'E497F1EFE2E198EDED0D94ADDEE4CEFACDDC3B1674133A0123C765F8061B9600', 'PreviousTxnLgrSeq': 997087, 'Subject': 'rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S', 'SubjectNode': '0', 'index': 'F3A9475871E7BA994E257732D0C7CB0B91CACBBB9F840BDFA6ABABD6F71454CD'}
+Credential has expiration: 2025-03-11T20:00:00+00:00
+Looking up validated ledger to check for expiration.
+Most recent validated ledger is: 2025-03-11T20:02:03+00:00
+Credential is expired.
+```
+{% /tab %}
+
+{% tab label="Unaccepted" %}
+```text
+$ ./verify_credential.py rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S unaccepted_credential
+
+Encoded credential_type as hex: 756E61636365707465645F63726564656E7469616C
+Looking up credential...
+{'ledger_index': 'validated', 'method': 'ledger_entry', 'api_version': 2, 'credential': {'subject': 'rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S', 'issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'credential_type': '756E61636365707465645F63726564656E7469616C'}, 'binary': False}
+Found credential:
+{'CredentialType': '756E61636365707465645F63726564656E7469616C', 'Flags': 0, 'Issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'IssuerNode': '0', 'LedgerEntryType': 'Credential', 'PreviousTxnID': '59DB4B17E5552AB1CA1E2A89F5C03E51C2ACD0D293955FA701AE4A1801E94C96', 'PreviousTxnLgrSeq': 997107, 'Subject': 'rs9DtpwyCSGMCyxiYEvVG29ZXo99iFjZ9S', 'SubjectNode': '0', 'index': '8E5AD9444D566BE5C6F87C94D696139CEEE43ACB9A96137A59C003B48DF565C6'}
+Credential is not accepted.
+```
+{% /tab %}
+{% /tabs %}
+
+
+## Code Walkthrough
+
+### 1. Initial setup
+
+The `verify_credential.py` file implements the code for this tutorial. 
+This file can be run as a commandline tool, so it starts with a [shebang](https://en.wikipedia.org/wiki/Shebang_(Unix)). Then it imports dependencies, with standard lib first and then specific parts of the `xrpl-py` library:
+
+{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" before="# Set up logging" /%}
+
+The next section of the code sets the default log level for messages that might be written to the console through the utility:
+
+{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Set up logging" before="# Define an error to throw" /%}
+
+Then it defines a type of exception to throw if something goes wrong when connecting to the XRP Ledger:
+
+{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Define an error to throw" before="# Main function" /%}
+
+### 2. Main function
+
+The `verify_credential(...)` function performs the main work for this tutorial. The function definition and docstring define the parameters:
+
+{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Main function" before="# Handle function inputs" /%}
+
+The first thing the function does is verify that the user provided a credential type in either the `credential_type` or `credential_type_hex` parameter. The XRP Ledger APIs require the credential type to be hexadecimal, so it converts the user input if necessary:
+
+{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Handle function inputs" before="# Perform XRPL lookup" /%}
+
+Next, it calls the [ledger_entry method](/docs/references/http-websocket-apis/public-api-methods/ledger-methods/ledger_entry#get-credential-entry) to look up the requested Credential ledger entry:
+
+{% code-snippet file="/_code-samples/verify-credential/py/verify_credential.py" language="py" from="# Perform XRPL lookup" before="# Confirm that the credential has been accepted" /%}
+
+If it succeeds in finding the credential, the function continues by checking that the credential has been accepted by its holder. (Anyone can issue any credential to anyone else, so a credential is not considered valid unless the subject of the credential has accepted it.)
path: docs/tutorials/python/compliance/verify-credential.md
user: login: maria-robobug
body: ```suggestion
If it succeeds in finding the credential, the function continues by checking that the credential has been accepted by its holder. Since anyone can issue a credential to anyone else, a credential is only considered valid if its subject has accepted it.
```
created_at: 2025-03-13T12:02:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,214 @@
+---
+seo:
+    description: Verify that an account holds a valid credential on the XRP Ledger.
+labels:
+    - Credentials
+---
+# Verify Credentials in Python
+
+This tutorial describes how to verify that an account holds a valid [credential](/docs/concepts/decentralized-storage/credentials) on the XRP Ledger, which has different use cases depending on the type of credential and the meaning behind it. A few possible reasons to verify a credential include:
+
+- Confirming that a recipient has passed a background check before sending a payment.
+- Checking a person's professional certifications, after verifying their identity with a [DID](/docs/concepts/decentralized-storage/decentralized-identifiers).
+- Displaying a player's achievements in a blockchain-connected game.
+
+This tutorial uses sample code in Python using the [xrpl-py library](../index.md).
+
+## Prerequisites
+
+- You must have Python installed and know how to run Python code from the command line. Python 3.8 or later is required for xrpl-py.
+- You should have a basic understanding of the XRP Ledger
+- The credential you want to verify should exist in the ledger already, and you should know the addresses of both the issuer and the holder, as well as the official credential type you want to check.
+<!-- Uncomment when both are merged:
+     - For sample code showing how to create credentials, see [Build a Credential Issuing Service](../build-apps/credential-issuing-service.md).
+-->
+
+## Setup
+
+First, download the complete sample code for this tutorial from GitHub:
+
+- {% repo-link path="_code-samples/verify-credential/py/" %}Verify Credential sample code{% /repo-link %}
+
+Then, in the appropriate directory, set up a virtual environment and install dependencies:
+
+```sh
+python -m venv .venv
+source .venv/bin/activate
+pip install -r requirements.txt
+```
+
+This should install an appropriate version of the `xrpl-py` library. There are no other dependencies for this tutorial outside of the Python standard library.
path: docs/tutorials/python/compliance/verify-credential.md
user: login: maria-robobug
body: ```suggestion
This installs the appropriate version of the `xrpl-py` library. There are no other dependencies for this tutorial outside of the Python standard library.
```
created_at: 2025-03-13T12:10:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,214 @@
+---
+seo:
+    description: Verify that an account holds a valid credential on the XRP Ledger.
+labels:
+    - Credentials
+---
+# Verify Credentials in Python
+
+This tutorial describes how to verify that an account holds a valid [credential](/docs/concepts/decentralized-storage/credentials) on the XRP Ledger, which has different use cases depending on the type of credential and the meaning behind it. A few possible reasons to verify a credential include:
+
+- Confirming that a recipient has passed a background check before sending a payment.
+- Checking a person's professional certifications, after verifying their identity with a [DID](/docs/concepts/decentralized-storage/decentralized-identifiers).
+- Displaying a player's achievements in a blockchain-connected game.
+
+This tutorial uses sample code in Python using the [xrpl-py library](../index.md).
+
+## Prerequisites
+
+- You must have Python installed and know how to run Python code from the command line. Python 3.8 or later is required for xrpl-py.
+- You should have a basic understanding of the XRP Ledger
+- The credential you want to verify should exist in the ledger already, and you should know the addresses of both the issuer and the holder, as well as the official credential type you want to check.
+<!-- Uncomment when both are merged:
+     - For sample code showing how to create credentials, see [Build a Credential Issuing Service](../build-apps/credential-issuing-service.md).
+-->
+
+## Setup
+
+First, download the complete sample code for this tutorial from GitHub:
+
+- {% repo-link path="_code-samples/verify-credential/py/" %}Verify Credential sample code{% /repo-link %}
+
+Then, in the appropriate directory, set up a virtual environment and install dependencies:
+
+```sh
+python -m venv .venv
+source .venv/bin/activate
+pip install -r requirements.txt
+```
+
+This should install an appropriate version of the `xrpl-py` library. There are no other dependencies for this tutorial outside of the Python standard library.
+
+## Overview
+
+The Verify Credential sample code consists of one file, `verify_credential.py`, containing two main parts:
path: docs/tutorials/python/compliance/verify-credential.md
user: login: maria-robobug
body: ```suggestion
The Verify Credential sample code consists of one file, `verify_credential.py`, and contains two main parts:
```
created_at: 2025-03-13T12:10:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,214 @@
+---
+seo:
+    description: Verify that an account holds a valid credential on the XRP Ledger.
+labels:
+    - Credentials
+---
+# Verify Credentials in Python
+
+This tutorial describes how to verify that an account holds a valid [credential](/docs/concepts/decentralized-storage/credentials) on the XRP Ledger, which has different use cases depending on the type of credential and the meaning behind it. A few possible reasons to verify a credential include:
+
+- Confirming that a recipient has passed a background check before sending a payment.
+- Checking a person's professional certifications, after verifying their identity with a [DID](/docs/concepts/decentralized-storage/decentralized-identifiers).
+- Displaying a player's achievements in a blockchain-connected game.
+
+This tutorial uses sample code in Python using the [xrpl-py library](../index.md).
+
+## Prerequisites
+
+- You must have Python installed and know how to run Python code from the command line. Python 3.8 or later is required for xrpl-py.
+- You should have a basic understanding of the XRP Ledger
+- The credential you want to verify should exist in the ledger already, and you should know the addresses of both the issuer and the holder, as well as the official credential type you want to check.
+<!-- Uncomment when both are merged:
+     - For sample code showing how to create credentials, see [Build a Credential Issuing Service](../build-apps/credential-issuing-service.md).
+-->
+
+## Setup
+
+First, download the complete sample code for this tutorial from GitHub:
+
+- {% repo-link path="_code-samples/verify-credential/py/" %}Verify Credential sample code{% /repo-link %}
+
+Then, in the appropriate directory, set up a virtual environment and install dependencies:
+
+```sh
+python -m venv .venv
+source .venv/bin/activate
+pip install -r requirements.txt
+```
+
+This should install an appropriate version of the `xrpl-py` library. There are no other dependencies for this tutorial outside of the Python standard library.
+
+## Overview
+
+The Verify Credential sample code consists of one file, `verify_credential.py`, containing two main parts:
+
+1. A function, `verify_credential(...)` which can be called with appropriate arguments to verify that a credential exists and is valid. This function can be imported into other code to be used as part of a larger application.
+2. A commandline utility that can be used to verify any credential.
+
+## Usage
+
+To test that you have the code installed and working properly, you can run the commandline utility with no arguments to check the existence of a default credential on Devnet, such as:
+
+```sh
+python verify_credential.py
+```
+
+If all goes well, you should see output such as the following:
+
+```text
+Encoded credential_type as hex: 6D795F63726564656E7469616C
+Looking up credential...
+{'ledger_index': 'validated', 'method': 'ledger_entry', 'api_version': 2, 'credential': {'subject': 'rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA', 'issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'credential_type': '6D795F63726564656E7469616C'}, 'binary': False}
+Found credential:
+{'CredentialType': '6D795F63726564656E7469616C', 'Flags': 65536, 'Issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'IssuerNode': '0', 'LedgerEntryType': 'Credential', 'PreviousTxnID': '7D1257779E2D298C07C7E0C73CD446534B143FBD1F13DB268A878E40FD153B9A', 'PreviousTxnLgrSeq': 803275, 'Subject': 'rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA', 'SubjectNode': '0', 'index': '9603F0E204A8B1C61823625682EB0ECE98A4ECF22FF46CD4845FA9BFA3606B24'}
+Credential is valid.
+```
+
+If the code instead reports that the credential was not found when called with no arguments, it's possible that the example credential has been deleted or the Devnet has been reset. If you have another credential you can verify, you can provide the details as commandline arguments. For example:
+
+```sh
+python verify_credential.py rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA my_credential
+```
+
+A full usage statement is available with the `-h` flag.
+
+### Interactive Shell
+
+You can open an interactive python shell and import the `verify_credential` function, as in the following example:
+
+```py
+>>> from verify_credential import verify_credential
+>>> from xrpl.clients import JsonRpcClient
+>>> client = JsonRpcClient("https://s.devnet.rippletest.net:51234/")
+>>> verify_credential(client, issuer="rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS", subject="rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA", credential_type="my_credential")
+True
+```
+
+You can import the `verify_credential(...)` function into other scripts and use it the same way.
+
+### Other Examples
+
+The following examples show other possible scenarios. The data for these examples may or may not still be present in Devnet—for example, anyone can delete an expired credential.
path: docs/tutorials/python/compliance/verify-credential.md
user: login: maria-robobug
body: ```suggestion
The following examples show other possible scenarios. The data for these examples may or may not still be present in Devnet. For example, anyone can delete an expired credential.
```
created_at: 2025-03-19T14:16:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,214 @@
+---
+seo:
+    description: Verify that an account holds a valid credential on the XRP Ledger.
+labels:
+    - Credentials
+---
+# Verify Credentials in Python
+
+This tutorial describes how to verify that an account holds a valid [credential](/docs/concepts/decentralized-storage/credentials) on the XRP Ledger, which has different use cases depending on the type of credential and the meaning behind it. A few possible reasons to verify a credential include:
+
+- Confirming that a recipient has passed a background check before sending a payment.
+- Checking a person's professional certifications, after verifying their identity with a [DID](/docs/concepts/decentralized-storage/decentralized-identifiers).
+- Displaying a player's achievements in a blockchain-connected game.
+
+This tutorial uses sample code in Python using the [xrpl-py library](../index.md).
+
+## Prerequisites
+
+- You must have Python installed and know how to run Python code from the command line. Python 3.8 or later is required for xrpl-py.
+- You should have a basic understanding of the XRP Ledger
path: docs/tutorials/python/compliance/verify-credential.md
user: login: maria-robobug
body: ```suggestion
- You should have a basic understanding of the XRP Ledger.
```
created_at: 2025-03-19T16:12:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,214 @@
+---
+seo:
+    description: Verify that an account holds a valid credential on the XRP Ledger.
+labels:
+    - Credentials
+---
+# Verify Credentials in Python
+
+This tutorial describes how to verify that an account holds a valid [credential](/docs/concepts/decentralized-storage/credentials) on the XRP Ledger, which has different use cases depending on the type of credential and the meaning behind it. A few possible reasons to verify a credential include:
+
+- Confirming that a recipient has passed a background check before sending a payment.
+- Checking a person's professional certifications, after verifying their identity with a [DID](/docs/concepts/decentralized-storage/decentralized-identifiers).
+- Displaying a player's achievements in a blockchain-connected game.
+
+This tutorial uses sample code in Python using the [xrpl-py library](../index.md).
+
+## Prerequisites
+
+- You must have Python installed and know how to run Python code from the command line. Python 3.8 or later is required for xrpl-py.
+- You should have a basic understanding of the XRP Ledger
+- The credential you want to verify should exist in the ledger already, and you should know the addresses of both the issuer and the holder, as well as the official credential type you want to check.
+<!-- Uncomment when both are merged:
+     - For sample code showing how to create credentials, see [Build a Credential Issuing Service](../build-apps/credential-issuing-service.md).
+-->
+
+## Setup
+
+First, download the complete sample code for this tutorial from GitHub:
+
+- {% repo-link path="_code-samples/verify-credential/py/" %}Verify Credential sample code{% /repo-link %}
+
+Then, in the appropriate directory, set up a virtual environment and install dependencies:
+
+```sh
+python -m venv .venv
+source .venv/bin/activate
+pip install -r requirements.txt
+```
+
+This should install an appropriate version of the `xrpl-py` library. There are no other dependencies for this tutorial outside of the Python standard library.
+
+## Overview
+
+The Verify Credential sample code consists of one file, `verify_credential.py`, containing two main parts:
+
+1. A function, `verify_credential(...)` which can be called with appropriate arguments to verify that a credential exists and is valid. This function can be imported into other code to be used as part of a larger application.
+2. A commandline utility that can be used to verify any credential.
+
+## Usage
+
+To test that you have the code installed and working properly, you can run the commandline utility with no arguments to check the existence of a default credential on Devnet, such as:
+
+```sh
+python verify_credential.py
+```
+
+If all goes well, you should see output such as the following:
+
+```text
+Encoded credential_type as hex: 6D795F63726564656E7469616C
+Looking up credential...
+{'ledger_index': 'validated', 'method': 'ledger_entry', 'api_version': 2, 'credential': {'subject': 'rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA', 'issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'credential_type': '6D795F63726564656E7469616C'}, 'binary': False}
+Found credential:
+{'CredentialType': '6D795F63726564656E7469616C', 'Flags': 65536, 'Issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'IssuerNode': '0', 'LedgerEntryType': 'Credential', 'PreviousTxnID': '7D1257779E2D298C07C7E0C73CD446534B143FBD1F13DB268A878E40FD153B9A', 'PreviousTxnLgrSeq': 803275, 'Subject': 'rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA', 'SubjectNode': '0', 'index': '9603F0E204A8B1C61823625682EB0ECE98A4ECF22FF46CD4845FA9BFA3606B24'}
+Credential is valid.
+```
+
+If the code instead reports that the credential was not found when called with no arguments, it's possible that the example credential has been deleted or the Devnet has been reset. If you have another credential you can verify, you can provide the details as commandline arguments. For example:
+
+```sh
+python verify_credential.py rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA my_credential
+```
path: docs/tutorials/python/compliance/verify-credential.md
user: login: maria-robobug
body: Just a suggestion, feel free to ignore. Might be nice to add the alternative command with an encoded credential as well.
created_at: 2025-03-19T18:00:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,168 @@
+#!/usr/bin/env python
+
+import argparse
+import logging
+import sys
+from binascii import hexlify
+from re import match
+
+from xrpl.clients import JsonRpcClient
+from xrpl.models.requests import LedgerEntry, Ledger
+from xrpl.utils import ripple_time_to_datetime
+
+# Set up logging --------------------------------------------------------------
+# Use WARNING by default in case verify_credential is called from elsewhere.
+logger = logging.getLogger("verify_credential")
+logger.setLevel(logging.WARNING)
+logger.addHandler(logging.StreamHandler(sys.stderr))
+
+# Define an error to throw when XRPL lookup fails unexpectedly ----------------
+class XRPLLookupError(Exception):
+    def __init__(self, xrpl_response):
+        self.body = xrpl_response.result
+
+# Main function ---------------------------------------------------------------
+def verify_credential(client:JsonRpcClient, 
+                      issuer:str, 
+                      subject:str, 
+                      credential_type:str="", 
+                      credential_type_hex:str=""):
+    """
+    Check whether an XRPL account holds a specified credential,
+    as of the most recently validated ledger.
+
+    Paramters:
+        client - JsonRpcClient for the XRPL network to use.
+        issuer - Address of the credential issuer, in base58
+        subject - Address of the credential holder/subject, in base58
+        credential_type - Credential type to check for as a string,
+                          which will be encoded as UTF-8 (1-64 bytes long).
+        credential_type_hex - Credential type (binary) as hexadecimal.
+        verbose - If true, print details to stdout during lookup.
+    You must provide either credential_type or credential_type_hex.
+
+    Returns True if the account holds the specified, valid credential.
+    Returns False if the credential is missing, expired, or not accepted.
+    """
+    # Handle function inputs --------------------------------------------------
+    if not (credential_type or credential_type_hex):
+        raise ValueError("Provide a non-empty credential_type or " +
+                         "credential_type_hex")
+    if credential_type and credential_type_hex:
+        raise ValueError("Provide either credential_type or " +
+                         "credential_type_hex, but not both")
+    
+    # Encode credential_type as uppercase hex, if needed
+    if credential_type:
+        credential_type_hex = hexlify(credential_type.encode("utf-8")
+                                      ).decode("ascii")
+        logger.info("Encoded credential_type as hex: "+credential_type_hex.upper())
+    credential_type_hex = credential_type_hex.upper()
+
+    if len(credential_type_hex) % 2 or \
+            not match(r"[0-9A-F]{2,128}", credential_type_hex):
+        # Hexadecimal is always 2 chars per byte, so an odd length is invalid.
+        raise ValueError("credential_type_hex must be 1-64 bytes as hexadecimal.")
+
+    # Perform XRPL lookup of Credential ledger entry --------------------------
+    ledger_entry_request = LedgerEntry(
+        credential={
+            "subject": subject,
+            "issuer": issuer,
+            "credential_type": credential_type_hex
+        },
+        ledger_index="validated"
+    )
+    logger.info("Looking up credential...")
+    logger.info(ledger_entry_request.to_dict())
+    xrpl_response = client.request(ledger_entry_request)
+
+    if xrpl_response.status != "success":
+        if xrpl_response.result["error"] == "entryNotFound":
+            logger.info("Credential was not found")
+            return False
+        # Other errors, for example invalidly-specified addresses.
+        raise XRPLLookupError(xrpl_response)
+
+    credential = xrpl_response.result["node"]
+    logger.info("Found credential:")
+    logger.info(credential)
+
+    # Confirm that the credential has been accepted ---------------------------
+    lsfAccepted = 0x00010000
+    if not credential["Flags"] & lsfAccepted:
+        logger.info("Credential is not accepted.")
+        return False
+    
+    # Confirm that the credential is not expired ------------------------------
+    if credential.get("Expiration"):
+        ## TODO: test this code with expired and non-expired example credentials
path: _code-samples/verify-credential/py/verify_credential.py
user: login: maria-robobug
body: Do we still need this TODO? Seems to work.
created_at: 2025-03-19T18:14:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,168 @@
+#!/usr/bin/env python
+
+import argparse
+import logging
+import sys
+from binascii import hexlify
+from re import match
+
+from xrpl.clients import JsonRpcClient
+from xrpl.models.requests import LedgerEntry, Ledger
+from xrpl.utils import ripple_time_to_datetime
+
+# Set up logging --------------------------------------------------------------
+# Use WARNING by default in case verify_credential is called from elsewhere.
+logger = logging.getLogger("verify_credential")
+logger.setLevel(logging.WARNING)
+logger.addHandler(logging.StreamHandler(sys.stderr))
+
+# Define an error to throw when XRPL lookup fails unexpectedly ----------------
+class XRPLLookupError(Exception):
+    def __init__(self, xrpl_response):
+        self.body = xrpl_response.result
+
+# Main function ---------------------------------------------------------------
+def verify_credential(client:JsonRpcClient, 
+                      issuer:str, 
+                      subject:str, 
+                      credential_type:str="", 
+                      credential_type_hex:str=""):
+    """
+    Check whether an XRPL account holds a specified credential,
+    as of the most recently validated ledger.
+
+    Paramters:
+        client - JsonRpcClient for the XRPL network to use.
+        issuer - Address of the credential issuer, in base58
+        subject - Address of the credential holder/subject, in base58
+        credential_type - Credential type to check for as a string,
+                          which will be encoded as UTF-8 (1-64 bytes long).
+        credential_type_hex - Credential type (binary) as hexadecimal.
+        verbose - If true, print details to stdout during lookup.
+    You must provide either credential_type or credential_type_hex.
+
+    Returns True if the account holds the specified, valid credential.
+    Returns False if the credential is missing, expired, or not accepted.
+    """
+    # Handle function inputs --------------------------------------------------
+    if not (credential_type or credential_type_hex):
+        raise ValueError("Provide a non-empty credential_type or " +
+                         "credential_type_hex")
+    if credential_type and credential_type_hex:
+        raise ValueError("Provide either credential_type or " +
+                         "credential_type_hex, but not both")
+    
+    # Encode credential_type as uppercase hex, if needed
+    if credential_type:
+        credential_type_hex = hexlify(credential_type.encode("utf-8")
+                                      ).decode("ascii")
+        logger.info("Encoded credential_type as hex: "+credential_type_hex.upper())
+    credential_type_hex = credential_type_hex.upper()
+
+    if len(credential_type_hex) % 2 or \
+            not match(r"[0-9A-F]{2,128}", credential_type_hex):
+        # Hexadecimal is always 2 chars per byte, so an odd length is invalid.
+        raise ValueError("credential_type_hex must be 1-64 bytes as hexadecimal.")
+
+    # Perform XRPL lookup of Credential ledger entry --------------------------
+    ledger_entry_request = LedgerEntry(
+        credential={
+            "subject": subject,
+            "issuer": issuer,
+            "credential_type": credential_type_hex
+        },
+        ledger_index="validated"
+    )
+    logger.info("Looking up credential...")
+    logger.info(ledger_entry_request.to_dict())
+    xrpl_response = client.request(ledger_entry_request)
+
+    if xrpl_response.status != "success":
+        if xrpl_response.result["error"] == "entryNotFound":
+            logger.info("Credential was not found")
+            return False
+        # Other errors, for example invalidly-specified addresses.
+        raise XRPLLookupError(xrpl_response)
+
+    credential = xrpl_response.result["node"]
+    logger.info("Found credential:")
+    logger.info(credential)
+
+    # Confirm that the credential has been accepted ---------------------------
+    lsfAccepted = 0x00010000
+    if not credential["Flags"] & lsfAccepted:
+        logger.info("Credential is not accepted.")
+        return False
+    
+    # Confirm that the credential is not expired ------------------------------
+    if credential.get("Expiration"):
+        ## TODO: test this code with expired and non-expired example credentials
path: _code-samples/verify-credential/py/verify_credential.py
user: login: mDuo13
body: It does work, as I tested also. Just forgot to remove this. :+1: 
created_at: 2025-03-20T23:40:54Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,214 @@
+---
+seo:
+    description: Verify that an account holds a valid credential on the XRP Ledger.
+labels:
+    - Credentials
+---
+# Verify Credentials in Python
+
+This tutorial describes how to verify that an account holds a valid [credential](/docs/concepts/decentralized-storage/credentials) on the XRP Ledger, which has different use cases depending on the type of credential and the meaning behind it. A few possible reasons to verify a credential include:
+
+- Confirming that a recipient has passed a background check before sending a payment.
+- Checking a person's professional certifications, after verifying their identity with a [DID](/docs/concepts/decentralized-storage/decentralized-identifiers).
+- Displaying a player's achievements in a blockchain-connected game.
+
+This tutorial uses sample code in Python using the [xrpl-py library](../index.md).
+
+## Prerequisites
+
+- You must have Python installed and know how to run Python code from the command line. Python 3.8 or later is required for xrpl-py.
+- You should have a basic understanding of the XRP Ledger
+- The credential you want to verify should exist in the ledger already, and you should know the addresses of both the issuer and the holder, as well as the official credential type you want to check.
+<!-- Uncomment when both are merged:
+     - For sample code showing how to create credentials, see [Build a Credential Issuing Service](../build-apps/credential-issuing-service.md).
+-->
+
+## Setup
+
+First, download the complete sample code for this tutorial from GitHub:
+
+- {% repo-link path="_code-samples/verify-credential/py/" %}Verify Credential sample code{% /repo-link %}
+
+Then, in the appropriate directory, set up a virtual environment and install dependencies:
+
+```sh
+python -m venv .venv
+source .venv/bin/activate
+pip install -r requirements.txt
+```
+
+This should install an appropriate version of the `xrpl-py` library. There are no other dependencies for this tutorial outside of the Python standard library.
+
+## Overview
+
+The Verify Credential sample code consists of one file, `verify_credential.py`, containing two main parts:
+
+1. A function, `verify_credential(...)` which can be called with appropriate arguments to verify that a credential exists and is valid. This function can be imported into other code to be used as part of a larger application.
+2. A commandline utility that can be used to verify any credential.
+
+## Usage
+
+To test that you have the code installed and working properly, you can run the commandline utility with no arguments to check the existence of a default credential on Devnet, such as:
+
+```sh
+python verify_credential.py
+```
+
+If all goes well, you should see output such as the following:
+
+```text
+Encoded credential_type as hex: 6D795F63726564656E7469616C
+Looking up credential...
+{'ledger_index': 'validated', 'method': 'ledger_entry', 'api_version': 2, 'credential': {'subject': 'rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA', 'issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'credential_type': '6D795F63726564656E7469616C'}, 'binary': False}
+Found credential:
+{'CredentialType': '6D795F63726564656E7469616C', 'Flags': 65536, 'Issuer': 'rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS', 'IssuerNode': '0', 'LedgerEntryType': 'Credential', 'PreviousTxnID': '7D1257779E2D298C07C7E0C73CD446534B143FBD1F13DB268A878E40FD153B9A', 'PreviousTxnLgrSeq': 803275, 'Subject': 'rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA', 'SubjectNode': '0', 'index': '9603F0E204A8B1C61823625682EB0ECE98A4ECF22FF46CD4845FA9BFA3606B24'}
+Credential is valid.
+```
+
+If the code instead reports that the credential was not found when called with no arguments, it's possible that the example credential has been deleted or the Devnet has been reset. If you have another credential you can verify, you can provide the details as commandline arguments. For example:
+
+```sh
+python verify_credential.py rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA my_credential
+```
path: docs/tutorials/python/compliance/verify-credential.md
user: login: mDuo13
body: Added to the "Other Examples" tabs below.
created_at: 2025-03-20T23:49:25Z
author_association: COLLABORATOR

PR comments:
user: login: mDuo13
created_at: 2025-03-21T00:00:48Z
body: Applied review changes, rebased & squashed, and added links between the two Credentials tutorials.
author_association: COLLABORATOR