API Keys
Create and manage service API keys for the Amboss Payments API, scoped per environment with explicit resource permissions.
Service API keys authenticate backend calls to the Payments API. Every key is scoped to a single environment and carries an explicit resource permission set.
Payments uses the x-api-key header, not Authorization: Bearer. The
other Amboss products (Space, Reflex, Magma) use Bearer tokens — see
Developer → Authentication for the per-product
table.
x-api-key: amb_live_PvDaoBz6TXhuMKxk0VkckiPUTX9BTSDyCT2Pqc_vgOwKeys look like amb_test_… (sandbox environments) or amb_live_… (live environments). The prefix tells you which environment the key belongs to at a glance.
Permissions
Permissions are <resource>:<level> pairs. WRITE implies READ. Grant the minimum set your integration needs.
| Resource | READ | WRITE |
|---|---|---|
ENVIRONMENTS | payment.environment.find_one, find_many | READ + payment.environment.create (sandbox only), delete |
WALLETS | payment.wallet.find_one, find_many | READ + payment.wallet.create, delete |
WALLET_CREDENTIALS | payment.wallet.node_permissions | — |
PAYMENTS | payment.transaction.find_*, payment.destination.find_* | READ + payment.transaction.create_receive, create_send, payment.destination.create, update, delete |
WEBHOOKS | payment.webhook_endpoint.find_*, payment.webhook_event.* | READ + payment.webhook_endpoint.create, update, rotate_secret, delete |
WRITE implies READ. Grant the minimum set your integration needs.
The dotted paths above (e.g. payment.transaction.create_receive) reflect the nested structure of the GraphQL API. Each segment is a nested resolver — payment → transaction → operation. For example, payment.transaction.create_receive maps to:
mutation {
payment {
transaction {
create_receive(input: { … }) { … }
}
}
}Similarly, payment.wallet.create maps to mutation { payment { wallet { create(input: { … }) { … } } } }, and so on.
WALLET_CREDENTIALS is READ-only — there is no write operation behind it.
You can additionally scope a key to a single wallet_id to limit blast radius — the key will only work against that wallet.
Wallet-scoped credential keys
WALLET_CREDENTIALS reads encrypted node macaroons via PaymentsWallet.node_permissions. Because that field exposes per-node credentials, issuance applies stricter requirements:
- The key must also be granted
WALLETS: READ(so it can resolve the parent wallet). - The key must be bound to a specific
wallet_id— an unscoped key with this permission is rejected. - At runtime, the team must have a team password set, and the caller must pass
password_hash(Argon2id of the master-account password — see Rails Security) on every request. A team without a team password cannot read wallet credentials via API key.
Creating live environments still requires the master account. A key with
ENVIRONMENTS: WRITE can create SANDBOX environments programmatically
(useful for spinning up per-tenant or per-CI-run envs), but
payment.environment.create rejects type: LIVE from API keys — only the
master-account dashboard JWT can promote to live.
Create a key
Key creation requires master-account dashboard auth (Authorization: Bearer <dashboard-token>), not another API key. This is intentional: a leaked API key cannot mint a fresh one, which blocks privilege-escalation.
Likewise, listing and revoking keys go through the dashboard JWT — no service_api_keys:write permission exists on keys themselves.
mutation CreateApiKey {
service_api_key {
create(
input: {
name: "backend-prod"
environment_id: "81b73615-ddf3-46e3-943a-467c3e442e04"
permissions: [
{ resource: WALLETS, level: WRITE }
{ resource: PAYMENTS, level: WRITE }
{ resource: WEBHOOKS, level: WRITE }
]
}
) {
plaintext_key
key {
id
name
key_prefix
key_suffix
permissions {
resource
level
}
}
}
}
}curl -X POST https://rails.amboss.tech/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $AMBOSS_DASHBOARD_TOKEN" \
-d '{
"query": "mutation($input: CreateServiceApiKeyInput!) { service_api_key { create(input: $input) { plaintext_key key { id name key_prefix permissions { resource level } } } } }",
"variables": {
"input": {
"name": "backend-prod",
"environment_id": "81b73615-ddf3-46e3-943a-467c3e442e04",
"permissions": [
{ "resource": "WALLETS", "level": "WRITE" },
{ "resource": "PAYMENTS", "level": "WRITE" },
{ "resource": "WEBHOOKS", "level": "WRITE" }
]
}
}
}'import { GraphQLClient, gql } from "graphql-request";
const client = new GraphQLClient("https://rails.amboss.tech/graphql", {
headers: { Authorization: `Bearer ${process.env.AMBOSS_DASHBOARD_TOKEN}` },
});
const CREATE_API_KEY = gql`
mutation CreateApiKey($input: CreateServiceApiKeyInput!) {
service_api_key {
create(input: $input) {
plaintext_key
key {
id
name
key_prefix
permissions {
resource
level
}
}
}
}
}
`;
const { service_api_key } = await client.request(CREATE_API_KEY, {
input: {
name: "backend-prod",
environment_id: process.env.AMBOSS_ENV_ID,
permissions: [
{ resource: "WALLETS", level: "WRITE" },
{ resource: "PAYMENTS", level: "WRITE" },
{ resource: "WEBHOOKS", level: "WRITE" },
],
},
});
// Save plaintext_key in your secret manager — it will never be shown again.
const apiKey = service_api_key.create.plaintext_key;Example response:
{
"data": {
"service_api_key": {
"create": {
"plaintext_key": "amb_test_PvDaoBz6TXhuMKxk0VkckiPUTX9BTSDyCT2Pqc_vgOw",
"key": {
"id": "28826356-6213-4aad-ae56-8c86e7cf8923",
"name": "backend-prod",
"key_prefix": "amb_test_PvDa",
"key_suffix": "vgOw",
"permissions": [
{ "resource": "WALLETS", "level": "WRITE" },
{ "resource": "PAYMENTS", "level": "WRITE" },
{ "resource": "WEBHOOKS", "level": "WRITE" }
]
}
}
}
}
}plaintext_key is returned once at creation. It's never retrievable
afterwards. Store it in your secret manager immediately. If you lose it,
revoke the key and create a new one.
Example: wallet-scoped credentials key
To mint a key that can only read node credentials for a single wallet:
mutation CreateCredentialsKey {
service_api_key {
create(
input: {
name: "wallet-credentials-prod"
environment_id: "81b73615-ddf3-46e3-943a-467c3e442e04"
wallet_id: "5e4b1e2a-9f3c-4a5b-8c7d-1234567890ab"
permissions: [
{ resource: WALLETS, level: READ }
{ resource: WALLET_CREDENTIALS, level: READ }
]
}
) {
plaintext_key
key {
id
name
key_prefix
}
}
}
}Issuance fails if WALLET_CREDENTIALS is granted without WALLETS, or without a wallet_id.
List keys
query ListApiKeys($environment_id: String!) {
service_api_keys {
get_many(environment_id: $environment_id) {
list {
id
name
key_prefix
key_suffix
active
last_used_at
expires_at
permissions {
resource
level
}
}
}
}
}Keys never expose their plaintext on listing — only the prefix/suffix and metadata. Use the prefix to identify keys in logs.
Rotate a key
There's no rotate_secret mutation for API keys (unlike webhook endpoints). The pattern is:
- Create a new key with the same permissions.
- Update consumers to use the new key.
- Revoke the old key once traffic has shifted.
mutation RevokeKey($id: String!) {
service_api_key {
revoke(id: $id) {
id
active
}
}
}Revoked keys can no longer authenticate; outstanding requests in flight will return UNAUTHENTICATED on the next call.
Auth errors
| GraphQL error code | Meaning |
|---|---|
UNAUTHENTICATED | Header missing, key is invalid, or key has been revoked |
FORBIDDEN | Key is valid but lacks the required resource:level |
Both surface as standard GraphQL errors with extensions.code.