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

# Credentials

> Store one set of per-organization credentials, use it everywhere it fits, and rotate it in one place

A credential is one real set of secrets — a Five9 API login, an OAuth2 app registration, an API token. It belongs to the **tenant** it can authenticate to (see [platform tenancy](/concepts/platform-tenancy)), not to any one service, and it serves **every compatible service of that tenant**. Tasks never carry secret values themselves; they reference a credential by id, and the platform resolves the material at runtime. Credentials belong to your organization and are never visible to another account.

```json theme={null}
{
  "task_id": "add_contact",
  "task_name": "Add to dialing list",
  "platform_id": "five9",
  "service_id": "five9__configuration_service",
  "action_id": "five9__configuration_service__add_to_list",
  "connection_id": "cred_e5f6g7h8",
  "parameters": { "list_name": "Outbound Q3" }
}
```

<Note>
  The task field is spelled `connection_id` and its value is a `cred_` id. The field name is fixed by workflow-history compatibility and is not going to change; read it as "the credential this task runs with". Built-in utility actions (like `getdialed__utils__echo`) need none — their service reports `requires_connection: false`.
</Note>

## One credential, many services

The thing you actually possess is a login, not a login-per-service. So one credential record covers every service of its tenant that accepts its authentication method:

* **You create it once.** Supply the platform, the authentication method and the secret material. There is no service to pick.
* **It is offered wherever it fits.** A Five9 basic-auth credential is available to every Five9 service that takes basic auth, on the Five9 account it authenticates to.
* **You rotate it once.** One update reaches every flow using it — see [rotating a secret](#rotating-a-secret).

## The credential object

| Field                | Type             | Description                                                                                                                                                                                                                                      |
| -------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id`                 | string           | Credential identifier (e.g. `cred_e5f6g7h8`).                                                                                                                                                                                                    |
| `name`               | string           | Human-readable name you choose (e.g. "Five9 Production"). This is how the credential is identified when you pick one.                                                                                                                            |
| `platform_id`        | string           | Platform from the [catalog](/concepts/catalog) (e.g. `five9`). Fixed for the credential's whole life.                                                                                                                                            |
| `auth_method`        | string           | How the credential presents itself: `basic_auth`, `api_key`, `jwt`, `oauth2`, or `aws_access_key`.                                                                                                                                               |
| `api_key_placement`  | object \| null   | For `api_key`: **where** the key is presented — a header name with an optional scheme, or a query-parameter name. Required for that method, meaningless for the others. See [declaring where an API key goes](#declaring-where-an-api-key-goes). |
| `credentials_ref`    | string           | Reference to where the secret material is stored. The secret itself is resolved from secure storage at runtime and is never returned by the API.                                                                                                 |
| `tenancy_id`         | string \| null   | The [tenant](/concepts/platform-tenancy) this credential belongs to. **Server-managed: you never send it.** `null` while unbound.                                                                                                                |
| `tenant`             | object \| null   | The same tenant, resolved — `id`, `label`, `platform_id`, `is_manual` — so a picker row can show a label without a second request.                                                                                                               |
| `grant_type`         | string \| null   | For `oauth2`: the grant in use (e.g. `client_credentials`).                                                                                                                                                                                      |
| `token_url`          | string \| null   | For `oauth2`: the authorization server's token endpoint. Required when `auth_method` is `oauth2`.                                                                                                                                                |
| `scopes`             | string\[]        | For `oauth2`: the scopes requested.                                                                                                                                                                                                              |
| `audience`           | string \| null   | For `oauth2`: the provider's identifier for the API a token is requested **for**, sent with the token request. Some authorization servers require it and issue an unusable token — or none — without it.                                         |
| `aws_region`         | string \| null   | For `aws_access_key`: the AWS region the credential works in. **Required** for that method and rejected on every other. See [AWS access key](#aws-access-key).                                                                                   |
| `aws_default_bucket` | string \| null   | For `aws_access_key`: the bucket a storage step uses when it names none itself.                                                                                                                                                                  |
| `aws_endpoint_url`   | string \| null   | For `aws_access_key`: an S3-compatible endpoint to use instead of Amazon's. See [S3-compatible storage](#s3-compatible-storage).                                                                                                                 |
| `telemetry`          | object           | The [observed-use record](#what-a-credential-can-actually-do), keyed by `service_id`.                                                                                                                                                            |
| `status`             | string           | `active`, `expired`, or `revoked`. Yours to set; never flipped automatically, even when a test fails.                                                                                                                                            |
| `last_verified`      | datetime \| null | When the material last passed a liveness check. Cleared by a rotation.                                                                                                                                                                           |
| `created_at`         | datetime         | When the credential was created.                                                                                                                                                                                                                 |

### Authentication methods

`auth_method` describes **only** the credential material and how it is presented. It says nothing about the transport — whether a service speaks SOAP or REST is the service's own `protocol` field in the catalog.

| Value            | Material                                                                                                                                                                                                 |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `basic_auth`     | A username and password.                                                                                                                                                                                 |
| `api_key`        | A single token presented as a key. Say [where the key goes](#declaring-where-an-api-key-goes).                                                                                                           |
| `jwt`            | A signed token.                                                                                                                                                                                          |
| `oauth2`         | A client id and client secret, exchanged at `token_url` for an access token. Carries `grant_type`, and `audience` where the provider requires one.                                                       |
| `aws_access_key` | An AWS access key **pair** — an id and a secret — used to sign each request. Carries `aws_region`, and optionally a default bucket and an S3-compatible endpoint. See [AWS access key](#aws-access-key). |

A catalog service publishes the method it accepts as its own `auth_method`, which additionally may be `none` — meaning the service needs no credential at all. A credential that presents nothing is not a credential, so `none` is not a value a credential can have.

### Basic auth: two ways to supply it, one stored shape

A `basic_auth` credential is **always** stored as a username and a password. Supply it either way:

```json theme={null}
{ "credentials": { "username": "api_user@example.com", "password": "REPLACE_ME" } }
```

```json theme={null}
{ "credentials": { "basic_token": "YXBpX3VzZXJAZXhhbXBsZS5jb206UkVQTEFDRV9NRQ==" } }
```

`basic_token` is the convenience input for when what you have on hand is the pre-encoded token a provider's documentation prints. It is decoded and stored as the username and password pair, and the encoded form is never kept. Supply **one or the other**, never both — they are two encodings of one identity, and a body carrying both is refused.

`base64(username:password)` is a wire encoding, not a credential format: encoding is the transport's business, and the `Authorization` header is computed fresh each time the credential is used. Storing both forms would mean two records for one real login, which is exactly what "one credential, rotated once" is meant to prevent.

A `basic_token` that is not valid base64, or that decodes to something without a colon in it, is refused with a message naming the field only — never any part of the token.

## Declaring where an API key goes

An `api_key` credential needs one more fact than the key itself: **where the key is presented**. Providers disagree — some want a header, some want `Bearer` in front of the value, some want a query parameter — and the key alone does not say which.

That fact is declared **once, on the credential**, as `api_key_placement`. Exactly one of `header_name` or `query_param`, and `scheme` only alongside `header_name`:

```bash theme={null}
curl -X POST "$BASE_URL/credentials" \
  -H "X-API-Key: $GETDIALED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme CRM",
    "platform_id": "getdialed",
    "auth_method": "api_key",
    "tenant_name": "Acme CRM",
    "api_key_placement": {"header_name": "Authorization", "scheme": "Bearer"},
    "credentials": {"api_key": "REPLACE_ME"}
  }'
