Configuring KERIpy Components, KERIA Agents, and Well-known URIs

How do you configure, publish, discover, and connect KERI components? Whether witnesses, verifiers, KERIA deployments, mailboxes, or general KERI controllers, configuration touches the topics of node publication, discovery, and connection, data discovery, and service endpoints. Node discovery and connection occurs with out of band identifiers (OOBIs). Data discovery of ACDC schemas and contents also utilize with OOBIs. Service endpoint configurations combine location scheme records and endpoint role authorizations. This article explains these concepts, KERI configuration mechanisms, and how to use them for real world, production configurations.

Disclaimer: This article assumes some basic familiarity with KERI concepts including identifiers (AIDs), witnesses, OOBIs, and the difference between KERIpy (the KERI implementation) and KERIA (the agent service). You should be able to follow along if you have some experience with KERIpy, the KLI, or KERIA and SignifyTS. If you are new to KERI and KERIA then go to the vLEI Trainings repository and work through those trainings to have a proper introduction and then return to this configuration guide.

Source review update, July 2026: The KERIA tock and escrow sections below were checked against the KERIA 0.4.1 development source with KERIpy 1.2.13 and against the current KERIpy development source. KERIA 0.3.0 with KERIpy 1.2.6 has the same central tock wiring and the same indefinitely retried multisig escrow families described here.

Introduction: The Configuration Challenge

Configuring KERIpy controllers and KERIA agents occurs at multiple levels:

Node Configuration

  1. Controller Configuration (KERIpy) - How your local identifier finds witnesses and peers, and which ports it listens on, if any.
  • This is also used as the basis for verifier configuration in components like sally or the vLEI Verifier.
  1. Witness Configuration (KERIpy) - The endpoints witnesses listen on
  2. Agency Configuration (KERIA) - Global settings for the KERIA service
  3. Agent Configuration (KERIA) - Per-agent settings within KERIA
  4. Mailbox Configuration (KERIpy) - Where receipts and messages are delivered to

Content Hosting Configuration

  1. ACDC Schema Hosting (KERIpy) - Hosting ACDC schemas for resolution by the OOBI URL resolution process.
  2. ACDC Content Hosting (KERIpy) - Hosting ACDC content for resolution by the OOBI URL resolution process.

Well Known Discovery Configuration

  1. Well-known URIs for Hosted Components - /.well-known/keri/oobi/ general discovery endpoints for KERI OOBIs, ACDC schemas and witness infrastructure

Let's explore each, starting with file-based configuration and progressing to runtime endpoint role authorization.

Part 1: KERIpy Controller Configuration

Controller configuration applies to components like verifiers, such as the sally verifier service that runs its own direct-mode listeners, or the KERIA service that listens on three ports.

You specify a configuration file with the kli init command like so:

kli init \
  --name my_aid_ks \
  --salt 0ACDEyMzQ1Njc4OWxtbm9aBc \
  --passcode 0ADDZyMzQ1Njc99Wxtbm7ssE \
  --config-dir "<directory-containing-config-tree>" \
  --config-file demo-witness-oobis

and in your config directory you would need to hav the following directory tree:

config-dir\
  - keri\
    - cf\
      - demo-witness-oobis.json

The Configer Class

At the heart of KERIpy configuration is the Configer class in keri/app/configing.py. It's a specialized file handler supporting four serialization formats:

class Configer(filing.Filer):
    """
    Habitat Config File
    
    Supports four serializations: HJSON, JSON, MGPK (MsgPack), and CBOR
    The serialization is determined by the file extension (.json, .mgpk, .cbor)
    """
    TailDirPath = "keri/cf"
    
    def __init__(self, name="conf", base="main", filed=True, 
                 fext="json", human=True, **kwa):
        # Creates file at path like: ~/.keri/cf/main/conf.json

Configuration files follow this structure:

{
  "dt": "2022-01-01T00:00:00.000000+00:00",
  "<AID name or prefix>": {
    "dt": "2022-01-20T12:57:59.823350+00:00",
    "curls": ["tcp://127.0.0.1:12344/", "http://127.0.0.1:12345/"]
  },
  "iurls": [
    "http://127.0.0.1:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha/controller"
  ],
  "durls": [
    "http://127.0.0.1:7723/oobi/EMhvwOlyEJ9kN4PrwCpr9Jsv7TxPhiYveZ0oP3lJzdEi"
  ],
  "wurls": [
    "http://127.0.0.1:5644/.well-known/keri/oobi/EBNa..."
  ]
}

Example: Sally verifier - configuring listening endpoints

The "sally" property of the JSON object corresponds to the --alias argument to kli incept. The curls section of the sally object tells the verifier which port to listen on.

Located at your config_dir/keri/cf/sally.json you would find the following JSON. And the sally.json at the end of the path is really the <alias>.json where alias corresponds to the --alias argument to kli incept.

