End-to-end shell workflow.
Need first credentials? Create an API client with app create-client first.
See CLI commands reference.
Prerequisites:
curljq
export BASE_URL="http://localhost:8080"
export CLIENT_ID="<client-id>"
export CLIENT_SECRET="<client-secret>"TOKEN=$(curl -s -X POST "$BASE_URL/v1/token" \
-H "Content-Type: application/json" \
-d "{\"client_id\":\"$CLIENT_ID\",\"client_secret\":\"$CLIENT_SECRET\"}" | jq -r .token)request_with_retry() {
local method="$1"
local url="$2"
local body="${3:-}"
local attempt=0
while [ "$attempt" -lt 5 ]; do
attempt=$((attempt + 1))
local headers_file
headers_file=$(mktemp)
local status
if [ -n "$body" ]; then
status=$(curl -s -o /tmp/resp.json -D "$headers_file" -w "%{http_code}" -X "$method" "$url" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$body")
else
status=$(curl -s -o /tmp/resp.json -D "$headers_file" -w "%{http_code}" -X "$method" "$url" \
-H "Authorization: Bearer $TOKEN")
fi
if [ "$status" != "429" ]; then
rm -f "$headers_file"
cat /tmp/resp.json
return 0
fi
local retry_after
retry_after=$(awk 'tolower($1)=="retry-after:" {print $2}' "$headers_file" | tr -d '\r')
rm -f "$headers_file"
sleep "${retry_after:-1}"
done
echo "request failed after retries" >&2
return 1
}curl -X POST "$BASE_URL/v1/secrets/app/prod/api-key" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"value":"YXBpLWtleS12YWx1ZQ=="}'curl "$BASE_URL/v1/secrets/app/prod/api-key" \
-H "Authorization: Bearer $TOKEN"For transit decrypt, pass ciphertext exactly as returned by encrypt (<version>:<base64-ciphertext>).
curl -X POST "$BASE_URL/v1/transit/keys" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"pii","algorithm":"aes-gcm"}'
CIPHERTEXT=$(curl -s -X POST "$BASE_URL/v1/transit/keys/pii/encrypt" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"plaintext":"am9obkBleGFtcGxlLmNvbQ=="}' | jq -r .ciphertext)
PLAINTEXT_B64=$(curl -s -X POST "$BASE_URL/v1/transit/keys/pii/decrypt" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"ciphertext\":\"$CIPHERTEXT\"}" | jq -r .plaintext)
test "$PLAINTEXT_B64" = "am9obkBleGFtcGxlLmNvbQ==" && echo "Transit round-trip verified"
# Note: `plaintext` is base64. Decode it in your app/runtime before use.Use this pattern in automation: create once, rotate when key already exists.
CREATE_STATUS=$(curl -s -o /tmp/transit-create.json -w "%{http_code}" -X POST "$BASE_URL/v1/transit/keys" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"pii","algorithm":"aes-gcm"}')
if [ "$CREATE_STATUS" = "201" ]; then
echo "Transit key created"
elif [ "$CREATE_STATUS" = "409" ]; then
echo "Transit key already exists, rotating"
curl -s -X POST "$BASE_URL/v1/transit/keys/pii/rotate" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"algorithm":"aes-gcm"}' >/tmp/transit-rotate.json
else
echo "Unexpected create status: $CREATE_STATUS"
cat /tmp/transit-create.json
exit 1
fi# First page (no cursor)
curl "$BASE_URL/v1/audit-logs?limit=50" \
-H "Authorization: Bearer $TOKEN"
# Response (abbreviated)
# {
# "data": [
# {
# "id": "01936c8e-7f2a-7b3c-9d4e-5f6a7b8c9d0e",
# "client_id": "01936c8e-1234-5678-9abc-def012345678",
# "action": "secret.read",
# "resource_type": "secret",
# "resource_path": "/app/prod/api-key",
# "created_at": "2026-03-03T10:30:00Z"
# }
# ],
# "next_cursor": "01936c8e-7f2a-7b3c-9d4e-5f6a7b8c9d0e"
# }
# Subsequent page (with after_id cursor)
curl "$BASE_URL/v1/audit-logs?limit=50&after_id=01936c8e-7f2a-7b3c-9d4e-5f6a7b8c9d0e" \
-H "Authorization: Bearer $TOKEN"
# When no more pages exist, next_cursor is omitted from response# First page (no cursor)
curl "$BASE_URL/v1/clients?limit=50" \
-H "Authorization: Bearer $TOKEN"
# Response (abbreviated)
# {
# "data": [
# {
# "id": "01936c8e-1234-5678-9abc-def012345678",
# "name": "production-app",
# "description": "Main production application",
# "is_active": true,
# "created_at": "2026-03-01T08:00:00Z"
# }
# ],
# "next_cursor": "01936c8e-1234-5678-9abc-def012345678"
# }
# Subsequent page (with after_id cursor)
curl "$BASE_URL/v1/clients?limit=50&after_id=01936c8e-1234-5678-9abc-def012345678" \
-H "Authorization: Bearer $TOKEN"# First page (no cursor)
curl "$BASE_URL/v1/secrets?limit=50" \
-H "Authorization: Bearer $TOKEN"
# Response (abbreviated)
# {
# "data": [
# {
# "path": "/app/dev/database-url",
# "version": 3,
# "created_at": "2026-03-02T14:20:00Z",
# "updated_at": "2026-03-03T09:15:00Z"
# },
# {
# "path": "/app/prod/api-key",
# "version": 1,
# "created_at": "2026-03-03T10:00:00Z",
# "updated_at": "2026-03-03T10:00:00Z"
# }
# ],
# "next_cursor": "/app/prod/api-key"
# }
# Subsequent page (with after_path cursor)
curl "$BASE_URL/v1/secrets?limit=50&after_path=/app/prod/api-key" \
-H "Authorization: Bearer $TOKEN"
# Note: Secrets use path-based cursor (after_path) instead of UUID cursor# Create a tokenization key
curl -X POST "$BASE_URL/v1/tokenization/keys" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"payment-cards","format_type":"luhn-preserving","is_deterministic":true,"algorithm":"aes-gcm"}'
# Tokenize a value
TOKENIZED=$(curl -s -X POST "$BASE_URL/v1/tokenization/keys/payment-cards/tokenize" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"plaintext":"NDUzMjAxNTExMjgzMDM2Ng==","metadata":{"last_four":"0366"},"ttl":3600}' | jq -r .token)
# Validate token
curl -X POST "$BASE_URL/v1/tokenization/validate" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"token\":\"$TOKENIZED\"}"
# Detokenize token
curl -X POST "$BASE_URL/v1/tokenization/detokenize" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"token\":\"$TOKENIZED\"}"Deterministic caveat:
- When
is_deterministic=true, the engine uses per-key version salts and HMAC-SHA256 to prevent rainbow table attacks. - Identical plaintext tokenized with the same active key version will return the same token.
- Prefer non-deterministic mode unless you explicitly need equality matching.
# First page (no cursor)
curl "$BASE_URL/v1/tokenization/keys?limit=50" \
-H "Authorization: Bearer $TOKEN"
# Response (abbreviated)
# {
# "data": [
# {
# "name": "payment-cards",
# "format_type": "luhn-preserving",
# "is_deterministic": true,
# "algorithm": "aes-gcm",
# "current_version": 1,
# "created_at": "2026-03-03T10:00:00Z"
# },
# {
# "name": "ssn-tokens",
# "format_type": "numeric",
# "is_deterministic": false,
# "algorithm": "aes-gcm",
# "current_version": 2,
# "created_at": "2026-03-02T15:30:00Z"
# }
# ],
# "next_cursor": "ssn-tokens"
# }
# Subsequent page (with after_name cursor)
curl "$BASE_URL/v1/tokenization/keys?limit=50&after_name=ssn-tokens" \
-H "Authorization: Bearer $TOKEN"
# Note: Tokenization keys use name-based cursor (after_name)# First page (no cursor)
curl "$BASE_URL/v1/transit/keys?limit=50" \
-H "Authorization: Bearer $TOKEN"
# Response (abbreviated)
# {
# "data": [
# {
# "name": "encryption-key",
# "algorithm": "aes-gcm",
# "current_version": 1,
# "min_decryption_version": 1,
# "created_at": "2026-03-01T12:00:00Z"
# },
# {
# "name": "pii",
# "algorithm": "aes-gcm",
# "current_version": 3,
# "min_decryption_version": 1,
# "created_at": "2026-03-02T08:30:00Z"
# }
# ],
# "next_cursor": "pii"
# }
# Subsequent page (with after_name cursor)
curl "$BASE_URL/v1/transit/keys?limit=50&after_name=pii" \
-H "Authorization: Bearer $TOKEN"
# Note: Transit keys use name-based cursor (after_name)- Sending raw plaintext instead of base64 in
value/plaintext - Building your own decrypt
ciphertextinstead of reusing encrypt response exactly - Missing
Bearerprefix inAuthorizationheader - Using create repeatedly for same transit key name instead of rotate after
409 - Sending token in URL path for tokenization lifecycle endpoints (the API expects token in JSON body)
- Ignoring
429and retrying immediately instead of honoringRetry-After - Using offset-based pagination parameters (removed in v1.0.0 - use cursor-based pagination instead)