```

The three shapes that cover essentially every provider:

| Provider wants                | `api_key_placement`                                    |
| ----------------------------- | ------------------------------------------------------ |
| `Authorization: Bearer <key>` | `{"header_name": "Authorization", "scheme": "Bearer"}` |
| `X-API-Key: <key>`            | `{"header_name": "X-API-Key"}`                         |
| `?api_key=<key>`              | `{"query_param": "api_key"}`                           |

### Postmark server token

**Postmark** takes the second shape. A [transactional email](/concepts/transactional-email) credential is an `api_key` credential whose key is presented in an `X-Postmark-Server-Token` header, with no scheme in front of it:

```json theme={null}
{
  "name": "Postmark Production",
  "platform_id": "postmark",
  "auth_method": "api_key",
  "api_key_placement": {"header_name": "X-Postmark-Server-Token"},
  "credentials": {"server_token": "REPLACE_ME"}
}
```

The key is a Postmark **server token**, not an account token, and the distinction is not cosmetic: an account token administers the whole account and a server token is scoped to one server's mail. Only the server token belongs here — the material field is named `server_token` to say so at the point where it is supplied.

**One credential is one Postmark server.** GetDialed calls Postmark with the token, reads back which server it belongs to, and files the credential under that [Server](/concepts/platform-tenancy) — you never look up or type a provider identifier.

<Warning>
  **A server token can read that server's message history, so the server is an isolation boundary and not merely a budget key.** Postmark retains message content after delivery and hands it back to anything holding the server token. That is why one server per account is the design: a token that covered several accounts' mail would let one account's credential reach another's message content, which is a content leak rather than a shared allowance.

  Keep the account token out of GetDialed entirely. Nothing in a flow needs it, and a credential holding one would grant every server on the account rather than the one it was meant for.
</Warning>

### Why the placement lives on the credential

Declaring it here rather than per step is what makes the guarantee hold: **every step using this credential authenticates identically, and no step configuration mentions authentication at all.** A placement that could be overridden per use would put the same credential on the wire two different ways and reintroduce the "which one is this step actually doing" question.

It also decides what a step may not author. A step that selects this credential cannot set the header or query parameter the placement names — that name joins the three that are always refused. See [calling an HTTP API](/concepts/http-requests#authentication-is-selection-never-a-pasted-secret).

A placement declaring neither a header nor a query parameter, declaring both, or putting `scheme` on a query parameter is refused: each is either no answer to "where does the key go" or two answers.

### OAuth2 and `audience`

An `oauth2` credential exchanges its client id and secret at `token_url` for an access token, and the platform refreshes that token for you. Alongside `grant_type` and `scopes`, some authorization servers also require an **`audience`** — their own identifier for the API the token is being requested for.

```bash theme={null}
curl -X POST "$BASE_URL/credentials" \
  -H "X-API-Key: $GETDIALED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Partner API",
    "platform_id": "getdialed",
    "auth_method": "oauth2",
    "tenant_name": "Partner API",
    "grant_type": "client_credentials",
    "token_url": "https://login.example.com/oauth/token",
    "audience": "https://api.example.com/",
    "scopes": ["contacts:read"],
    "credentials": {"client_id": "REPLACE_ME", "client_secret": "REPLACE_ME"}
  }'
