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

# Running the integration provider

> Stand up the service Receipt Connect cannot work without: three variables, a registered OAuth callback on your own domain, and a signed webhook — plus the secret key that does not exist until after your first deploy.

Connecting an app is the first thing most people do in Receipt, and on a self-hosted install none of it works until you run an integration provider beside Receipt. Once it is up, members can authorize Gmail, GitHub, Jira and the rest from [Global Integrations](/mcp-gateway/connect-an-app) or a workspace, and those connections become available to chat, to the MCP Gateway, to the CLI and to Factory workers.

Receipt does not store provider credentials. It stores an **encrypted reference** — organization, workspace, provider, name → the provider's integration id and connection id — and the integration provider holds the actual OAuth tokens and API keys, along with authorization and refresh. That split is the reason the provider is not optional: without it there is nothing to authorize against.

The configuration surface is provider-agnostic, but `nango` is the only accepted value of `RECEIPT_INTEGRATIONS_PROVIDER` today.

## The three variables that make it exist

```bash theme={null}
RECEIPT_INTEGRATIONS_URL=            # base URL Receipt services use to reach the provider
RECEIPT_INTEGRATIONS_SECRET_KEY=     # the provider's own environment secret key
RECEIPT_INTEGRATIONS_WEBHOOK_SECRET= # HMAC secret shared with the provider
```

Without all three, the provider configuration does not resolve and every provider-backed connect route answers `503`:

```
Self-hosted Nango is not configured. Set RECEIPT_INTEGRATIONS_URL, RECEIPT_INTEGRATIONS_SECRET_KEY, and RECEIPT_INTEGRATIONS_WEBHOOK_SECRET.
```

The web app rewrites that for the person in front of it. On a hosted install they see `Integrations are temporarily unavailable because the connection service is not configured. Contact your Receipt administrator.`; running locally they see `Integrations are not configured for this local app. Start the integrated stack with bun run start:all, then try again.`

The rest of the surface:

| Variable                                | Purpose                                                                                                                                                                                                                 |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `RECEIPT_INTEGRATIONS_PROVIDER`         | Only `nango` is accepted                                                                                                                                                                                                |
| `RECEIPT_INTEGRATIONS_PUBLIC_URL`       | The provider's public URL, as browsers reach it; it is placed into the Connect link the dialog loads                                                                                                                    |
| `RECEIPT_INTEGRATIONS_DATABASE_URL`     | **Server-only.** The database owning the provider's private schema, used solely for organization-tagged connection read repair. It must never reach web clients, CLI artifacts, Factory task packets or sandbox workers |
| `RECEIPT_CONNECTION_ENCRYPTION_KEY_B64` | AES-256-GCM key for the stored connection references                                                                                                                                                                    |
| `RECEIPT_CONNECT_JWT_SECRET`            | HS256 secret for Receipt Connect JWTs, falling back to `BETTER_AUTH_SECRET`                                                                                                                                             |

<Warning>
  Provider-native names are **deliberately not accepted**: `NANGO_SECRET_KEY`, `NANGO_API_KEY`, `NANGO_INTERNAL_URL` and `NANGO_WEBHOOK_SECRET` are ignored by Receipt, as are the older `RECEIPT_CONNECT_NANGO_*` names. Receipt reads only the `RECEIPT_INTEGRATIONS_*` keys. The provider container itself is configured separately with its own `NANGO_*` variables.
</Warning>