{
  "dt": "2022-10-31T12:59:57.823350+00:00",
   "sally": {
    "dt": "2022-01-20T12:57:59.823350+00:00",
    "curls": ["http://127.0.0.1:9723/"]
  },
  "iurls": [],
  "durls": [
    "http://127.0.0.1:7723/oobi/EBNaNu-M9P5cgrnfl2Fvymy4E_jvxxyjb70PRtiANlJy",
    "http://127.0.0.1:7723/oobi/EH6ekLjSr8V32WyFbGe1zXjTzFs9PkTYmupJ9H65O14g",
    "http://127.0.0.1:7723/oobi/EKA57bKBKxr_kN7iN5i7lMUxpMG-s19dRcmov1iDxz-E",
    "http://127.0.0.1:7723/oobi/ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY",
    "http://127.0.0.1:7723/oobi/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao",
    "http://127.0.0.1:7723/oobi/EEy9PkikFcANV1l7EHukCeXqrzT1hNZjGlUk7wuMO5jw"
  ]
}

The iurls section is for witness OOBIs. The durls section is for data OOBIs, usually ACDC schemas or ACDC components. This example config does not have wurls for well knowns.

Example: KERIA agent server - configuring listening endpoints

Similar to the Sally verifier, the keria property in the config corresponds to the value passed to the --name argument to keria start which defaults to keria.

Again, you see the curls section for configuring the HTTP port used for listening to CESR transmissions to an agent. It has iurls and then KERIA-specific configuration for async loop repeat times, called "tocks."

{
  "dt": "2025-01-03T16:08:30.123456+00:00",
  "keria": {
    "dt": "2025-01-03T16:08:30.123457+00:00",
    "curls": ["http://127.0.0.1:3902/"]
  },
  "iurls": [
    "http://127.0.0.1:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha/controller?name=Wan&tag=witness",
    "http://127.0.0.1:5643/oobi/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM/controller?name=Wil&tag=witness",
    "http://127.0.0.1:5644/oobi/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX/controller?name=Wes&tag=witness"
  ],
  "tocks": {
    "initer": 0.0,
    "escrower": 1.0
  }
}

What KERIA tocks actually configure

A tock is a scheduler interval, not an escrow timeout. It controls how soon an HIO task is eligible to run again. It does not determine how old an escrow record may become before KERIA discards it.

KERIA's root Doist uses a fixed 0.03125-second cycle. A task with a tock of 0.0 is therefore eligible on every scheduler cycle—up to roughly 32 cycles per second when the process has enough capacity. A task with a tock of 1.0 is eligible about once per second.

This matters because every missing KERIA Agent tock defaults to 0.0. If tocks.escrower is absent, misspelled, or unavailable because the configuration did not load, KERIA may run its complete central escrow pass on every root scheduler cycle.

KERIA recognizes the following exact, case-sensitive keys:

Config keyTaskWhat its interval controls
initerIniterOne-time agent readiness and logging check.
querierQuerierParent queue for key-state queries. Spawned query tasks retain their own scheduling.
escrowerEscrowerCentral KEL, TEL, reply, EXN, credential-verifier, registrar, and credentialer escrow pass.
parserParserDoerProcessing of incoming CESR messages.
witnesserWitnesserWitness receipt and catch-up request queue.
delegatorDelegatorQueue that starts delegation work; not the internal delegation escrow loop.
exchangeSenderExchangeSenderParent queue for sending EXN messages.
granterGranterIPEX grant queue and the GrantDoer tasks it creates.
admitterAdmitterIPEX admit queue.
groupRequesterGroupRequesterQueue that starts multisig Counselor work; not the Counselor escrow loop.
seekerSeekerDoerCredential indexing cues.
exchangecueExchangeCueDoerEXN indexing and query cues.

Unknown keys are ignored. Missing known keys become 0.0. KERIA currently does not validate these names or expose the effective tock mapping from GET /config, so spelling errors can silently change scheduling behavior.

Do not set every task to 1.0 merely because that is appropriate for the central escrow scan. Parser, witness, query, and message-delivery latency have different requirements. The defensible production baseline from the KERIA source is to set the central escrow interval explicitly:

"tocks": {
  "initer": 0.0,
  "escrower": 1.0
}

Increasing escrower can reduce CPU spent rescanning retained records, but it also increases retry latency. It does not limit escrow size or expire stuck records.

What the central Escrower processes

Each central Escrower pass invokes all of these processors:

  1. KERIpy Kevery core KEL and receipt escrows
  2. KERIpy delegable-event escrow processing
  3. KERIpy Regery local TEL escrows
  4. KERIpy Revery reply escrows
  5. KERIpy Tevery non-local TEL escrows
  6. KERIpy Exchanger partially signed EXN escrows
  7. KERIpy credential Verifier dependency escrows
  8. KERIA registry and credential Registrar escrows
  9. KERIA credential-completion escrows

The single escrower tock retimes this entire group. It does not retime two important escrow processors:

  • KERIpy's multisig Counselor scans group partially signed, delegated, and partially witnessed escrows every 0.5 seconds.
  • KERIA's delegation Anchorer scans delegated partial-witness and unanchored escrows every 0.5 seconds.

