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_vgOw

Keys 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.

ResourceREADWRITE
ENVIRONMENTSpayment.environment.find_one, find_manyREAD + payment.environment.create (sandbox only), delete
WALLETSpayment.wallet.find_one, find_manyREAD + payment.wallet.create, delete
WALLET_CREDENTIALSpayment.wallet.node_permissions
PAYMENTSpayment.transaction.find_*, payment.destination.find_*READ + payment.transaction.create_receive, create_send, payment.destination.create, update, delete
WEBHOOKSpayment.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 — paymenttransaction → 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:

  1. Create a new key with the same permissions.
  2. Update consumers to use the new key.
  3. 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 codeMeaning
UNAUTHENTICATEDHeader missing, key is invalid, or key has been revoked
FORBIDDENKey is valid but lacks the required resource:level

Both surface as standard GraphQL errors with extensions.code.

Next steps