```

Leave it out where the provider does not ask for one. Where it **is** required and missing, the symptom is not an obvious error at create time — the token exchange succeeds and the token it issues is rejected by the API you then call. If an OAuth2 credential authenticates but every call with it comes back unauthorized, `audience` is the first thing to check in the provider's documentation.

`token_url` is required for `oauth2` and rejected on every other method, and only an `https` token endpoint is ever contacted — a plain `http` one is refused when the exchange runs. `grant_type`, `scopes` and `audience` are likewise `oauth2`-only, and `api_key_placement` is `api_key`-only and required there: a credential carrying configuration that belongs to a different method is refused, because it is a second and contradictory answer to what the credential presents.

Both `audience` and `api_key_placement` are non-secret configuration. They are returned by the API like any other field, and a `PUT` can change either without touching the stored material — so correcting a placement is not a rotation and does not clear `last_verified`.

## AWS access key

An `aws_access_key` credential is the access-key **pair** of an IAM user in **your** AWS account, plus the region it works in. It is what the [S3 file steps](/concepts/files#the-steps) authenticate with, and it reaches nothing in your account that its IAM policy does not name.

```bash theme={null}
curl -X POST "$BASE_URL/credentials" \
  -H "X-API-Key: $GETDIALED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Warehouse Staging Bucket",
    "platform_id": "aws",
    "auth_method": "aws_access_key",
    "aws_region": "us-east-1",
    "aws_default_bucket": "your-bucket",
    "credentials": {
      "access_key_id": "REPLACE_ME",
      "secret_access_key": "REPLACE_ME"
    }
  }'