groupRequester and delegator only control the queues that start work. They do not control those internal escrow scans.

Escrow retry interval versus escrow lifetime

Many low-level KERIpy escrows do have age checks. In KERIpy 1.2.13, the important defaults include:

Escrow familyDefault lifetime
KEL out-of-order events1,200 seconds
KEL partial signatures, witness receipts, and delegation3,600 seconds
Unverified receipts and likely-duplicitous events3,600 seconds
Query-not-found300 seconds
Reply messages3,600 seconds
Partially signed EXN messages20 seconds
Credential dependency verification3,600 seconds
Transaction-state broker escrows3,600 seconds

Other important escrow families have no age-based timeout:

Escrow familyTablesRetry behavior
Group multisig Counselorgpse, gdee, gpweFull scan every 0.5 seconds until protocol completion or manual clearing.
KERIA delegation Anchorerdpwe, duneFull scan every 0.5 seconds until completion or manual clearing.
TEL out-of-order and anchorless eventsoots, taesRetried by Tevery without an age check.
KERIA registry workflowtpwe, tmse, tedeRetried at the configured central escrower cadence.
KERIA credential completioncmseRetried at the configured central escrower cadence.

Here, "manual clearing" describes a storage-level possibility, not a supported general KERIA administration workflow. The generic KERIpy escrow commands can reach the Baser-side group and delegation tables, but they do not correctly open the KERIA Reger that contains the TEL, registry, and credential tables.

This distinction is operationally important. A failed multisig operation can remain in one of these tables indefinitely. Every scan touches it again. As the tables grow, repeated scans consume CPU; because LMDB is memory-mapped, touching more database pages can also increase resident memory.

Agency config versus persisted per-agent config

KERIA copies the agency configuration into a separate configuration file when it creates each Signify controller's agent. The per-agent configuration is named with the Signify controller AID and is loaded again when that agent reopens.

Changing the agency's keria.json does not rewrite existing per-agent files. This means adding "escrower": 1.0 to the deployment template fixes newly created agents but may leave existing agents running their older persisted configuration. Inspect and migrate the persisted per-agent files when changing tocks for an existing deployment.

Inspecting and clearing KERIA escrows

KERIA itself does not currently provide a general escrow administration API. GET /escrows/rpy is read-only and covers reply escrows only. Deleting a long-running operation or notification does not clear protocol escrows.

The underlying KERIpy version includes kli escrow list and kli escrow clear, but these are generic Habery maintenance commands, not KERIA-aware escrow administration tools.

For the KERIA versions reviewed, the commands can open an Agent's KEL Baser when --name is the Signify controller AID and --base matches KERIA's base. They do not correctly open that Agent's TEL and credential Reger. KERIA names the Reger agent-<controller-aid> and places it under the configured base, whereas KERIpy 1.2.12 and 1.2.13 derive the Reger name from the Habery name and omit the base.

Consequently:

  • kli escrow list may accurately report Baser-side KEL, multisig, delegation, reply, and EXN escrows while reporting empty or unrelated TEL, registry, and credential escrows.
  • Running list may create an empty Reger at the incorrectly derived path.
  • kli escrow clear must not be described as clearing all of a KERIA Agent's escrows. In KERIpy 1.2.12 and 1.2.13 it clears the real Baser but not the real KERIA Reger. Current KERIpy development clears only the Baser.

Do not use either command as proof that a KERIA Agent has no remaining escrows, and do not recommend kli escrow clear as a complete KERIA recovery procedure. If Baser-side maintenance is deliberately required, stop KERIA, snapshot the complete volume, and test against a restored copy. Accessing or clearing the actual KERIA Reger requires version-specific tooling that uses the exact KERIA Reger name and base; KERIA currently provides no supported general-purpose API or CLI for that operation.

The long-term production fix is not merely a larger tock or a periodic blanket clear. KERIA needs per-family metrics, oldest-record ages, bounded scan batches, and protocol-aware quarantine or expiry for escrow families that currently retry forever.

Configuration URL Types:

  • curls (Controller URLs): URLs + ports on which to set up either a TCP or HTTP port listener - where this controller can be reached
  • iurls (Introduction URLs): OOBI URLs for witnesses, watchers, mailboxes, and any KERI controller to resolve at startup
  • durls (Data URLs): OOBI URLs for static data like ACDC schemas or credentials
  • wurls (Well Known URLs): OOBI URLs specifically for well known components

Part 2: Witness Configuration

Witnesses are non-transferable identifiers that provide receipt services. Their configuration differs from controllers:

Witness Endpoint Configuration

Witnesses need two types of configuration:

  1. Controller endpoints - Where the witness accepts events
  2. Location schemes - The URLs where the witness is reachable

In this case the wan configuration property corresponds to the --alias property passed to the kli incept or kli witness start command. A witness listens on both a TCP and an HTTP port, as shown in the curls section below.

