Driving credenco-cli from an AI Agent
An AI agent with a shell can operate a Business Wallet through credenco-cli without any
wallet-specific integration. The CLI was built with that in mind, and this page shows what makes
it work, what the agent needs, and five requests in plain English turned into commands and real
output.
Every terminal screen below is a capture of a real run against a Credenco test environment.
Commands, flags, field names, statuses and response bodies are verbatim. Three things are
normalised for readability: the host name is written as wallet.acc.credenco.com, home
directories are shown as ~, and secrets are redacted.
Why a CLI suits an agent
Four properties matter more for an agent than for a person:
- The command surface is self-describing.
credenco-cli operationsprints every command with its HTTP method, path and one-line summary, and--helpon any command lists its flags. The agent does not need the OpenAPI document, a plugin or an MCP server; the binary tells it what exists. - API responses are JSON whenever the output is not a terminal. An agent's shell is never a
TTY, so the agent always receives JSON it can parse, with no flag to remember. Progress messages,
QR codes and errors go to standard error, so standard output stays parseable even when something
goes wrong. The local commands that print no API response,
operations,profile listand every--help, are plain text in both cases and are meant to be read, not parsed. - Failures are typed. The process exit code says what kind of failure happened:
2usage or configuration,3authentication,4not found,5server error. An agent can retry, ask for a credential or give up without pattern-matching an error message. - Changes can be previewed.
--dry-runprints the exact request and sends nothing, not even an OAuth token request. An agent can show the user what it is about to do and only then run it for real.
What the agent needs
Credentials, through the environment
Give the agent one of these, in the environment of the shell it runs commands in:
export CREDENCO_BASE_URL=https://wallet.acc.credenco.com
export CREDENCO_API_KEY=... # or:
export CREDENCO_CLIENT_ID=...
export CREDENCO_CLIENT_SECRET=...
export CREDENCO_WALLET=... # the wallet it may touch
Alternatively run credenco-cli login yourself once, and the agent inherits the stored profile.
CREDENCO_API_KEY and CREDENCO_CLIENT_SECRET deliberately have no flag equivalent, so there is
no way for an agent to write a secret into a command line, where it would land in shell history,
in the process table, and in the agent's own transcript. If an agent reports that it needs a flag
for the key, it has the wrong model of the tool.
The CLI redacts Authorization and x-api-key in everything it prints, --dry-run output
included, so an agent can safely paste a previewed request back to the user.
A scope, not the whole estate
An API key is bound to a single wallet, so scoping an agent is a matter of which key it gets. Give it a key for a sandbox wallet while it works something out, and only then point it at the real one. See API Keys.
A rule about --yes
Destructive commands exit with code 2 instead of running when --yes is missing. Keep that as
the human checkpoint: let the agent use --dry-run freely, and require it to come back for
confirmation before adding --yes.
How an agent discovers what to do
A useful first instruction is simply to let the CLI describe itself:
credenco-cli operations # every operation, grouped by API tag
credenco-cli --help # the global flags and the command groups
credenco-cli credential-template create-credential-template --help # one command's flags
The per-command help is generated from the API specification, including a flag for most top-level
request-body fields, so an agent that reads --help cannot invent a field name that does not
exist. Two kinds of field are not exposed as flags and go through --body, which takes inline
JSON, @file or @-: nested structures such as attributes, displays and queryDefinition,
and anything secret, meaning a name containing password or secret plus the one-time codes
code, tx_code and verificationCode. A list of plain strings does get a flag.
Five requests, end to end
The five examples below are the ones an operator is most likely to ask for. Each shows the request in plain English, the commands an agent turns it into, and what actually came back. They build on each other, and together they are the same walkthrough as Usage examples.
"Which credentials are stored in my wallet?"
One command, no arguments. The wallet comes from CREDENCO_WALLET or the stored profile.
Which credentials are stored in my wallet?
Bash(credenco-cli credential list-wallet-holder-credentials)
{
"content": [],
"page": {
… +6 lines (ctrl+o to expand)
The wallet holds no credentials yet: totalElements is 0.
After the issuance in the third example, the same command answers with the credential and its claims:
Bash(credenco-cli credential list-wallet-holder-credentials | jq '.content[] | {type, credentialStatus, issuedAt, claims: (.payload | {iban, account_name, currency, account_type})}')
{
"type": "BankAccountCredential",
"credentialStatus": "VALID",
… +8 lines (ctrl+o to expand)
One valid BankAccountCredential, issued on 2026-10-13 at 14:26:
| Claim | Value |
|---|---|
iban | NL91ABNA0417164300 |
account_name | Ada Lovelace |
currency | EUR |
account_type | checking |
"I want to issue a credential called Bank Account with the attributes iban, account_name, currency and account_type"
This is two objects, not one: an issuer template (the OpenID4VCI credential issuer) and a
credential template inside it (the credential's format, type and claims). An agent that read
--help will know that attributes has no flag and must go through --body.
I want to issue a credential called Bank Account with the attributes iban, account_name, currency and account_type
Bash(credenco-cli issuer-template create-issuer-template \
--issuer-template-key bank-account-issuer \
--name "Tulip Trust Bank" \
--description "Issues Bank Account credentials" \
--interop-profile DIIP_V4)
{
"createdAt": "2026-10-13T14:26:08",
"createdBy": "Tulip Trust Bank",
… +11 lines (ctrl+o to expand)
Issuer template bank-account-issuer is created, with interop profile DIIP_V4. The credential template needs the identifier that signs the credential, so I look that up first.
The credential template needs the externalKey of the identifier that signs the credential, which
is itself one command away, so an agent chains the two:
Bash(credenco-cli identifier list-identifiers | jq -r '.content[] | select(.defaultIdentifier) | .externalKey')
f8124ef0-bf81-46fe-9655-5a2b3864ade9
Bash(credenco-cli credential-template create-credential-template bank-account-issuer --body @- <<'JSON' …)
{
"attributes": [
{
… +34 lines (ctrl+o to expand)
Created credential template bank-account-sd-jwt under bank-account-issuer, format SD_JWT:
| Attribute | Mandatory | Selectively disclosable |
|---|---|---|
iban | yes | yes |
account_name | yes | yes |
currency | no | yes |
account_type | no | yes |
The response echoes the stored template, so the agent can confirm the four claims landed in order rather than assuming they did. The full response is in Usage examples, step 3.
--dry-run on either command prints the request it would send, with the key redacted. It is the
cheapest way to review an agent's interpretation of a request like this one before anything is
created.
"Test the issue of the Bank Account credential"
issue is one of the four workflow commands. It creates the offer, prints the offer URI and a
QR code to standard error, and polls the status until it is terminal. The agent only has to read
the final status from standard output.
Test the issue of the Bank Account credential
Bash(credenco-cli issue bank-account-sd-jwt \
--claims '{"iban":"NL91ABNA0417164300","account_name":"Ada Lovelace","currency":"EUR","account_type":"checking"}')
Scan or open:
openid-credential-offer://?credential_offer_uri=https%3A%2F%2Fwallet.acc.credenco.com%2Fpublic%2F2a0ae7d8-2b17-4d05-94ab-02a1cc74606f%2Fbank-account-issuer%2FcredentialOffer%3Fid%3Dbfbc0336-7f8c-49d6-9d4f-4d83f4abb1e4
… +12 lines (ctrl+o to expand)
The wallet accepted the offer: the issuance ended with status issue_completed (correlation id 0a590152-d44a-4b5b-8745-b7eb93bf82bf).
The QR code is left out of the capture above; the full version is in Usage examples, step 4.
Two things an agent has to get right here:
- A credential offer needs a holder. The flow only reaches
issue_completedonce a wallet accepts the offer. An agent working unattended should pass--wait=false, hand therequest_urito whoever is holding the phone, and pollcredenco-cli issue-credential get-credential-issuance-status <correlation_id>afterwards. awaiting_confirmationis terminal for this command, but not for the flow. It means the issuance is waiting for an out-of-band confirmation at a desk. Polling longer will not move it;credenco-cli issue-credential confirm-credential-issuance <correlation_id> --confirmation-code <code>will.--confirmation-codeis required: it is the code the citizen reads from their phone and passes on at the desk, so without it the command exits 2 before sending anything.
"I want to receive BankAccount credentials, with the iban and account_name attributes. Make the configuration for me"
Receiving a credential is a verifier template: a purpose shown to the holder plus a DCQL query naming the credential type and the exact claims wanted. This is the request where an agent earns its keep, because the mapping from the sentence to DCQL is mechanical but easy for a person to get wrong.
I want to receive BankAccount credentials, with the iban and account_name attributes. Make the configuration for me
Bash(credenco-cli verifier-template create-verifier-template --body @bank-account-check.json --dry-run)
{
"dryRun": true,
"method": "POST",
… +39 lines (ctrl+o to expand)
This is the request I would send. It is a dry run, so nothing is created yet:
- Verifier template
bank-account-check, interop profileDIIP_V5 - Asks for a
BankAccountCredentialin formatdc+sd-jwt - Requested claims:
ibanandaccount_name
Say go and I will create it.
Note the format: in DCQL it is the OpenID4VP identifier dc+sd-jwt under DIIP_V4, DIIP_V5
and HAIP, not the Business Wallet's own SD_JWT enum. An agent that copies SD_JWT into the
query here produces a template that matches nothing, and --dry-run is where that shows up.
"Test the receive of a BankAccount credential"
verify takes the verifier template key, creates the OpenID4VP authorization request, waits for
the holder's response to be verified, and prints the claims.
Test the receive of a BankAccount credential
Bash(credenco-cli verify bank-account-check)
Scan or open:
openid4vp://authorize?client_id=decentralized_identifier%3Adid%3Aweb%3Awallet.acc.credenco.com%3Apublic%3Adid%3Afddcd738-826f-4702-ab02-f2a5bcd97b1a&request_uri=https%3A%2F%2Fwallet.acc.credenco.com%2Fpublic%2Foid4vp%2Frequest%2F08b44511-405c-4f3b-b57d-967b7fcd154c
… +27 lines (ctrl+o to expand)
Verified. The holder disclosed exactly the requested claims:
| Claim | Value |
|---|---|
iban | NL91ABNA0417164300 |
account_name | Ada Lovelace |
currency and account_type stayed hidden.
currency and account_type are absent even though the credential carries them, because the DCQL
query asked for iban and account_name and selective disclosure kept the rest hidden. That is
the result an agent should check: not "the command exited 0", but "the claims I asked for are in
verified_data.credential_claims, and nothing else is".
Of the five verification statuses, only authorization_response_verified has verified_data.
An agent that treats the first poll as the answer gets authorization_request_created and no
claims at all, and would report a success that proved nothing. verify waits for the verified
status for exactly this reason; if you poll
credenco-cli universal-oidc4-vp get-oid4vp-request-status yourself, wait for it too.
A prompt that works
Nothing here needs a custom tool definition. Pointing the agent at the binary is enough:
You have `credenco-cli` on your PATH and a Business Wallet API key in the environment.
- Run `credenco-cli operations` and `credenco-cli <command> --help` to find out what exists. Never guess a flag or a field name.
- An API call writes JSON to standard output, because you are not a terminal. Parse it. Progress messages, QR codes and errors are on standard error. `operations`, `profile list` and `--help` are plain text instead: read them, do not parse them.
- Branch on the exit code, not on the message: 2 usage or configuration, 3 authentication, 4 not found, 5 server error.
- Before anything that creates, changes or deletes, run the command with `--dry-run`, show me the request, and wait for me to say go.
- Never pass a secret as a flag. There is no flag for one; use the environment.
- Never add `--yes` without asking me first.
Next
- Usage examples is the same walkthrough with every step spelled out.
- The credenco-cli introduction covers installation, profiles, output formats and the full exit-code table.