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

# Credentials

> Resolve a stored third-party credential inside a step, in place of an environment variable - the credential is used only transiently and never enters a step's durable input or output.

A **credential** is a secret Duraton stores on your behalf (an API key, an OAuth token, SMTP
settings) so a workflow can call a third-party service without the credential living in an
environment variable. `client.credentials.resolve()` reads one back at execution time.

## Resolve inside a step

Resolution must happen **inside** a `step.run` (or `step.ai.*`) closure, never in the workflow
handler's top-level body. A step's input and output are durable - they are recorded and replayed
forever - so a credential resolved outside a step, then passed into one, would sit in that replayed
record permanently. Resolved inside the closure, the credential is used only for the one call that
needs it and is gone once the step returns:

```ts theme={null}
import { createClient } from "@duraton/sdk";

const duraton = createClient({
  url: process.env.DURATON_URL!,
  apiKey: process.env.DURATON_API_KEY,
});

await ctx.step.run("publish", async () => {
  const cred = await duraton.credentials.resolve(credentialId);
  return post(cred.credential, topic);
});
```

`resolve()` called outside a step - directly in the handler body, or stashed in a variable before a
`step.run` call - throws immediately (`CredentialResolveOutsideStepError`) rather than silently
handing back a credential with no durable-input protection.

| Field        | Type                  | Description                                                                                                              |
| ------------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `credential` | `string`              | The credential's plaintext, decoded from the wire. Duraton treats it as opaque - see the shapes below for what it holds. |
| `authShape`  | `CredentialAuthShape` | Which shape `credential` holds.                                                                                          |

## Auth shapes

`authShape` tells you how to read `credential` and how to present it to the third party. A
single-value shape is the value itself; a multi-field shape is a JSON string you parse yourself.

| `authShape`                          | `credential` holds                                                                    |
| ------------------------------------ | ------------------------------------------------------------------------------------- |
| `api_key`                            | The key itself, with no placement implied - you decide where it goes.                 |
| `header_auth`                        | `{"name": "X-Api-Key", "value": "..."}` - send `value` as the request header `name`.  |
| `query_auth`                         | `{"name": "token", "value": "..."}` - send `value` as the URL query parameter `name`. |
| `basic_auth`                         | The credential itself.                                                                |
| `oauth2`                             | `{"access_token": "...", "refresh_token": "..."}`, written and refreshed by Duraton.  |
| `oauth1`, `smtp`, `incoming_webhook` | Whatever you stored. Duraton defines no schema for these - the bytes are yours.       |

`header_auth` and `query_auth` are distinct shapes rather than an `api_key` with a placement
setting, because placement changes how you use the credential, not just what its bytes are - so it
travels in the shape rather than hidden inside the blob.

A replayed step never re-invokes its body, so a memoized `resolve()` call never runs twice and never
fails if the credential was since deleted - it only had to exist at the moment the step first ran.

## Retrying a rejected credential

Duraton refreshes an OAuth credential's access token ahead of its known expiry, so a normal resolve
already returns a live token. A token can still be rejected by the provider despite looking valid
(clock skew, early revocation) - catch that in the step and let the step's own retry call `resolve()`
again:

```ts theme={null}
await ctx.step.run(
  "publish",
  async () => {
    const cred = await duraton.credentials.resolve(credentialId);
    const res = await post(cred.credential, topic);
    if (res.status === 401) throw new Error("credential rejected, retrying");
    return res;
  },
  { retry: { maxAttempts: 2 } },
);
```

The second attempt calls `resolve()` again from scratch, picking up a refreshed token if one is due.
A token rejected well before its known expiry (the provider revoked access early) is not fixed by a
retry - the credential needs re-authorization; see [Retries & failure handling](/core/retries)
for `NonRetriableError` to fail fast on that case instead of spending the retry budget.