{
  "dt": "2022-01-20T12:57:59.823350+00:00",
  "wan": {
    "dt": "2022-01-20T12:57:59.823350+00:00",
    "curls": ["tcp://127.0.0.1:5632/", "http://127.0.0.1:5642/"]
  },
  "iurls": [
    "http://127.0.0.1:5643/oobi/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM/controller?name=Wil&tag=witness"
  ],
  "durls": [
    "http://127.0.0.1:7723/oobi/EBNaNu-M9P5cgrnfl2Fvymy4E_jvxxyjb70PRtiANlJy",
  ],
  "wurls": [
    "http://127.0.0.1:7723/.well-known/keri/oobi/EPflJSbTCs2WKoGx4zIJ5OpOXHXuY0JE9et9ile2gMpv"
  ]
}

Configuring Witness Endpoints with KLI

After starting a witness with kli witness demo, configure its endpoint roles and location:

Add witness endpoint role:

kli ends add \
  --name wan \
  --alias wan \
  --role witness \
  --eid BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha

Add location schemes for both TCP and HTTP:

# TCP location
kli loc add \
  --name wan \
  --alias wan \
  --eid BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha \
  --scheme tcp \
  --url tcp://127.0.0.1:5632/

# HTTP location
kli loc add \
  --name wan \
  --alias wan \
  --eid BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha \
  --scheme http \
  --url http://127.0.0.1:5642/

Witness Discovery

Controllers discover witnesses through:

  1. Configuration iurls: Pre-configured witness OOBIs
  2. OOBI resolution: Resolving witness OOBIs at runtime
  3. Well-known endpoints: Using .well-known/keri/oobi/ discovery

The witnesses must have their endpoint role authorizations configured so the controller can find them.

Part 3: Mailbox Configuration

Mailboxes are where witnesses and other agents deliver messages. Configuration involves:

Agent Endpoint Roles

An agent acts as a mailbox by having the mailbox endpoint role authorized:

# Configure agent with multiple roles
msgs = bytearray()

# Controller role
msgs.extend(agentHab.makeEndRole(
    eid=agentHab.pre,
    role=kering.Roles.controller,
    stamp=helping.nowIso8601()
))

# Location
msgs.extend(agentHab.makeLocScheme(
    url='http://127.0.0.1:6666',
    scheme=kering.Schemes.http,
    stamp=helping.nowIso8601()
))

# Agent role (for controller to reach agent)
msgs.extend(hab.makeEndRole(
    eid=agentHab.pre,
    role=kering.Roles.agent,
    stamp=helping.nowIso8601()
))

# Mailbox role (for witnesses to deliver messages)
msgs.extend(hab.makeEndRole(
    eid=agentHab.pre,
    role=kering.Roles.mailbox,
    stamp=helping.nowIso8601()
))

agentHab.psr.parse(ims=bytearray(msgs))
hab.psr.parse(ims=bytearray(msgs))  # Controller processes too

Adding Endpoint Roles with KLI

The KLI provides commands to add endpoint roles without writing Python code:

Add a controller endpoint role:

kli ends add \
  --name alice \
  --alias alice \
  --role controller \
  --eid EBErgFZ...

Add a witness endpoint role:

kli ends add \
  --name alice \
  --alias alice \
  --role witness \
  --eid BN8t3n1...

Add an agent/mailbox endpoint role:

kli ends add \
  --name alice \
  --alias alice \
  --role agent \
  --eid EBErgFZ...

kli ends add \
  --name alice \
  --alias alice \
  --role mailbox \
  --eid EBErgFZ...

Add a location scheme for an endpoint:

kli loc add \
  --name alice \
  --alias alice \
  --eid EBErgFZ... \
  --scheme http \
  --url http://127.0.0.1:6666

Querying Endpoints with KLI

Query configured endpoints for an identifier:

kli ends list \
  --name alice \
  --alias alice

Output shows all endpoint roles and their locations:

{
  "agent": {
    "EBErgFZ...": {
      "http": "http://127.0.0.1:6666"
    }
  },
  "controller": {
    "EGadHcy...": {
      "http": "http://127.0.0.1:7777"
    }
  },
  "mailbox": {
    "EBErgFZ...": {
      "http": "http://127.0.0.1:6666"
    }
  },
  "witness": {
    "BN8t3n1...": {
      "http": "http://127.0.0.1:8888"
    }
  }
}

Querying Endpoints Programmatically

Controllers can query their configured endpoints:

ends = hab.endsFor(hab.pre)
# Returns:
# {
#     'agent': {
#         'EBErgFZ...': {'http': 'http://127.0.0.1:6666'}
#     },
#     'controller': {
#         'EGadHcy...': {'http': 'http://127.0.0.1:7777'}
#     },
#     'mailbox': {
#         'EBErgFZ...': {'http': 'http://127.0.0.1:6666'}
#     },
#     'witness': {
#         'BN8t3n1...': {'http': 'http://127.0.0.1:8888'}
#     }
# }

Part 4: KERIA Agency Configuration

KERIA introduces a two-level configuration model: agency-level and agent-level.