```

The two halves land in different places, and the split is the same one every method here follows:

| Field                           | Where it goes                                                | Required |
| ------------------------------- | ------------------------------------------------------------ | -------- |
| `credentials.access_key_id`     | Secret storage. On no record and on no response.             | yes      |
| `credentials.secret_access_key` | Secret storage, beside its id.                               | yes      |
| `aws_region`                    | The credential record, as ordinary non-secret configuration. | **yes**  |
| `aws_default_bucket`            | The credential record.                                       |          |
| `aws_endpoint_url`              | The credential record.                                       |          |

`credentials` accepts **exactly** those two keys. A third — a session token, say — is refused rather than quietly dropped, because nothing ever reads one back out and storing an unread secret is worse than not accepting it.

`aws_region` is required on this method and refused on every other, as are the other two `aws_` fields. A credential carrying configuration belonging to a different method is refused: it is a second and contradictory answer to what the credential presents.

`aws_default_bucket` is the bucket a storage step uses when it names none itself. Set it and the bucket name lives in one place instead of in every step. It is a bucket **name**, not a URL or a path — a value containing `/`, `://` or whitespace is refused.

<Note>
  **There is no api-key placement to declare, and that is why this is its own method rather than a variant of `api_key`.** An access-key pair is not presented as a header value: it **signs** the request, and the signature covers the whole request. There is nowhere to put the key, so [`api_key_placement`](#declaring-where-an-api-key-goes) would be simultaneously required and meaningless.
</Note>

Creating the credential identifies which AWS **Account** the pair belongs to and files the credential under it — you never look up or type the account number. See [platform tenancy](/concepts/platform-tenancy) for what that record owns.

### The IAM policy to attach

Scope the IAM user to the one bucket. [Store a report in S3](/guides/store-a-report-in-s3#the-iam-policy) prints the exact two-statement policy, including why `s3:ListBucket` on the bucket ARN belongs on it even though nothing lists your bucket — without it, S3 answers `403` for an object that is simply not there, and a missing object becomes indistinguishable from a broken credential.

### S3-compatible storage

`aws_endpoint_url` points the credential at an **S3-compatible** service instead of Amazon's — Cloudflare R2, MinIO, Backblaze B2, DigitalOcean Spaces. One field; the storage steps are otherwise identical.

Set it together with `aws_default_bucket`. An S3-compatible provider exposes no account-identity call, so reaching the bucket is the only way to establish that the key pair works at all.

<Warning>
  **The endpoint must be an `https` URL naming a publicly routable host.** A `http` URL, or one naming a private, loopback, link-local or otherwise internal address, is **refused when you save it** — on create and on update alike — and the same refusal is applied again when a step uses the credential.

  This is a platform safety rule and it has no exceptions or overrides. A stored endpoint is a destination the platform will connect to on your behalf, and an endpoint pointing inward would make a credential into a way to reach services that are not yours to reach. Every refused address class produces the **same** message, which deliberately does not tell you which rule you tripped — a message that distinguished them would answer questions about our network for anyone who asked enough times.
</Warning>

Leave the field empty for real AWS. A trailing newline or stray whitespace still counts as a set endpoint, so paste carefully — an R2 credential is not silently sent to Amazon.

### Testing an AWS credential

`POST /credentials/{credential_id}/test` makes **two** checks, not one, and reports which of them it managed:

* It resolves the account identity, proving the pair is valid. This call requires no IAM permission and cannot be denied by a policy, which is exactly what makes it a good identity check and a worthless permission check.
* If `aws_default_bucket` is set, it then checks it can reach that bucket — the grant you actually need.

With no default bucket configured, only the first check runs and the result **says so in as many words** rather than reporting a bare success. A credential can pass the identity check and still be unable to read one byte from your bucket, and a success that did not name what it left unproven would send you into your first flow with false confidence.

<Note>
  **`POST /credentials/test` — the test-before-save endpoint — cannot test an AWS credential yet.** Its body carries no `aws_region`, and the region is not optional, so the attempt comes back as `INVALID_CREDENTIALS_SHAPE`. Create the credential and test it by id instead; a create that cannot be verified fails before anything is stored, so nothing is left behind.
</Note>

### Not supported yet

**Cross-account IAM roles.** There is no way to give GetDialed a role to assume with an external id — the access-key pair is the only AWS credential shape today. If your security posture requires roles rather than long-lived keys, that is a real gap and it is worth waiting for rather than designing around.

## Which credential an action can use

Compatibility is computed by the server, and the same rule runs in both places it matters — when you list candidates and when you save a definition. A credential offered to you will not be rejected at save time.

`GET /credentials?compatible_with={action_id}` returns only the credentials that action can actually use:

* **For a vendor action**, the credential's tenant must belong to the action's platform **and** its `auth_method` must be one the action's service accepts. An unbound credential is never offered for a vendor action — it has no tenant to have authenticated to.
* **For a built-in action**, the authentication method alone decides; there is no vendor tenant to compare against.
* **An action that needs no credential matches none.** You get an empty list rather than every credential you own.
* An unknown `action_id` is a `404`, not an empty list.

When exactly one candidate exists, a client may preselect it for you — but **the saved definition always carries an explicit credential id**. Nothing is filled in on your behalf at save time; a task that names no credential for an action that requires one is rejected with `422`.

## Rotating a secret

Supply `credentials` on `PUT /credentials/{credential_id}` to replace the stored material. The credential keeps its id and keeps pointing at the same stored secret, so:

* **No flow edit.** Every definition referencing this credential picks up the new material.
* **No re-selection and no redeployment.**
* **A running worker refreshes its cached copy within 300 seconds** — so allow up to five minutes before expecting the new material to be in force everywhere, and do not rotate and immediately re-dispatch expecting the new value.

Rotating **clears** `last_verified`, because the recorded check described the material you just replaced. Run a test to record a fresh one.

`platform_id` cannot be changed — a credential belongs to one platform for its whole life, and a `400` is returned rather than a rebind. Server-managed fields (the tenant binding, the stored-secret pointer, the observed-use record) cannot be set from a request body and are preserved.

## Credentials with no vendor account

Some platforms publish no way to identify "your account over there", and some credentials are not tied to a vendor at all — an internal HTTP service, a customer-specific endpoint. Those work through the **same create call**: supply `tenant_name` with a label you choose, and the credential is filed under a manual tenant of that name. There is no create-the-tenant-first step.

A manual tenant behaves like any other on [`/v1/tenants`](/concepts/platform-tenancy): it is listed, renamable and retirable. It has no provider-published rate caps, so its budget is empty, and its name is unique within your organization only.

## Endpoints

All endpoints require authentication and operate only on your organization's credentials. Cross-tenant access returns `404`, never `403`.

| Method   | Endpoint                            | Access     | Purpose                                                                                                                                                                          |
| -------- | ----------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET`    | `/credentials`                      | Any member | List credentials. Filter with `status`, `platform_id`, `tenancy_id`, `auth_method`, or `compatible_with={action_id}`; sort with `sort_by`/`order`; paginate with `limit`/`skip`. |
| `POST`   | `/credentials`                      | Admin      | Create a credential. Returns `201`.                                                                                                                                              |
| `GET`    | `/credentials/{credential_id}`      | Any member | Fetch one credential, including its tenant label and observed-use record.                                                                                                        |
| `PUT`    | `/credentials/{credential_id}`      | Admin      | Update name, status and authentication configuration; supply `credentials` to rotate.                                                                                            |
| `DELETE` | `/credentials/{credential_id}`      | Admin      | Delete a credential. Returns `204`, or `409` if other credentials still share its stored secret.                                                                                 |
| `POST`   | `/credentials/test`                 | Admin      | Test a credentials payload without saving anything.                                                                                                                              |
| `POST`   | `/credentials/{credential_id}/test` | Admin      | Test a saved credential's stored secret.                                                                                                                                         |

Creating one — the platform, the method, and the material, and nothing else:

```bash theme={null}
curl -X POST "$BASE_URL/credentials" \
  -H "X-API-Key: $GETDIALED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Five9 Production",
    "platform_id": "five9",
    "auth_method": "basic_auth",
    "credentials": {"username": "api_user@acme.com", "password": "REPLACE_ME"}
  }'
```

When the platform can identify the account those credentials belong to, this call discovers it, verifies the credentials in the process, and files the credential under that account — you never look up or type a provider account id. The response carries the resulting `tenancy_id` and a resolved `tenant`:

```json theme={null}
{
  "id": "cred_e5f6g7h8",
  "name": "Five9 Production",
  "platform_id": "five9",
  "auth_method": "basic_auth",
  "credentials_ref": "getdialed/acme/five9/cred_e5f6g7h8",
  "tenancy_id": "ten_9f2c1a7b3d4e5f60",
  "tenant": {
    "id": "ten_9f2c1a7b3d4e5f60",
    "label": "Acme VCC",
    "platform_id": "five9",
    "is_manual": false
  },
  "status": "active",
  "last_verified": "2026-08-22T09:00:00Z"
}
```

Because discovery is a live call, a create **fails before anything is stored**:

* `400` when the credentials could not be verified — wrong material, or the platform could not be reached. Nothing is written. Use `POST /credentials/test` first if you want to check without committing.
* `409` when that provider account already belongs to another GetDialed account, or when these exact credentials are already stored here. A vendor account belongs to exactly one GetDialed account platform-wide.

Supply `credentials_ref` **instead of** `credentials` to point at a secret that already exists. The two are mutually exclusive, and a ref-only create performs no live verification, so its `tenancy_id` stays `null`.

### Deleting a credential

Deleting removes the record, and reclaims the stored secret only when GetDialed created that secret in the first place:

* **A secret GetDialed created, used by nothing else** — the record is deleted and the secret is scheduled for deletion with a recovery window. Nothing is destroyed immediately.
* **A secret shared with other credentials in your account** — the delete is refused with `409` and the record is left untouched, so the same request can be retried. The message tells you how many other credentials still reference it. One secret serving several credentials is normal, not an error.
* **A secret GetDialed did not create** (one provisioned for you directly) — the record is removed and the secret is left exactly as it was.

Deleting the last credential bound to a vendor account frees that account for any organization to connect again. The tenant's tuned rate-limit settings are kept, so re-adding the same account later restores them.

## Testing credentials

Two endpoints check credentials against the live platform, so you catch a bad password before a flow fails at 2 a.m. Both require an admin caller.

<Steps>
  <Step title="Test before saving">
    `POST /credentials/test` takes `platform_id`, `auth_method` and a platform-specific `credentials` object. It returns a verdict and **persists nothing** — the test-before-save flow.
  </Step>

  <Step title="Test a saved credential">
    `POST /credentials/{credential_id}/test` (no request body) resolves the saved credential's stored secret and runs the same check. On success, `last_verified` is updated. This is what you want after a rotation, or when a flow starts failing.
  </Step>
</Steps>

**Both are liveness checks.** They prove the credentials are real and the platform is reachable with them. They do **not** prove what the credentials are permitted to do — verifying permissions would mean performing the operations themselves against your live vendor account, which this API will not do. For permissions, read the observed-use record below.

Both return `200` with a `TestResult` body for **every** outcome — success, bad credentials, even a timeout. `4xx` statuses are reserved for problems with your request itself.

```json theme={null}
{
  "success": false,
  "error": "AUTH_FAILED",
  "details": "Five9 rejected the supplied credentials (HTTP 401/403).",
  "latency_ms": 312,
  "tested_at": "2026-08-22T10:30:00Z"
}
```

| Field        | Type           | Description                                                                                     |
| ------------ | -------------- | ----------------------------------------------------------------------------------------------- |
| `success`    | boolean        | `true` if the platform accepted the credentials.                                                |
| `error`      | string \| null | An error code (below) when `success` is `false`; `null` on success.                             |
| `details`    | string         | Human-readable diagnostic. Never contains credential material or the name of the stored secret. |
| `latency_ms` | integer        | Round-trip time of the verification call, in milliseconds.                                      |
| `tested_at`  | datetime       | UTC timestamp of when the test started.                                                         |

| Code                        | Meaning                                                                                                                                                                                                   |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AUTH_FAILED`               | The platform rejected the credentials.                                                                                                                                                                    |
| `NETWORK`                   | The platform couldn't be reached (DNS, TCP, TLS), or the stored secret couldn't be resolved.                                                                                                              |
| `TIMEOUT`                   | The check exceeded its time budget.                                                                                                                                                                       |
| `RATE_LIMITED`              | The request was withheld for pacing reasons, or the platform throttled it.                                                                                                                                |
| `PERMISSION_DENIED`         | Authenticated, but not authorized to proceed — either the credentials lack permission for the verification call, or the vendor account they resolve to is already connected to another GetDialed account. |
| `INVALID_CREDENTIALS_SHAPE` | The `credentials` object is missing keys the platform requires (e.g. Five9 basic auth needs `username` and `password`) — caught before any network call.                                                  |
| `UNKNOWN`                   | Anything else, including a platform that offers no test.                                                                                                                                                  |

<Tip>
  `422` and `INVALID_CREDENTIALS_SHAPE` look similar but differ: `422` means the request body itself is malformed (wrong types, missing schema fields), while `200` + `INVALID_CREDENTIALS_SHAPE` means the body parsed fine but the credentials object lacks what the target platform needs.
</Tip>

## What a credential can actually do

A liveness test cannot tell you what a credential is permitted to do. The record of what it **has** done can.

Every credential carries a `telemetry` object keyed by `service_id`, stamped as work actually happens. For each service the credential has been used with it reports when it was last used, when it last succeeded, when it last failed, and what class of failure that was:

```json theme={null}
{
  "telemetry": {
    "five9__configuration_service": {
      "last_used_at": "2026-08-22T09:00:00Z",
      "last_succeeded_at": "2026-08-22T09:00:00Z",
      "last_failed_at": null,
      "last_error_class": null,
      "last_stamped_at": "2026-08-22T09:00:00Z"
    }
  }
}
```

This is the honest answer to "what can this credential do", and it costs nothing to read: the same record travels on every row of `GET /credentials`, so a list view can show health without a request per row and **without running a flow to find out**. A credential that has never been used has an empty record — which is a real answer, not a missing one.

`last_error_class` is a failure *class*, never a failure message. Provider error strings can embed request URLs carrying credential material, so they are never published here.

## Discovering credential fields

To build a credential form you need to know which fields a platform requires — a username and password for Five9 basic auth, a token for another platform. The catalog service listing carries that schema. `GET /catalog/platforms/{platform_id}/services` returns each service with a `credential_fields` array:

```json theme={null}
{
  "id": "five9__configuration_service",
  "platform_id": "five9",
  "auth_method": "basic_auth",
  "credential_fields": [
    {"name": "username", "label": "Username", "type": "string", "required": true, "secret": false},
    {"name": "password", "label": "Password", "type": "password", "required": true, "secret": true}
  ],
  "status": "active"
}
```

| Field      | Type    | Description                                          |
| ---------- | ------- | ---------------------------------------------------- |
| `name`     | string  | The credential key to submit (e.g. `password`).      |
| `label`    | string  | Human-readable label for the form input.             |
| `type`     | string  | Input type — e.g. `string`, `password`, `url`.       |
| `required` | boolean | Whether the field must be supplied.                  |
| `secret`   | boolean | Whether the value is sensitive and should be masked. |

This is metadata only — it describes what to collect, never actual credential values. A service without a published schema returns an empty `credential_fields` list.

## Next steps

<CardGroup cols={2}>
  <Card title="Platform tenancy" icon="building" href="/concepts/platform-tenancy">
    The tenant a credential belongs to, and the budget they share.
  </Card>

  <Card title="Integration catalog" icon="grid" href="/concepts/catalog">
    Browse the platforms and services a credential can reach.
  </Card>

  <Card title="Quota and progress" icon="gauge" href="/concepts/quota-and-progress">
    Which credential is spending a tenant's provider budget.
  </Card>

  <Card title="Build your first flow" icon="hammer" href="/guides/build-your-first-flow">
    Put a credential to work in a multi-step flow.
  </Card>
</CardGroup>
