# Provider interaction lifecycle

> Build guided Credential acquisition with redirects, forms, device instructions, callbacks, polling, and secure completion.

An interactive CredentialProvider does not implement an OAuth-shaped API. It implements a small state machine through `interaction.start` and `interaction.advance`. The provider chooses the next action; Constal durably coordinates the session, renders the action, validates the response, and stores the final Credential.

OAuth authorization code, device authorization, API-key enrollment, approval workflows, installation pickers, and asynchronous secret issuance all use the same contract.

## The protocol {#protocol}

`interaction.start(request, context)` receives the Credential identity, non-secret configuration, a signed state value, the shared callback URL, session expiry, and a platform-generated proof challenge. It returns an action and optional provider-private state.

`interaction.advance(request, context)` receives the next validated event, the same session evidence, the proof verifier, and the encrypted private state returned by the previous step. It returns another action or completes the Credential.

| Provider action | Console behavior | Next event |
| --- | --- | --- |
| `redirect` | Opens an allowlisted HTTPS URL | `callback` |
| `form` | Renders the supplied JSON Schema | `submit` |
| `display` | Renders its declared primary action, instructions, and optional code | Declared `continue` event |
| `wait` with `callback` | Waits for external ingress | `callback` |
| `wait` with `poll` | Polls no sooner than `pollAfterMs` | `poll` |
| `complete` | Encrypts, verifies, and activates material | Terminal |

Actions are data, not executable UI. This keeps provider packages portable across the Console, CLI, SDKs, and future clients.

## Inline acquisition {#inline-acquisition}

A Resource setup flow may request a Credential from a named provider without giving the Resource permission to mint it. For example, a Gateway package declares `credentialSlots.account.provider: "example-device"`. If no compatible Credential exists, the Console can start that provider's interaction, save the completed Credential, bind its CRN into the still-uncommitted Gateway form, and resume installation. Cancellation returns to the preserved Resource form without creating the Resource.

This is client orchestration over the same Credential APIs. Ownership remains separate: the Credential Provider controls acquisition and lifecycle, the Credential coordinator stores versions, and the Resource receives only an authorized binding at invocation time.

## Minimal interactive provider {#example}

```ts
import { credentialProvider } from "@constal/sdk";

export default credentialProvider({
  id: "example-device",
  version: "1.0.0",
  displayName: "Example device connection",
  description: "Connects an Example account through device authorization.",
  credentialSlots: ["client-secret"],
  configSchema: {
    type: "object",
    required: ["clientId"],
    additionalProperties: false,
    properties: { clientId: { type: "string", title: "Client ID" } }
  },
  credentialConfigSchema: { type: "object", additionalProperties: false },
  rotation: {
    mode: "auto", intervalMs: null, overlapMs: 60000,
    maxAgeMs: null, refreshBeforeMs: 300000
  },
  egress: { rules: [{
    operations: ["interaction.start", "interaction.advance", "mint"],
    origin: "https://api.example.com", pathPrefix: "/oauth/", methods: ["POST"]
  }] },
  interaction: {
    origins: ["https://example.com"],
    async start(request) {
      const grant = await beginDeviceGrant(request);
      return {
        action: {
          kind: "display",
          title: "Connect your Example account",
          instructions: "Open Example and enter this one-time code.",
          userCode: grant.userCode,
          primaryAction: {
            kind: "open-url",
            label: "Open Example login",
            url: "https://example.com/device",
            event: { kind: "continue" }
          }
        },
        privateState: JSON.stringify({ deviceCode: grant.deviceCode })
      };
    },
    async advance(request) {
      const state = JSON.parse(request.privateState ?? "{}");
      const token = await exchangeDeviceCode(state.deviceCode);
      if (token.pending) return {
        action: {
          kind: "wait", resume: "poll",
          pollAfterMs: 5000, expiresAt: request.expiresAt
        },
        privateState: request.privateState
      };
      return {
        action: {
          kind: "complete", material: token.accessToken,
          expiresAt: token.expiresAt
        },
        privateState: token.refreshToken
      };
    }
  },
  mintRecovery: { kind: "outcome-unknown" },
  async mint(request) {
    return refreshAccessToken(request.privateState);
  }
});
```

The example abbreviates external API helpers. Those calls must use `context.egress()` and the declared operation allowlist.

## Callbacks {#callbacks}

Interactive providers share one callback:

```text
https://platform.constal.ai/v1/credential-interactions/callback
```

Register it exactly when the external service redirects or posts back to Constal. Signed, expiring state identifies the Credential and interaction session; the callback URL does not contain a tenant identifier. The platform accepts bounded GET query callbacks and bounded POST bodies, strips authority-bearing headers, verifies state, and delivers one `callback` event to the pinned provider operation.

For a `redirect` action, the URL must be HTTPS, use one of the provider's exact declared origins, and contain the exact `request.state` value. An `open-url` primary action is subject to the same origin allowlist. The provider owns its label, destination, and declared event; clients only render and interpret the validated action.

## Forms and instructions {#forms-and-instructions}

A `form` action supplies a supported object JSON Schema with `additionalProperties: false`. Constal renders it and validates the submitted value before provider code sees it. Titles, descriptions, defaults, examples, enums, and bounds provide the same low-friction controls used during provider setup.

Use `display` when the operator must act elsewhere. Its required `primaryAction` is either an `open-url` action or a local `continue` action. Both declare a bounded label and the exact `continue` event that advances the provider state machine. Instructions and codes are presentation values only; never put access or refresh material in them.

## Session ownership and recovery {#recovery}

The Credential coordinator owns:

- authenticated principal and optional customer ownership;
- signed state, expiry, proof verifier, and callback routing;
- encrypted action and provider-private state;
- event/action matching, schema validation, and poll rate limits;
- replay fencing, immutable provider operation pins, verification, and activation.

Provider code owns protocol-specific requests and the decision about what comes next. It cannot choose another Credential, callback destination, executable UI, or undeclared network origin.

`interaction.start` is idempotent. `interaction.advance` uses `outcome-unknown` recovery because a callback code, device code, approval, or refresh token may be consumed exactly once. The invocation journal resumes the same transition; it must never manufacture a second external exchange.

## Security checklist {#security-checklist}

- Keep installation secrets in declared bootstrap Credential slots.
- Keep per-Credential inputs non-secret and schema bounded.
- Use the platform state and proof values exactly; do not generate substitutes.
- Return temporary protocol state only as `privateState`.
- Never log codes, verifiers, material, or private state.
- Give every wait step an expiry and a conservative poll interval.
- Verify material before activation when the service offers a verification endpoint.
- Implement refresh and source-side destruction when the external authority supports them.

For OAuth specifically, continue with [Authorize an OAuth credential](/docs/credentials/oauth/authorize.md) and [OAuth callback security](/docs/credentials/oauth/callback.md).