Agency Configuration: KERIAServerConfig

The agency configuration is defined in keria/app/agenting.py:

@dataclass
class KERIAServerConfig:
    """Agency-wide configuration"""
    
    # HTTP ports
    adminPort: int = 3901
    httpPort: int | None = 3902
    bootPort: int = 3903
    
    # Master controller
    name: str = "keria"
    base: str = ""
    bran: str = None
    configFile: str = "keria"
    configDir: str = None
    
    # TLS
    keyPath: str = None
    certPath: str = None
    caFilePath: str = None
    
    # Logging
    logLevel: str = "CRITICAL"
    logFile: str = None
    logRequests: bool = False
    
    # Agency settings
    cors: bool = True
    releaseTimeout: int = 86400
    
    # Global OOBIs applied to all agents
    curls: List[str] = field(default_factory=list)
    iurls: List[str] = field(default_factory=list)
    durls: List[str] = field(default_factory=list)

Key Agency Configuration Fields:

  • Ports: Three HTTP servers (admin, boot, main)
  • Master controller: Agency's own identifier credentials
  • Global OOBIs: Applied to every agent created
    • curls: Controller service endpoint OOBIs
    • iurls: Introduction OOBIs for witnesses/watchers/mailboxes
    • durls: Data OOBIs for schemas/credentials

Creating an Agency

config = KERIAServerConfig(
    name="my-agency",
    bran="MySecurePasscodeHere",
    adminPort=3901,
    httpPort=3902,
    bootPort=3903,
    curls=["http://localhost:7723/oobi/controller"],
    iurls=["http://localhost:5644/oobi/witness1"],
    durls=["http://schemas.example.com/oobi/schema1"],
    temp=False
)

agency = Agency(
    name=config.name,
    bran=config.bran,
    base=config.base,
    releaseTimeout=config.releaseTimeout,
    curls=config.curls,
    iurls=config.iurls,
    durls=config.durls,
    temp=False
)

Agency Configuration File

Agencies can also load from a config file:

cf = configing.Configer(
    name="keria",
    base="",
    headDirPath="/etc/keria",
    temp=False,
    reopen=True
)

agency = Agency(
    name="keria",
    bran=bran,
    cf=cf,
    temp=False
)

AS shown earlier, the config file follows the same structure as KERIpy controller configs:

{
  "dt": "2026-01-21T00:00:00.000000+00:00",
  "keria": {
    "dt": "2026-01-21T00:00:00.000000+00:00",
    "curls": ["http://localhost:7723/oobi/agency-controller"]
  },
  "iurls": [
    "http://localhost:5644/oobi/witness1",
    "http://localhost:5644/oobi/watcher1"
  ],
  "durls": [
    "http://schemas.example.com/oobi/vlei-schema"
  ]
}

Part 5: KERIA Agent Configuration

Each agent in KERIA gets its own configuration derived from the agency configuration.

Agent Configuration Loading

When creating an agent, the agency loads configuration:

def _loadConfigForAgent(self, caid):
    """
    Loads configuration for an agent by:
    1. Reading agency configuration
    2. Extracting agency subsection
    3. Renaming it to agent-specific name
    4. Merging with environment variable OOBIs
    """
    timestamp = nowIso8601()
    config = dict(self.cf.get() if self.cf is not None else {"dt": timestamp})
    
    # Rename agency section to agent section
    habName = f"agent-{caid}"
    config_name = self.name if self.name else "keria"
    if config_name in config:
        config[habName] = config[config_name]
        del config[config_name]
    else:
        config[habName] = {}
    
    # Set up curls for agent (controller-specific)
    config[habName]["curls"] = config[habName].get("curls", [])
    
    # Set up iurls/durls (shared across all agents)
    config["iurls"] = config.get("iurls", [])
    config["durls"] = config.get("durls", [])
    
    # Merge with environment variables
    if self.curls is not None and isinstance(self.curls, list):
        config[habName]["curls"] = config[habName]["curls"] + self.curls
    
    if self.iurls is not None and isinstance(self.iurls, list):
        config["iurls"] = config["iurls"] + self.iurls
    
    if self.durls is not None and isinstance(self.durls, list):
        config["durls"] = config["durls"] + self.durls
    
    return config

Important Distinction:

  • config[habName]["curls"]: Controller-specific endpoints (unique per agent)
  • config["iurls"]: Shared introduction OOBIs (witnesses, watchers, mailboxes)
  • config["durls"]: Shared data OOBIs (schemas, credentials)

Agent Configuration Writing

The agency writes agent-specific config files:

def _writeAgentConfig(self, caid):
    """
    Writes agent configuration as modified copy of agency config
    """
    config = self._loadConfigForAgent(caid)
    cf = configing.Configer(
        name=f"{caid}",
        base="",
        human=False,
        temp=self.temp,
        reopen=True,
        clear=False
    )
    cf.put(config)
    return cf

This creates a config file at ~/.keri/cf/{caid}.json containing:

