> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gr4vy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Managing API keys

> Provision and manage API key pairs over the API, including multi-account keys, your own public keys, and enabling or disabling keys.

API key pairs can be provisioned and managed over the API, in addition to the
[dashboard](/guides/dashboard/integrations/api-keys). This lets you automate
key provisioning at scale, such as issuing a key for each merchant during an
independent sales organization (ISO) onboarding flow.

## The Key Manager role

Managing API keys over the API requires the **Key Manager** role. This role is
scoped strictly to the API key lifecycle through the `api-key-pairs.read` and
`api-key-pairs.write` permissions. It grants no access to payments or back-office
data. The Administrator role can also manage keys, but Key Manager keeps key
management separate from payments and configuration access.

Because Key Manager grants no access of its own, it cannot be the only role on a
key. Pair it with the role you want the key to operate under. A key holding both
**Key Manager** and **Integration** has the permissions of both, so it can
provision further **Integration** keys without being an Administrator.

Key Manager is not granted by default and is only assignable to an API key.
Assign it from the [dashboard](/guides/dashboard/integrations/api-keys) to the
key that provisions your other keys.

<Note>
  A role that grants `api-key-pairs.write`, which means **Administrator** and
  **Key Manager**, can only be assigned by a dashboard user. An API key cannot
  create or update another key with either role, even when it holds Key Manager
  itself.
</Note>

## Find role IDs

`role_ids` takes role IDs, not role names. List the roles available in your
instance with a `GET` to the `/roles` endpoint, then match on `slug`, which is
unique and does not change.

Listing roles requires the `roles.read` scope. The `key-manager` role holds it,
so a key that provisions other keys can read the catalogue without needing full
access.

The endpoint is paged with `cursor` and `limit`, returning 20 roles per page by
default and up to 100. Page through the results rather than assuming one call
returns the whole catalogue.

<CodeGroup>
  ```csharp C# theme={"system"}
  ListRolesResponse? res = await client.Roles.ListAsync();

  while (res != null)
  {
      // handle items

      res = await res.Next!();
  }
  ```

  ```go Go theme={"system"}
  res, err := client.Roles.List(ctx, nil, nil)
  if err != nil {
    log.Fatal(err)
  }

  for res != nil {
    // handle items

    res, err = res.Next()
    if err != nil {
      log.Fatal(err)
    }
  }
  ```

  ```java Java theme={"system"}
  gr4vyClient.roles().list()
    .callAsStream()
    .forEach((ListRolesResponse page) -> {
      // handle page
    });
  ```

  ```php PHP theme={"system"}
  $responses = $client->roles->list();

  foreach ($responses as $response) {
    // handle response
  }
  ```

  ```python Python theme={"system"}
  res = client.roles.list()

  while res is not None:
      # handle items

      res = res.next()
  ```

  ```ts TypeScript theme={"system"}
  const result = await gr4vy.roles.list();

  for await (const page of result) {
    // handle page
  }
  ```
</CodeGroup>

Each role in the response describes how it can be used.

| Field | Description |
| - | - |
| `slug` | The unique, stable identifier for the role, such as `integration`. Match on this rather than on `name`. |
| `assignable_to` | Whether the role can be given to a `user`, an `api-key-pair`, or both. |
| `applies_to` | The slugs of the roles this role extends. Empty when the role is not an add-on. |
| `is_standalone_assignable` | Whether the role can be assigned on its own, or has to be combined with another role. |

Roles are shared across every merchant account, so the
`X-Gr4vy-Merchant-Account-Id` header has no effect here. The SDKs send it on
every request, and this endpoint returns the same roles either way.

See the [List roles](/reference/roles/list-roles) reference for the full response
body.

## Create a key

