Create, rotate, and revoke the API keys that authenticate SDP API requests. Each key belongs to a project, inherits its environment, and carries a role plus optional fine-grained controls — see [Authentication](/docs/developing-with-sdp/authentication) for how those layers are enforced.

The full secret is returned exactly once — at creation and at rotation. SDP stores only a salted hash and a display prefix; a lost key cannot be recovered, only rotated or replaced. Every key tracks `lastUsedAt` so you can identify stale credentials before revoking them.

## Key format

| Environment | Prefix | Solana network |
| --- | --- | --- |
| Sandbox | `sk_test_` | devnet |
| Production | `sk_live_` | mainnet-beta |

## Roles

| Role | Description |
| --- | --- |
| `api_admin` | Full access including custody and platform operations |
| `api_developer` | Read/write access, excludes custody actions |
| `api_readonly` | Read-only access to all resources |

A key can also carry explicit `permissions` that override the role's defaults. A key can only be granted permissions its creator holds.

## Create a key

<Tabs items={["Dashboard", "API"]}>
<Tab value="Dashboard">

1. Navigate to **API keys** in the sidebar. You start with an empty list.

   ![API keys page with no keys yet](/images/getting-started/api-keys-empty.png)

2. Click **New API key** in the top right, then fill in the key details:
   - **Name** — a descriptive label (e.g., "CI deploy key")
   - **Role** — Admin, Developer, or Read only
   - **Wallet access** — All wallets or Selected wallets
   - **Expiration (optional)** — date/time picker

   ![Create API key modal](/images/getting-started/api-key-create.png)

3. Click **Continue**, review the summary, and click **Create key**.

   ![Review API key modal](/images/getting-started/api-key-review.png)

4. Click **Copy** on the **API key generated** modal and save the key — it will not be shown again.

   ![API key generated modal showing the full key](/images/getting-started/api-key-generated.png)

5. Dismiss the modal. The key now appears in the table with its prefix, role, environment, and status.

   ![API keys list with one active key](/images/getting-started/api-keys-list.png)

</Tab>
<Tab value="API">

<Tabs items={["curl", "TypeScript", "Java"]}>
<Tab value="curl">
```bash title="Terminal"
curl -X POST https://api.solana.com/v1/api-keys \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "CI deploy key",
    "role": "api_developer",
    "walletScope": "all",
    "expiresAt": "2026-12-31T23:59:59Z"
  }'
```
</Tab>
<Tab value="TypeScript">
```typescript title="create-api-key.ts"
const response = await fetch("https://api.solana.com/v1/api-keys", {
  method: "POST",
  headers: {
    "Authorization": "Bearer sk_test_...",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "CI deploy key",
    role: "api_developer",
    walletScope: "all",
    expiresAt: "2026-12-31T23:59:59Z",
  }),
});
const { data } = await response.json();
// data.apiKey.key — save this, only shown once
```
</Tab>
<Tab value="Java">
```java title="CreateApiKey.java"
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.solana.com/v1/api-keys"))
    .header("Authorization", "Bearer sk_test_...")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("""
        {
          "name": "CI deploy key",
          "role": "api_developer",
          "walletScope": "all",
          "expiresAt": "2026-12-31T23:59:59Z"
        }"""))
    .build();
```
</Tab>
</Tabs>

The new key belongs to the project of the key that created it and inherits that project's environment — there is no environment field on the request. The response includes the full `key` value — **save it, it is only returned once**.

Optional fields: `description`, `permissions` (fine-grained overrides), `allowedIps` (IPv4/IPv6/CIDR ranges), `signingWalletId` / `signingWalletIds`, `walletBindings` (per-wallet permission lists).

</Tab>
</Tabs>

## Rotate a key

Rotation replaces the secret without an availability gap: the new key is a fresh record that clones the old key's role, permissions, IP allowlist, wallet bindings, and policy profiles, while the old key keeps working until its grace deadline. During the grace period both keys are valid; after it, the old key fails with `EXPIRED_API_KEY`.

<Tabs items={["Dashboard", "API"]}>
<Tab value="Dashboard">

1. In the API keys table, open the **Actions** dropdown next to the key and click **Rotate key (24h grace)**. The dashboard always uses a 24-hour grace period — use the API for a custom value (0–168h).

   ![Actions dropdown showing Rotate key and Delete key options](/images/getting-started/api-key-rotate.png)

2. Copy the new key value from the generated key modal — it is shown once.

</Tab>
<Tab value="API">

<Tabs items={["curl", "TypeScript", "Java"]}>
<Tab value="curl">
```bash title="Terminal"
curl -X POST https://api.solana.com/v1/api-keys/key_abc123/rotate \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{ "gracePeriodHours": 24 }'
```
</Tab>
<Tab value="TypeScript">
```typescript title="rotate-api-key.ts"
const response = await fetch(
  "https://api.solana.com/v1/api-keys/key_abc123/rotate",
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer sk_test_...",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ gracePeriodHours: 24 }),
  }
);
const { data } = await response.json();
// data.apiKey — the new key, only shown once
// data.previousKey.rotationDeadline — when the old key stops working
```
</Tab>
<Tab value="Java">
```java title="RotateApiKey.java"
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.solana.com/v1/api-keys/key_abc123/rotate"))
    .header("Authorization", "Bearer sk_test_...")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("""
        { "gracePeriodHours": 24 }"""))
    .build();
```
</Tab>
</Tabs>

`gracePeriodHours` accepts 0–168 (default 24). A key cannot rotate itself — authenticate the rotation with a different key or a dashboard session.

</Tab>
</Tabs>

## Revoke a key

<Tabs items={["Dashboard", "API"]}>
<Tab value="Dashboard">

Open the **Actions** dropdown next to the key and click **Delete key**. The key stops working immediately and cannot be restored.

</Tab>
<Tab value="API">

<Tabs items={["curl", "TypeScript", "Java"]}>
<Tab value="curl">
```bash title="Terminal"
curl -X DELETE https://api.solana.com/v1/api-keys/key_abc123 \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{ "confirmation": "CI deploy key" }'
```
</Tab>
<Tab value="TypeScript">
```typescript title="revoke-api-key.ts"
await fetch("https://api.solana.com/v1/api-keys/key_abc123", {
  method: "DELETE",
  headers: {
    "Authorization": "Bearer sk_test_...",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ confirmation: "CI deploy key" }),
});
```
</Tab>
<Tab value="Java">
```java title="RevokeApiKey.java"
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.solana.com/v1/api-keys/key_abc123"))
    .header("Authorization", "Bearer sk_test_...")
    .header("Content-Type", "application/json")
    .method("DELETE", HttpRequest.BodyPublishers.ofString("""
        { "confirmation": "CI deploy key" }"""))
    .build();
```
</Tab>
</Tabs>

The `confirmation` field must match the key's **name**. Revocation takes effect immediately, is idempotent (revoking an already-revoked key succeeds), and cannot be undone. As with rotation, a key cannot revoke itself.

</Tab>
</Tabs>