{
  "dt": "2026-01-21T00:00:00.000000+00:00",
  "agent-EIaGMMWJ...": {
    "dt": "2026-01-21T00:00:00.000000+00:00",
    "curls": [
      "http://localhost:7723/oobi/agency-controller",
      "http://localhost:8080/oobi/my-controller"
    ]
  },
  "iurls": [
    "http://localhost:5644/oobi/witness1"
  ],
  "durls": [
    "http://schemas.example.com/oobi/schema1"
  ]
}

Part 6: Endpoint Role Authorization at Runtime

Beyond file-based configuration, KERI uses endpoint role authorization messages for dynamic configuration.

Endpoint Role Authorization Messages

These are KERI events that authorize specific endpoints for specific roles:

# Controller endpoint
endRoleMsg = hab.makeEndRole(
    eid=hab.pre,                    # Endpoint identifier
    role=kering.Roles.controller,   # Role type
    stamp=helping.nowIso8601()      # Timestamp
)

# Location scheme
locSchemeMsg = hab.makeLocScheme(
    url='http://127.0.0.1:7777',    # Endpoint URL
    scheme=kering.Schemes.http,     # URL scheme
    stamp=helping.nowIso8601()      # Timestamp
)

Available Roles:

Roles = Rolage(
    controller='controller', 
    witness='witness', 
    registrar='registrar',
    watcher='watcher', 
    judge='judge', 
    juror='juror', 
    peer='peer', 
    mailbox="mailbox", 
    agent="agent")

Available Schemes:

Schemes = Schemage(
    tcp='tcp', 
    http='http', 
    https='https')

Adding Endpoint Roles with KLI

Instead of writing Python code, use KLI commands to add endpoint roles and locations:

Add a controller endpoint role:

kli ends add \
  --name alice \
  --alias alice \
  --role controller \
  --eid EGadHcy...

Add the corresponding location:

kli loc add \
  --name alice \
  --alias alice \
  --eid EGadHcy... \
  --scheme http \
  --url http://127.0.0.1:7777

Add multiple roles for the same endpoint:

# Agent role
kli ends add \
  --name alice \
  --alias alice \
  --role agent \
  --eid EBErgFZ...

# Mailbox role
kli ends add \
  --name alice \
  --alias alice \
  --role mailbox \
  --eid EBErgFZ...

# Location (shared by both roles)
kli loc add \
  --name alice \
  --alias alice \
  --eid EBErgFZ... \
  --scheme http \
  --url http://127.0.0.1:6666

Processing Endpoint Messages Programmatically

These messages are parsed into the database:

msgs = bytearray()
msgs.extend(hab.makeEndRole(eid=hab.pre, role="controller", 
                           stamp=helping.nowIso8601()))
msgs.extend(hab.makeLocScheme(url='http://127.0.0.1:7777',
                              scheme=kering.Schemes.http,
                              stamp=helping.nowIso8601()))
hab.psr.parse(ims=msgs)

# Verify stored in database
ender = hab.db.ends.get(keys=(hab.pre, "controller", hab.pre))
assert ender.allowed  # Role is authorized

locer = hab.db.locs.get(keys=(hab.pre, kering.Schemes.http))
assert locer.url == 'http://127.0.0.1:7777'  # Location stored

OOBI Resolution vs File Configuration

The key distinction:

File Configuration (curls, iurls, durls):

  • Static configuration loaded at startup
  • Processed during Habery.makeHab() or reconfigure()
  • Stored in config file permanently

OOBI Resolution:

  • Dynamic configuration at runtime
  • Processed via hab.db.oobis.pin() and OOBI resolution
  • Results in same endpoint role authorizations

Example:

# File-based
conf = {"curls": ["http://localhost:7723/oobi/controller"]}
cf.put(conf)
hab = hby.makeHab(name="alice")  # Processes curls

# Equivalent OOBI resolution
oobi = "http://localhost:7723/oobi/controller"
obr = OobiRecord(date=helping.toIso8601(helping.nowUTC()))
hab.db.oobis.pin(keys=(oobi,), val=obr)
# ... OOBI resolver processes and creates endpoint records

Both methods result in the same database records:

  • hab.db.ends: Endpoint role authorizations
  • hab.db.locs: Location schemes

Part 7: Answering the Original Questions

Now we can answer the Discord questions:

Q: "How to configure a controller/key store with iurls/curls like with kli init --file?"

Answer: Three methods exist:

Method 1: File-based initialization

# Create config file
cf = configing.Configer(name="myconfig", temp=False)
conf = {
    "dt": help.nowIso8601(),
    "mycontroller": {
        "curls": ["http://localhost:7723/oobi/controller"]
    },
    "iurls": ["http://localhost:5644/oobi/witness1"]
}
cf.put(conf)

# Initialize Habery with config
hby = habbing.Habery(name="myconfig", cf=cf, temp=False)
hab = hby.makeHab(name="mycontroller")

Method 2: Programmatic configuration

# Create Habery
hby = habbing.Habery(name="test", temp=True)

