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

# Webhooks and credentials

> Tell your backend what happens in a workspace, and give each run short-lived credentials from it.

Two optional connections let a product built on OpenGeni work without polling and without storing long-lived secrets in OpenGeni. Set them up in **Settings > Developer** for one workspace, or in **Organization settings > Developer** for every shared workspace at once. Both are also available through the API and `@opengeni/sdk`.

| | Webhooks | Credential provider |
| - | - | - |
| Direction | OpenGeni to your backend | OpenGeni asks your backend |
| When | A turn finishes or fails, the agent needs approval or asks a question, a status changes, usage limits | Before a run starts, and again before its credentials expire |
| You answer with | Any 2xx | Environment variables, files, Git or MCP credentials, or `not_applicable` |

## Who sets them up where

* **Workspace** (Settings > Developer): workspace admins. A workspace's own credential provider replaces the organization's for that workspace; pausing it means runs there get no provider credentials at all.
* **Organization** (Organization settings > Developer): organization owners and admins, or a full-access organization API key. Choose every shared workspace, or only workspaces your product created with one external source. Personal workspaces are never included.

A workspace's Developer page shows what it inherits: the organization's provider (marked **Organization**) and how many organization webhooks also get its events.

## Signatures

Every request carries `OpenGeni-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>`. OpenGeni creates the signing secret and shows it once, when you add the webhook or connect the provider; **New signing secret** in its ⋯ menu replaces it immediately. Verify the raw body before parsing it:

```ts theme={null}
import { verifyCredentialProviderRequest, verifyWebhookEvent } from "@opengeni/sdk";

const { event } = await verifyWebhookEvent({ body: rawBody, headers, secret: webhookSecret });
const request = await verifyCredentialProviderRequest({
  body: rawBody,
  headers,
  secret: providerSecret,
});
```

Both throw `OpenGeniSignatureError` on a bad or stale (older than five minutes) signature. Authorize by mapping the trusted `workspaceId` to your own customer, never by the body alone.

## Webhooks

The body is a thin event; read details through the API:

```json theme={null}
{
  "id": "…",
  "type": "turn.completed",
  "workspaceId": "…",
  "sessionId": "…",
  "turnId": "…",
  "sequence": 42,
  "occurredAt": "2026-10-01T10:00:00.000Z",
  "data": { "status": "idle" }
}
```

* Delivery is at least once and may be out of order: skip an `id` you already handled, and use `sequence` within a session.
* Anything but a 2xx within 10 seconds is retried with growing gaps, up to 12 attempts. A webhook's page lists recent deliveries with their status, the last answer and the next try, and can send a settled one again.
* **Send test event** posts a signed `webhook.test` event (no session) right away and shows what your endpoint answered. Acknowledge it with any 2xx.
* Pausing a webhook keeps new events queued until you resume it.

## Credential provider

Before a run's first command, OpenGeni sends your endpoint a signed `credentials.request`: the workspace, the run, and who started it. Answer within 10 seconds:

```json theme={null}
{
  "status": "ok",
  "environment": { "AWS_ACCESS_KEY_ID": "…", "AWS_SECRET_ACCESS_KEY": "…" },
  "files": [{ "path": "gcp/key.json", "content": "…" }],
  "fileEnvironment": { "GOOGLE_APPLICATION_CREDENTIALS": "gcp/key.json" },
  "git": [{ "host": "github.com", "password": "ghs_…" }],
  "expiresAt": "2026-10-01T12:00:00Z"
}
```

or `{ "status": "not_applicable" }`, or `{ "status": "auth_needed", "authNeeded": [...] }`. The values reach the agent's sandbox, never the conversation or logs. OpenGeni asks again five minutes before `expiresAt`, or every 30 minutes without one.

**Test connection** sends the same request with `purpose: "test"` (session, turn and attempt ids are the nil UUID) and shows what a run would get, by name only: never the values. Answer it as you would a run in that workspace, or with `not_applicable`.

The full protocol, including renewable MCP headers and the management API, is in the [integration reference](https://github.com/Cloudgeni-ai/opengeni/blob/main/docs/workspace-integrations.md).
