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

# Applications & keys

> Create the application your integration lives in, and mint the API keys it authenticates with.

An **application** is the container your integration lives in; its **keys** are what
you authenticate with. Every other endpoint in this reference needs a key, so this
is where an integration starts.

## Two ways in

A key can't be the entry point to itself. A key belongs to an application, and an
application has to be created by someone already signed in. So these endpoints
accept **either** credential:

* a **session**, for bootstrapping before any key exists, `custral login → pick an org → create an application → mint a key`
* an **API key** holding **`applications:manage`**, for managing applications programmatically afterwards

<Warning>
  A key may not mint a key with scopes it does not itself hold. That request is
  refused with `insufficient_scope`. Escalating beyond your current key is a job
  for the dashboard or the CLI, which authenticate as a signed-in human.
</Warning>

<Note>
  You can also do all of this from **Settings → Applications** in the dashboard, or
  from an MCP client. See [MCP](/dev/mcp/overview). Key *revocation* is
  deliberately not exposed over MCP, since it is immediate and irreversible.
</Note>

## The Application object

<ResponseExample>
  ```json The Application object theme={null}
  {
    "id": "app_3Ab9xK2mQ7",
    "name": "Billing Sync",
    "description": "Mirrors invoices into Deals",
    "type": "internal",
    "status": "private",
    "rails": "internal",
    "url": "",
    "createdAt": "2026-07-23T18:04:11.000Z",
    "updatedAt": "2026-07-23T18:04:11.000Z"
  }
  ```
</ResponseExample>

### Attributes

<ResponseField name="id" type="string">
  Unique identifier, prefixed `app_`.
</ResponseField>

<ResponseField name="name" type="string">
  Human-readable name, shown in Settings → Applications.
</ResponseField>

<ResponseField name="type" type="string">
  `internal` for something you run yourself, `workflow` for an app that plugs into
  workflows.
</ResponseField>

<ResponseField name="status" type="string">
  `private`, `unlisted`, or `public`, who can discover the application.
</ResponseField>

<ResponseField name="rails" type="string">
  How the application is invoked, `internal` or `webhook`.
</ResponseField>

## The API key object

Key **metadata**. The secret itself is returned exactly once, at creation, and is
hashed at rest. Nothing can retrieve it afterwards. It is excluded from this shape
and from the service that lists keys, so no endpoint can return it.

<ResponseExample>
  ```json The API key object theme={null}
  {
    "id": "key_5Bc0xJ1kP4",
    "applicationId": "app_3Ab9xK2mQ7",
    "name": "production",
    "type": "secret",
    "keyPreview": "sk_live_abc…",
    "scopes": ["records:read", "records:write", "objects:read"],
    "environment": "live",
    "status": "active",
    "expiresAt": null,
    "lastUsedAt": "2026-07-23T19:12:44.000Z",
    "createdAt": "2026-07-23T18:04:11.000Z"
  }
  ```
</ResponseExample>

### Attributes

<ResponseField name="id" type="string">
  Unique identifier, prefixed `key_`. This is what you pass to revoke a key.
</ResponseField>

<ResponseField name="type" type="string">
  `secret` (`sk_…`) for server-side use, or `publishable` (`pk_…`) for the browser.
</ResponseField>

<ResponseField name="keyPreview" type="string">
  A non-sensitive prefix for identifying the key in a list. Not usable for auth.
</ResponseField>

<ResponseField name="scopes" type="string[]">
  What the key may do. **An empty array means the key can only call `/v1/me`**.
  It authenticates but authorises nothing.
</ResponseField>

<ResponseField name="environment" type="string">
  `live` or `test`.
</ResponseField>

<ResponseField name="status" type="string">
  `active`, or `inactive` once revoked.
</ResponseField>

<ResponseField name="expiresAt" type="string | null">
  ISO 8601 expiry, or `null` for a key that doesn't expire.
</ResponseField>

<ResponseField name="lastUsedAt" type="string | null">
  When the key last authenticated a request.
</ResponseField>

## Create an application

<ParamField body="name" type="string" required>
  A human-readable name. Shown in Settings → Applications.
</ParamField>

<ParamField body="type" type="string" required>
  `internal` for something you run yourself, `workflow` for an app that plugs into
  workflows.
</ParamField>

<ParamField body="status" type="string" default="private">
  `private`, `unlisted`, or `public`, who can discover the application.
</ParamField>

<RequestExample>
  ```bash CLI theme={null}
  custral api POST applications '{"name":"Billing Sync","type":"internal"}'
  ```

  ```bash curl theme={null}
  curl -X POST https://api.custral.com/v1/applications \
    -H "Authorization: Bearer sk_live_..." \
    -H "Content-Type: application/json" \
    -d '{"name":"Billing Sync","type":"internal"}'
  ```

  ```ts SDK theme={null}
  const app = await custral.applications.create({
    name: "Billing Sync",
    type: "internal",
  });
  ```
</RequestExample>

