Skip to main content
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 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

Without all three, the provider configuration does not resolve and every provider-backed connect route answers 503:
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:
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.
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 explains why neither has a way back.

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.
1

Deploy once, without the key

Everything else comes up. Receipt Connect does not work yet; that is expected.
2

Open the provider UI at your public origin

It is served under /integrations on the public origin, through the gateway.
3

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.
4

Set it and deploy again

Put it in your secrets bundle, upload the bundle, and deploy a second time.
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.

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:
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:
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.
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.
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:
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:

The two shapes that are handled

type: "auth", operation: "creation" or "override", success: true.Receipt resolves who and where from the webhook’s tags, with fallbacks:It then upserts the encrypted connection reference.
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.
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:
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.
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 explains what that distinction means to a member.

Operational warnings

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.
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 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.
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.
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.