<Warning>
  **`RECEIPT_CONNECTION_ENCRYPTION_KEY_B64` cannot be rotated** — change it after connections exist and every stored reference becomes permanently undecryptable, with re-entry as the only recovery. The same is true of `BYOK_ENCRYPTION_KEY_B64`; [Configuration, secrets, and keys](/core/configuration#encryption-keys-and-shared-secrets) explains why neither has a way back.
</Warning>

## The key you cannot have yet

This is a genuine chicken-and-egg, and it is the reason a first deploy looks broken.

`RECEIPT_INTEGRATIONS_SECRET_KEY` is the provider's own `prod` **environment** secret key. The provider only creates real environment keys once its database exists — which happens when you deploy it. So there is no value to configure before the first deploy.

<Steps>
  <Step title="Deploy once, without the key">
    Everything else comes up. Receipt Connect does not work yet; that is expected.
  </Step>

  <Step title="Open the provider UI at your public origin">
    It is served under `/integrations` on the public origin, through the gateway.
  </Step>

  <Step title="Copy the prod environment key">
    Environment Settings → API Keys. Take the **`prod` environment secret key in its UUID form** — not the public key. Those are two different values and only one of them works.
  </Step>

  <Step title="Set it and deploy again">
    Put it in your secrets bundle, upload the bundle, and deploy a second time.
  </Step>
</Steps>

<Note>
  The secret bundle loader validates this before anything is uploaded. A value that is not a UUID v4 is rejected, with a message that names the generated placeholder and the provider's public key as the two mistakes it is warning against. The three base64 encryption keys in the bundle are checked in the same pass and must each decode to **exactly 32 bytes**.

  The reference pipeline also has an explicit bootstrap mode that writes a UUID-shaped placeholder — which is why it passes the loader's UUID check — so the first deploy can proceed, plus a separate command that replaces the placeholder with the real key afterwards. What actually catches a placeholder is a separate flag: a bundle still carrying the flagged placeholder blocks a production release.
</Note>

## OAuth applications and the callback URL

For each OAuth connector you intend to offer, register an application with that provider, add the callback URL below, and set the connector's `RECEIPT_NANGO_<PREFIX>_CLIENT_ID` and `_CLIENT_SECRET` (with `RECEIPT_NANGO_<PREFIX>_SCOPES` when the defaults do not fit). OAuth integrations are never synthesized for you.

Because you run the provider yourself, the callback is on **your** domain — not on the provider's hosted API:

```
https://<your-receipt-host>/oauth/callback
```

The gateway hands the entire `/oauth` subtree to the integration provider, so that path is answered by the provider through your public origin. Every OAuth connector configured with custom credentials — rather than the provider's own defaults — must have that exact URL in its application record: `https`, no trailing slash, no port.

Get it wrong and the error comes from the **upstream provider**, not from Receipt or from the integration provider. Linear, for example, answers:

```
The redirect URI provided does not match any registered URI for this client.
```

which means the client id is configured correctly and only the registered callback list is wrong. Fix it in the upstream provider's own OAuth application record and retry Connect; no redeploy is needed, because the redirect URI is validated by the upstream provider at authorize time.

<Tip>
  Running the integration-reconciliation script with `--check` prints the exact `oauthCallbackUrl` it expects, so you can compare it against what is registered. The script derives that value from `RECEIPT_NANGO_OAUTH_CALLBACK_URL`, then `NANGO_SERVER_URL`, then your public base URL, and appends `/oauth/callback` itself.
</Tip>

**Credential-only connectors need no registration.** A connector whose setup is provider-only — API key, personal access token, access keys — is **auto-created on the provider on first use**, when a connect session is created for it, if the catalog declares it and the provider does not have it yet.

**Self-hosted Receipt does not need the provider's admin dashboard for connector setup.** Receipt creates integrations through the provider's authenticated public API.

## Webhooks: the primary consistency path

Webhooks are how a completed authorization actually becomes a stored connection. Read repair is the fallback, not the mechanism.

Point the provider's webhook URL at your public origin:

```
<public base URL>/connect/nango/webhook
```

Then enable HMAC on the provider and give it the same value you set in `RECEIPT_INTEGRATIONS_WEBHOOK_SECRET`. The repository ships a script that applies this configuration directly into the provider's private schema — it sets the webhook URL, clears the secondary URL, enables auth webhooks, disables always-send, turns HMAC on and sets the HMAC key — inside a transaction that rolls back on any failure, and takes exactly one of `--apply` or `--check`.

### How Receipt verifies one

Receipt computes an **HMAC-SHA256 over the raw request body**, reads the provider's signature from the `x-nango-hmac-sha256` header, and compares the two with a timing-safe comparison over hex. The route has no other authentication — the signature is the authentication.

An unsigned or wrongly signed request gets:

```
401 {"ok":false,"error":"invalid Nango webhook signature"}
```

### The two shapes that are handled

<AccordionGroup>
  <Accordion title="Auth creation or override">
    `type: "auth"`, `operation: "creation"` or `"override"`, `success: true`.

    Receipt resolves who and where from the webhook's tags, with fallbacks:

    | Field           | From                   | Falls back to                            |
    | --------------- | ---------------------- | ---------------------------------------- |
    | Organization    | `tags.organization_id` | the end user's organization id           |
    | Actor           | `tags.end_user_id`     | the end user's id                        |
    | Workspace       | `tags.workspace_id`    | the organization's **Default** workspace |
    | Connection name | `tags.connection_name` | `default`                                |

    It then upserts the encrypted connection reference.
  </Accordion>

  <Accordion title="Auth refresh failure">
    `type: "auth"`, `operation: "refresh"`, `success: false`.

    Receipt marks the matching stored reference `invalid`, with the reason `nango_auth_refresh_failed`. Nothing is deleted — the row stays, visibly broken, so a member can reconnect it.
  </Accordion>
</AccordionGroup>

A webhook of any other shape is acknowledged and ignored.

### When the actor has lost membership

A webhook can arrive after the person who started the authorization has been removed from the workspace. Receipt does not project the connection in that case, and it does not silently keep it either:

* The webhook is **acknowledged with 200** — so the provider does not retry forever — carrying `{"ok":true,"ignored":true,"reason":"workspace_membership_revoked"}`.
* The projection is refused, so no reference is stored.
* The connection just created on the provider is **best-effort deleted**, so no orphaned credential is left behind.

## Read repair: the secondary path

Read repair reconciles the provider's view of connections with Receipt's stored references. It reads two sources — the provider's paginated HTTP connection list, and a direct read of the provider's private connections table filtered on the organization and workspace tags. The second source exists because the HTTP list can omit a provider-only connection, and it is the only thing `RECEIPT_INTEGRATIONS_DATABASE_URL` is used for.

A stored reference the list did not report is **direct-checked** against the provider by connection id. A healthy direct result leaves the row alone. Anything else invalidates only that one row, never the rest: reported connection errors give the reason `nango_connection_errors`, an exhausted refresh gives `nango_refresh_exhausted`, and a `404` or `410` gives `nango_connection_missing`.

Where it runs matters:

| Endpoint                                               | How read repair runs                  | Why                                                                          |
| ------------------------------------------------------ | ------------------------------------- | ---------------------------------------------------------------------------- |
| `GET /connect/connections`                             | **Awaited**                           | The caller is asking for the connection list, so it should be current        |
| `GET /connect/capabilities`, `GET /connect/connectors` | **Deferred** — scheduled, not awaited | Capabilities sit on the chat preflight path and must not hold a request open |

<Info>
  Read-repair failures are logged and swallowed. That is deliberate: a briefly unreachable provider must never delete a member's stored connections. A provider outage degrades freshness, not data.
</Info>

Connections tagged with an organization but no workspace — the shape that predates workspace isolation — are reconciled into the hidden **Global** scope rather than into Default, because Default is reserved for MCP clients. [Connection scopes](/mcp-gateway/connection-scopes) explains what that distinction means to a member.

## Operational warnings

<Warning>
  **The local container ships with default credentials.** The local compose file supplies a default at-rest encryption key and a default dashboard login. Replace `NANGO_ENCRYPTION_KEY`, `NANGO_DASHBOARD_USERNAME` and `NANGO_DASHBOARD_PASSWORD` before the provider is reachable by anything but your own machine. The encryption key is the one protecting every credential the provider holds.
</Warning>

<Note>
  Receipt pins one specific hosted provider image and applies checked-in patches to it at build time. Each patch is fail-closed: the image build errors out if the pinned upstream shape has drifted, rather than producing an image that is quietly wrong.

  Two of those patches change what you have to register:

  * **Azure DevOps** is switched from the upstream basic-auth setup (organization URL plus a personal access token) to Microsoft Entra ID OAuth2, the same login Outlook and Azure Blob Storage use — so it needs an Entra application, not a PAT.
  * **Azure Resource Manager** gains a two-step service-principal provider that the pinned image predates. This is the connector the [Agent Registry](/catalog/agent-registry) uses to discover agents in Azure subscriptions; the client secret is exchanged by the integration provider server-side and Receipt stores only the non-secret reference.

  The remaining patches fix migration-runner transaction problems, mirror the provider's own dynamic MCP registration on the public API, and adjust other provider definitions in the pinned catalog.
</Note>

<Note>
  The free self-hosted provider includes Auth and Proxy but **not** Functions or the provider's own MCP server. Receipt therefore builds its ordinary REST coverage on Auth and Proxy alone, so no connector depends on provider-side functions.
</Note>

The provider serves its API on port `3003` and its Connect UI on port `3009`, reached through the gateway at `/integrations` and `/integrations-connect`. Checking `/integrations-connect/` only proves the static UI is being served — check `/integrations/health` for the provider itself.

Next step: [deploy Receipt and learn the guards that stop a bad release](/core/deploying).