<ResponseExample>
  ```json Response theme={null}
  {
    "id": "app_3Ab9xK2mQ7",
    "name": "Billing Sync",
    "type": "internal",
    "status": "private"
  }
  ```
</ResponseExample>

## Mint a key

<Warning>
  **Always pass `scopes`.** A key minted without them authenticates fine and is
  authorised for nothing. Every endpoint answers `insufficient_scope` and only
  `/v1/me` works. It looks like a working key right up until the first real call.
</Warning>

<ParamField body="type" type="string" required>
  `secret` (`sk_…`) for server-side use, or `publishable` (`pk_…`) for the browser.
</ParamField>

<ParamField body="scopes" type="string[]">
  What the key may do. See [Scopes](#scopes).
</ParamField>

<ParamField body="environment" type="string" default="live">
  `live` or `test`.
</ParamField>

<ParamField body="expiresInDays" type="integer">
  Auto-expire the key after N days. Omit for a key that doesn't expire.
</ParamField>

The plaintext key comes back **once**. It's hashed at rest, so nothing can show it
to you again. Store it before you close the terminal; if you lose it, revoke it and
mint another.

<RequestExample>
  ```bash CLI theme={null}
  custral api POST applications/app_3Ab9xK2mQ7/keys \
    '{"type":"secret","name":"production","scopes":["records:read","records:write","objects:read"]}'
  ```

  ```ts SDK theme={null}
  const {plaintextKey} = await custral.applications.createKey("app_3Ab9xK2mQ7", {
    type: "secret",
    name: "production",
    scopes: ["records:read", "records:write", "objects:read"],
  });
  // Shown once — store it now.
  ```
</RequestExample>

<ResponseExample>
  ```json Response theme={null}
  {
    "key": {
      "id": "key_5Bc0xJ1kP4",
      "keyPreview": "sk_live_abc…",
      "scopes": ["records:read", "records:write", "objects:read"],
      "environment": "live"
    },
    "plaintextKey": "sk_live_AbC…the-only-time-you-see-this"
  }
  ```
</ResponseExample>

## Verify a key

`GET /v1/me` is the "does this key work, and what can it do?" probe. Any valid key
may call it, no scope required. The fastest way to confirm a key you just minted
carries the scopes you meant.

<RequestExample>
  ```bash curl theme={null}
  curl https://api.custral.com/v1/me \
    -H "Authorization: Bearer sk_live_..."
  ```

  ```ts SDK theme={null}
  const me = await custral.me();
  ```
</RequestExample>

<ResponseExample>
  ```json Response theme={null}
  {
    "orgId": "org_3Ab9xK2mQ7",
    "organizationName": "Acme Inc",
    "applicationId": "app_3Ab9xK2mQ7",
    "keyId": "key_5Bc0xJ1kP4",
    "keyPreview": "sk_live_abc…",
    "environment": "live",
    "scopes": ["records:read", "records:write", "objects:read"]
  }
  ```
</ResponseExample>

## Revoke a key

Revocation takes effect immediately, every integration holding the key breaks at
once, and it cannot be restored.

The response carries a **`success`** flag: `true` when the key was deactivated, and
`false` when no key matched the id. A key that was already revoked, or never
existed. A missing key is reported as `success: false`, not as an error.

<RequestExample>
  ```bash CLI theme={null}
  custral api DELETE applications/app_3Ab9xK2mQ7/keys/key_5Bc0xJ1kP4
  ```

  ```bash curl theme={null}
  curl -X DELETE https://api.custral.com/v1/applications/app_3Ab9xK2mQ7/keys/key_5Bc0xJ1kP4 \
    -H "Authorization: Bearer sk_live_..."
  ```

  ```ts SDK theme={null}
  const {success} = await custral.applications.revokeKey(
    "app_3Ab9xK2mQ7",
    "key_5Bc0xJ1kP4",
  );
  ```
</RequestExample>

<ResponseExample>
  ```json Response theme={null}
  {
    "success": true
  }
  ```
</ResponseExample>

## Scopes

| Scope                 | Grants                                            |
| --------------------- | ------------------------------------------------- |
| `records:read`        | Read records                                      |
| `records:write`       | Create + update records                           |
| `objects:read`        | Read the object/property schema                   |
| `conversations:read`  | Read conversations + dispositions                 |
| `conversations:write` | Update, create, and add messages to conversations |
| `tasks:read`          | Read tasks                                        |
| `applications:manage` | Create applications and mint keys                 |
| `webhooks:manage`     | Register webhooks                                 |
| `mcp:read`            | Connect an [MCP client](/dev/mcp/overview)        |
| `mcp:write`           | Let an MCP client take write actions              |

Grant the narrowest set that works. `applications:manage` is the one to be careful
with. A key holding it can mint further keys.


## Related topics

- [Authentication](/dev/auth/overview.md)
- [API Reference](/dev/api-reference/overview.md)
- [Install the widget](/comms/chat/widget-install.md)
- [Errors](/dev/errors/overview.md)
- [Applications & API keys](/dev/applications.md)