# Create config
conf = {
    "dt": help.nowIso8601(),
    "alice": {
        "curls": ["http://localhost:7723/oobi/controller"]
    },
    "iurls": ["http://localhost:5644/oobi/witness1"]
}
hby.cf.put(conf)

# Create hab (processes config)
hab = hby.makeHab(name="alice")

Method 3: OOBI resolution

# Create hab
hab = hby.makeHab(name="alice")

# Resolve OOBIs at runtime
oobi = "http://localhost:5644/oobi/witness1"
obr = OobiRecord(date=helping.toIso8601(helping.nowUTC()))
hab.db.oobis.pin(keys=(oobi,), val=obr)
# OOBI resolver processes in background

Method 4: KLI commands for endpoint roles

# Add controller endpoint role
kli ends add \
  --name alice \
  --alias alice \
  --role controller \
  --eid EGadHcy...

# Add location
kli loc add \
  --name alice \
  --alias alice \
  --eid EGadHcy... \
  --scheme http \
  --url http://localhost:7723/oobi/controller

# Resolve witness OOBI
kli oobi resolve \
  --name alice \
  --oobi-alias witness1 \
  --oobi http://localhost:5644/oobi/witness1

Q: "Is there a good example of running the controller server?"

Answer: KERIA provides the production pattern:

from keria.app.agenting import (
    KERIAServerConfig, createAgency, setupDoers, agencyDoist
)

# Configure agency
config = KERIAServerConfig(
    name="keria",
    bran="MySecurePasscodeHere",
    adminPort=3901,
    httpPort=3902,
    bootPort=3903,
    iurls=["http://localhost:5644/oobi/witness1"],
    temp=False
)

# Create agency
agency = createAgency(config, temp=False)

# Setup HTTP servers and doers
doers = setupDoers(agency, config)

# Run with Doist
doist = agencyDoist(doers)
doist.do()

For simpler use cases without HTTP servers:

# Just create the agency
agency = Agency(
    name="my-agency",
    bran=bran,
    base="",
    iurls=["http://localhost:5644/oobi/witness1"],
    temp=False
)

# Create agents
agent = agency.create(caid="EIaGMMWJ...")

# Run agency
doist = doing.Doist(limit=0.0, tock=0.03125, real=True)
doist.do(doers=[agency])

Q: "No way for the controller to provide durls/iurls except resolving OOBIs?"

Answer: This is partially correct but requires clarification:

In KERIpy: Controllers can provide curls, iurls, and durls through:

  1. Configuration file (shown above)
  2. OOBI resolution at runtime
  3. Endpoint role authorization messages

In KERIA: The agency configuration provides global iurls and durls that are inherited by all agents. However:

  • Limitation: Currently no API endpoint to add agent-specific curls/iurls/durls after agent creation
  • Workaround: Resolve OOBIs through the /oobis endpoint or KLI:

Via KERIA API:

POST /identifiers/{alias}/oobis
{
  "oobialias": "my-witness",
  "url": "http://localhost:5644/oobi/witness1"
}

Via KLI:

kli oobi resolve \
  --name alice \
  --alias alice \
  --oobi-alias my-witness \
  --oobi http://localhost:5644/oobi/witness1

Both methods trigger OOBI resolution which creates the same endpoint records as file-based configuration.

Improvement Opportunity: As Daniel noted, adding an API to modify agent configuration would be valuable:

# Proposed API (not yet implemented)
PUT /config
{
  "iurls": ["http://localhost:5644/oobi/new-witness"],
  "durls": ["http://schemas.example.com/oobi/new-schema"]
}

Part 8: Well-Known OOBI Discovery

Well-known URIs provide standardized discovery endpoints for KERI resources following RFC 8615. GLEIF publishes production witness, AID, and schema OOBIs at https://gleif-it.github.io/.well-known/keri/oobi/.

Structure

The well-known directory follows a hierarchical structure:

/.well-known/
├── index.json                  # Main discovery index
├── schema.json                 # JSON Schema for validation
└── keri/oobi/
    ├── index.json              # OOBI catalog (AIDs, witnesses, schemas)
    ├── schema.json             # OOBI index schema
    └── {identifier}/           # Per-identifier directories
        └── index.json          # Resource content

Discovery Protocol

Step 1: Discover available resources

curl https://gleif-it.github.io/.well-known/keri/oobi/index.json

Returns:

{
  "$schema": "https://gleif-it.github.io/.well-known/keri/oobi/schema.json",
  "aids": {
    "GLEIF RoOT": "EDP1vHcw_wc4M__Fj53-cJaBnZZASd-aMTaSyWEQ-PC2",
    "GLEIF External": "EINmHd5g7iV-UldkkkKyBIH052bIyxZNBn9pq-zNrYoS"
  },
  "witnesses": {
    "BDkq35LUU63xnFmfhljYYRY0ymkCg7goyeCxN30tsvmS": "BDkq35LUU63xnFmfhljYYRY0ymkCg7goyeCxN30tsvmS"
  },
  "schemas": {
    "LegalEntityvLEICredential": "ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY"
  }
}