Create a key pair with a `POST` to the `/api-key-pairs` endpoint. A key requires a
`display_name` and at least one role in `role_ids`. See
[Find role IDs](#find-role-ids) to look up the ID for a role.

<CodeGroup>
  ```csharp C# theme={"system"}
  var apiKeyPair = await client.ApiKeyPairs.CreateAsync(
      new APIKeyPairCreate()
      {
          DisplayName = "Onboarding service",
          RoleIds = new List<string>() { "<role-id>" },
          MerchantAccountIds = new List<string>() { "default", "acme-au" },
      }
  );
  ```

  ```go Go theme={"system"}
  apiKeyPair, err := client.APIKeyPairs.Create(ctx, components.APIKeyPairCreate{
    DisplayName: "Onboarding service",
    RoleIds: []string{
      "<role-id>",
    },
    MerchantAccountIds: []string{
      "default",
      "acme-au",
    },
  })
  ```

  ```java Java theme={"system"}
  CreateApiKeyPairResponse response = gr4vyClient.apiKeyPairs().create()
    .request(APIKeyPairCreate.builder()
      .displayName("Onboarding service")
      .roleIds(List.of("<role-id>"))
      .merchantAccountIds(List.of("default", "acme-au"))
      .build())
    .call();
  ```

  ```php PHP theme={"system"}
  $apiKeyPairCreate = new APIKeyPairCreate(
    displayName: 'Onboarding service',
    roleIds: ['<role-id>'],
    merchantAccountIds: ['default', 'acme-au'],
  );
  $response = $client->apiKeyPairs->create(
    request: $apiKeyPairCreate
  );
  ```

  ```python Python theme={"system"}
  api_key_pair: models.APIKeyPair = client.api_key_pairs.create(
      display_name="Onboarding service",
      role_ids=["<role-id>"],
      merchant_account_ids=["default", "acme-au"],
  )
  ```

  ```ts TypeScript theme={"system"}
  const apiKeyPair = await gr4vy.apiKeyPairs.create({
    displayName: "Onboarding service",
    roleIds: ["<role-id>"],
    merchantAccountIds: ["default", "acme-au"],
  });
  ```
</CodeGroup>

By default, Gr4vy generates the key pair and returns the `private_key` once, in
the response to this request. Store it securely, as it cannot be retrieved again.

See the [Create API key](/reference/api-key-pairs/new-api-key-pair) reference for
more detail on this endpoint.

## Assign multiple roles

A key can hold more than one role, in which case it has the combined permissions
of all of them. Pass every role ID in `role_ids`. On a `PUT`, the list you send
replaces the roles the key currently has. Each role can appear only once, and a
list containing the same role twice is rejected.

The main use for this is key provisioning. A key holding **Key Manager** and
**Integration** can create and manage **Integration** keys, because a caller can
only assign roles whose permissions are a subset of its own.

## Role assignment rules

Gr4vy validates the roles an assignee ends up with whenever you create or update
a user or an API key pair. A request that breaks one of these rules is rejected
with a `400` response.

* **Full access cannot be combined.** A role that already implies the access of
  every other role has to be the only role in the set.
* **The role has to suit the assignee.** A role is only accepted when its
  `assignable_to` list includes that kind of assignee. `integration` and
  `key-manager` are API key roles, so they cannot be given to a dashboard user.
* **At least one role has to stand alone.** The resulting set needs one role with
  `is_standalone_assignable` set to `true`.
* **An add-on needs one of its targets.** A role with a non-empty `applies_to`
  has to be accompanied by one of the roles it lists.
* **Key management needs a dashboard user.** A role granting `api-key-pairs.write`
  can only be assigned from the dashboard, never by an API key. This stops a
  leaked key from replicating itself.
* **You cannot grant more than you hold.** A caller can only assign roles whose
  permissions are a subset of its own.
* **You cannot remove more than you hold either.** The same subset rule applies
  to the roles being taken away, so a User Manager cannot strip a role it could
  not have granted.

These rules are applied to the set of roles the assignee ends up with, not only
to the roles being added. Removing a role can therefore be rejected as well, for
example when it would leave an add-on without its target.

## Provide your own public key

Rather than have Gr4vy generate the key pair, you can supply your own public key
in the `public_key` field on create. When you do, Gr4vy stores the public key and
does not generate or return a private key, so the private key never leaves your
systems.

The public key must be a PEM-encoded ECDSA key on the P-521 (ES512) curve. RSA
keys are not accepted. Set `algorithm` to match, and note that the public key is
immutable for the life of the key.

<CodeGroup>
  ```csharp C# theme={"system"}
  var apiKeyPair = await client.ApiKeyPairs.CreateAsync(
      new APIKeyPairCreate()
      {
          DisplayName = "Onboarding service",
          RoleIds = new List<string>() { "<role-id>" },
          Algorithm = "ES512",
          PublicKey =
              "-----BEGIN PUBLIC KEY-----\n<pem-encoded-es512-public-key>\n-----END PUBLIC KEY-----",
      }
  );
  ```

  ```go Go theme={"system"}
  apiKeyPair, err := client.APIKeyPairs.Create(ctx, components.APIKeyPairCreate{
    DisplayName: "Onboarding service",
    RoleIds: []string{
      "<role-id>",
    },
    Algorithm: "ES512",
    PublicKey: gr4vy.String("-----BEGIN PUBLIC KEY-----\n<pem-encoded-es512-public-key>\n-----END PUBLIC KEY-----"),
  })
  ```

  ```java Java theme={"system"}
  CreateApiKeyPairResponse response = gr4vyClient.apiKeyPairs().create()
    .request(APIKeyPairCreate.builder()
      .displayName("Onboarding service")
      .roleIds(List.of("<role-id>"))
      .algorithm("ES512")
      .publicKey("-----BEGIN PUBLIC KEY-----\n<pem-encoded-es512-public-key>\n-----END PUBLIC KEY-----")
      .build())
    .call();
  ```

  ```php PHP theme={"system"}
  $apiKeyPairCreate = new APIKeyPairCreate(
    displayName: 'Onboarding service',
    roleIds: ['<role-id>'],
    algorithm: 'ES512',
    publicKey: "-----BEGIN PUBLIC KEY-----\n<pem-encoded-es512-public-key>\n-----END PUBLIC KEY-----",
  );
  $response = $client->apiKeyPairs->create(
    request: $apiKeyPairCreate
  );
  ```

  ```python Python theme={"system"}
  api_key_pair: models.APIKeyPair = client.api_key_pairs.create(
      display_name="Onboarding service",
      role_ids=["<role-id>"],
      algorithm="ES512",
      public_key="-----BEGIN PUBLIC KEY-----\n<pem-encoded-es512-public-key>\n-----END PUBLIC KEY-----",
  )
  ```

  ```ts TypeScript theme={"system"}
  const apiKeyPair = await gr4vy.apiKeyPairs.create({
    displayName: "Onboarding service",
    roleIds: ["<role-id>"],
    algorithm: "ES512",
    publicKey:
      "-----BEGIN PUBLIC KEY-----\n<pem-encoded-es512-public-key>\n-----END PUBLIC KEY-----",
  });
  ```
</CodeGroup>

## Issue a key for multiple merchant accounts

A single key can be scoped to more than one merchant account by passing an array
of merchant account IDs in `merchant_account_ids`. This is useful when one ISO or
brand groups several merchant accounts.

The list is editable after creation. Add or remove merchant accounts with a `PUT`
to the key. This does not regenerate the key or its private key.

To grant a key access to all merchant accounts, pass an empty list. You can also
omit `merchant_account_ids` entirely. This is only allowed when the caller can
already access all merchant accounts.

## Turn a key on or off

Each key has an `active` field that you can update over the API and in the
dashboard. Set `active` to `false` to turn off a key. A request authenticated
with an off key is rejected with a `401 Unauthorized` response, which lets you
revoke access immediately without deleting the key.

<CodeGroup>
  ```csharp C# theme={"system"}
  var apiKeyPair = await client.ApiKeyPairs.UpdateAsync(
      apiKeyPairId: "<api-key-pair-id>",
      apiKeyPairUpdate: new APIKeyPairUpdate()
      {
          Active = false,
      }
  );
  ```

  ```go Go theme={"system"}
  apiKeyPair, err := client.APIKeyPairs.Update(ctx, "<api-key-pair-id>", components.APIKeyPairUpdate{
    Active: gr4vy.Bool(false),
  })
  ```

  ```java Java theme={"system"}
  UpdateApiKeyPairResponse response = gr4vyClient.apiKeyPairs().update()
    .apiKeyPairId("<api-key-pair-id>")
    .apiKeyPairUpdate(APIKeyPairUpdate.builder()
      .active(false)
      .build())
    .call();
  ```

  ```php PHP theme={"system"}
  $apiKeyPairUpdate = new APIKeyPairUpdate(
    active: false,
  );
  $response = $client->apiKeyPairs->update(
    apiKeyPairId: '<api-key-pair-id>',
    apiKeyPairUpdate: $apiKeyPairUpdate
  );
  ```

  ```python Python theme={"system"}
  api_key_pair: models.APIKeyPair = client.api_key_pairs.update(
      api_key_pair_id="<api-key-pair-id>",
      active=False,
  )
  ```

  ```ts TypeScript theme={"system"}
  const apiKeyPair = await gr4vy.apiKeyPairs.update(
    {
      active: false,
    },
    "<api-key-pair-id>",
  );
  ```
</CodeGroup>

## Track when a key was last used

Each API key pair includes a `last_used_at` field with the date and time it was
last used to authenticate a request, or `null` if it has never been used. This
also appears as the **Last used** column in the **API Keys** table in the
[dashboard](/guides/dashboard/integrations/api-keys). Use it to audit dormant
keys before disabling or deleting them.

`last_used_at` updates asynchronously and can lag behind the most recent
request by up to a few minutes.

## Lifecycle webhooks

Gr4vy emits [webhook events](/guides/features/webhooks/events) as keys change, so
you can keep an audit trail of provisioning:

* `api-key-pair.created`
* `api-key-pair.updated`
* `api-key-pair.deleted`

The `api-key-pair.updated` event covers all changes to an existing key, including
enabling or disabling it and editing its merchant accounts. Subscribe to these
events through the existing [webhook subscription](/reference/webhook-subscriptions/new-webhook-subscription)
mechanism.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.