Create your first context and signing key

Create an isolated context on your VTA, mint a signing key inside it, and sign your first payload.

By the end of this guide, your VTA has a context set aside for one application, with a signing key inside it, and you will have used that key to sign a real payload.

Prerequisites

  • A running VTA with pnm connected. See Deploy VTA appliance or Self-hosted (open source) if you haven’t deployed one yet.
  • Your VTA’s slug to hand.
    • Affinidi-hosted: the VTA slug printed by pnm bootstrap connect.
    • Self-hosted: the slug value printed by pnm setup, which is my-vta in that guide.
  • pnm --vta <vta-slug> health shows ✓ on every check.
  • Admin access to the VTA.

Steps

Create a context

A context is a named namespace: everything you create inside it, keys included, stays isolated from every other context on the same VTA.

pnm --vta <vta-slug> contexts create \
    --id   my-app \
    --name "My App"

Replace <vta-slug> with your VTA’s slug, from the Prerequisites above.

Expected output:

  VTA: <vta-slug>
  DID: did:webvh:QmVE1TQeCtg3aavpTqasqencJpagRr8JKdGRyoZ5Qx6kRp:your-appliance.vta.affinidi.io

Context created:
  ID:        my-app
  Name:      My App
  Base Path: m/26'/2'/2'

Mint a signing key

Every context starts empty. Mint an Ed25519 key inside it so there’s something to sign with.

pnm --vta <vta-slug> keys create \
    --key-type ed25519 \
    --context  my-app \
    --label    "my-app-key-1"

Expected output:

  VTA: <vta-slug>
  DID: did:webvh:QmVE1TQeCtg3aavpTqasqencJpagRr8JKdGRyoZ5Qx6kRp:your-appliance.vta.affinidi.io

Key created:
  Key ID:          m/26'/2'/2'/0'
  Key Type:        ed25519
  Derivation Path: m/26'/2'/2'/0'
  Public Key:      z6MkvZqY2CbpWQmbjuuNshtAptH8qbFaCGk3w6Jt72xBXiGi
  Status:          active
  Label:           my-app-key-1
  Created At:      2026-09-11 19:42:44 +08:00

The Key ID is the same value as the Derivation Path: a BIP-32 path, not a short opaque ID. That matters in the next step.

Sign your first payload

pnm has no built-in sign command, but you don’t need one to try signing right now: pnm auth show-token prints your current bearer token, and the VTA’s REST API takes it from there.

A Key ID like m/26'/2'/2'/0' contains / characters, so it cannot go into a URL path unencoded: each / would be read as a path separator, and the request would 404. Percent-encode it first:

KEY_ID="m/26'/2'/2'/0'"
KEY_ID_ENCODED=$(printf '%s' "$KEY_ID" | sed "s#/#%2F#g")
TOKEN=$(pnm --vta <vta-slug> auth show-token)

curl -X POST "https://<vta-url>/keys/${KEY_ID_ENCODED}/sign" \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"payload": "aGVsbG8", "algorithm": "EdDSA"}'

Set KEY_ID to the Key ID from the previous step. For <vta-url>, use your VTA’s REST endpoint:

  • Affinidi-hosted: the VTA URL shown on the configuration page in Affinidi Portal.
  • Self-hosted: the public_url you set in setup.toml. Where that is an http:// address such as http://localhost:8100, change the https:// in the snippet to match.

payload is your message, base64url-encoded. aGVsbG8 decodes to hello. The signing key never leaves the VTA: only the signature comes back.

Confirm

The curl call above returns a JSON body containing a base64url-encoded signature. That signature is proof your key works, without ever exposing the private key itself.

You can also confirm the key exists independently of signing:

pnm --vta <vta-slug> keys list --context my-app --status active

Your new key appears in the list, with Status: active and the my-app context.

Troubleshooting

SymptomLikely causeFix
context already exists: my-appA context with this ID already exists on the VTA.Pick a different --id, or use the existing context instead of creating a new one.
unauthorized on pnm contexts createYour session does not have admin access.Run pnm auth status to check your session, then contact a VTA admin if you’re not one.
pnm keys list returns no keysThe key was minted in a different context, or --context doesn’t match.Re-run pnm --vta <vta-slug> keys list --context my-app without --status to see every key, active or not.
curl returns 401 UnauthorizedThe token from pnm auth show-token expired, or wasn’t captured correctly.Re-run TOKEN=$(pnm --vta <vta-slug> auth show-token) immediately before the curl call; tokens are short-lived.
curl returns 404 Not Found on the keyKEY_ID_ENCODED wasn’t percent-encoded, so the / characters split the URL into the wrong path, or KEY_ID doesn’t match the Key ID printed when you minted the key.Confirm KEY_ID_ENCODED has no raw / characters left in it, and that KEY_ID matches Step 2’s output exactly.
curl returns {"error":"unauthorized: invalid token: InvalidSignature"}The <vta-url> in the curl command belongs to a different VTA than the one --vta <vta-slug> fetched the token from. Each VTA signs its own tokens; one VTA’s token never verifies against another.Confirm --vta <vta-slug> on the pnm auth show-token call and <vta-url> in the curl command point at the same VTA.

Next steps