Step 2: Resolve specific identifier

# Get AID witness URLs
curl https://gleif-it.github.io/.well-known/keri/oobi/EDP1vHcw_wc4M__Fj53-cJaBnZZASd-aMTaSyWEQ-PC2/index.json

Returns KERI rpy message with witness URLs:

{
  "v": "KERI10JSON000282_",
  "t": "rpy",
  "r": "/oobi/witness",
  "a": {
    "urls": [
      "http://5.161.69.25:5623/oobi/EDP1vHcw_.../witness",
      "http://51.161.130.60:5623/oobi/EDP1vHcw_.../witness"
    ],
    "aid": "EDP1vHcw_wc4M__Fj53-cJaBnZZASd-aMTaSyWEQ-PC2"
  }
}

Resource Types

Well-known endpoints serve three resource types:

TypePrefixContentContent-Type
AIDEKERI rpy message with witness URLsapplication/cesr
WitnessBWitness KEL (icp + rpy messages)application/cesr
SchemaEACDC JSON Schemaapplication/cesr

Configuration with wurls

Use well-known URIs in configuration files:

{
  "dt": "2026-01-21T00:00:00.000000+00:00",
  "wurls": [
    "https://gleif-it.github.io/.well-known/keri/oobi/EDP1vHcw_wc4M__Fj53-cJaBnZZASd-aMTaSyWEQ-PC2"
  ]
}

The wurls field is processed by Habery.reconfigure() similar to iurls and durls.

Implementation

The vLEI project includes a reference implementation in src/vlei/app/well_known.py:

from vlei.app.well_known import loadWellKnownEnds
import falcon

app = falcon.App()
oobi_dir = "./samples/oobis/.well-known/keri/oobi"
loadWellKnownEnds(app, oobi_dir)

The handler:

  1. Loads index.json to categorize identifiers
  2. Routes requests based on identifier type (AID/witness/schema)
  3. Returns appropriate content with correct content-type headers

Sample data from GLEIF's production well-known directory is available in vLEI/samples/oobis/.well-known/ including 3 AIDs, 10 witnesses, and 8 vLEI credential schemas.

Benefits

  • Standardized Discovery: RFC 8615 compliant path structure
  • No Authentication: Public discovery endpoints
  • Complete Resource Sets: AIDs return all witness URLs in single response
  • Schema Resolution: Direct access to ACDC schemas without OOBI resolution
  • Production Ready: GLEIF operates this at scale for vLEI ecosystem

Part 9: Configuration Best Practices

For KERIpy Controllers

  1. Use configuration files for static topology

    • Witnesses that rarely change
    • Well-known schema OOBIs
    • Persistent peer relationships
  2. Use OOBI resolution for dynamic discovery

    • New witnesses discovered at runtime
    • Peer identifiers learned through introductions
    • Ephemeral connections
  3. Use endpoint role authorization for services

    • Your own controller endpoints
    • Agent mailbox endpoints
    • Service availability changes

For KERIA Agencies

  1. Set global iurls in agency config

    • Standard witnesses all agents use
    • Common watcher pools
    • Shared schema registries
  2. Keep curls agent-specific

    • Each agent may have unique controller endpoints
    • Avoid global curls unless all agents share endpoints
  3. Use configuration files for production

    • Easier to manage than environment variables
    • Version controlled configuration
    • Clear separation of concerns
  4. Provide OOBI resolution API for runtime changes

    • New witnesses can be added without restart
    • Dynamic peer discovery
    • Schema updates

Conclusion

KERI configuration operates at multiple levels:

  1. Controller Configuration: File-based curls, iurls, durls or OOBI resolution
  2. Witness Configuration: Endpoint role authorization for witness services
  3. Mailbox Configuration: Endpoint role authorization for agent/mailbox services
  4. Agency Configuration: Global KERIA service settings and OOBIs
  5. Agent Configuration: Per-controller settings derived from agency config
  6. Well-Known Discovery: RFC 8615 compliant /.well-known/keri/oobi/ endpoints for standardized resource discovery

Understanding these levels clarifies the apparent confusion about "no way to provide iurls except OOBIs." The truth is:

  • KERIpy: Multiple methods exist (file, OOBI, endpoint messages, well-known URIs)
  • KERIA: Agency provides global iurls, agents inherit them, runtime updates via OOBI resolution

Both approaches ultimately create the same database records: endpoint role authorizations (db.ends) and location schemes (db.locs). The choice between file-based configuration, OOBI resolution, and well-known discovery depends on your use case: static infrastructure, dynamic topology, or public discovery.

For production deployments, use:

  • Well-known URIs for public discovery of organizational infrastructure
  • Agency config file for stable, shared witnesses and watchers
  • OOBI resolution API for dynamic peer discovery and new witnesses
  • Endpoint role messages for your own service availability

This provides the flexibility to handle static infrastructure, dynamic peer-to-peer discovery, and standardized public resource publication.

Further Reading

Table of Contents