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

# Deploying to Production

> Move your integration from staging to live — the endpoint map, the credentials you have to reissue, the app configuration you have to redo, and what to verify before real money moves.

Going live is a configuration cutover, not a rewrite. Your queries, mutations, and flows are identical in both environments. What changes is **which hosts you call** and **which credentials you call them with** — and those change completely.

<Warning>
  Nothing carries over from staging. Applications, API keys, client secrets, redirect URIs, and webhook subscriptions all exist separately in each environment. A staging credential will never work against a production host, and vice versa. That's deliberate — it's what makes it impossible to accidentally move real money from a test harness.
</Warning>

## Endpoint map

| Purpose             | Staging                                                          | Production                                               |
| :------------------ | :--------------------------------------------------------------- | :------------------------------------------------------- |
| Developer dashboard | `https://uni.staging.fluzapp.com/apps-and-integrations`          | `https://fluz.app/for-developers`                        |
| OAuth authorize     | `https://uni.staging.fluzapp.com/authorize`                      | `https://fluz.app/authorize`                             |
| Token exchange      | `https://uni.staging.fluzapp.com/token/exchange`                 | `https://fluz.app/token/exchange`                        |
| Token refresh       | `https://uni.staging.fluzapp.com/token/refresh`                  | `https://fluz.app/token/refresh`                         |
| GraphQL API         | `https://transactional-graph.staging.fluzapp.com/api/v1/graphql` | `https://transactional-graph.fluzapp.com/api/v1/graphql` |

<Note>
  The GraphQL host and the OAuth host are different services with different domains. Swapping one and not the other is the single most common cutover mistake — and it fails in a confusing way, because authorization succeeds and then every API call is rejected.
</Note>

### Reaching each portal

The live portal is at `https://fluz.app/for-developers`. From there, the developer tab has an **'Open Staging'** link into the staging portal — your login credentials are the same in both. Each portal shows only that environment's applications and API keys.

***

## Make the environment a variable

If any host, key, or redirect URI is a string literal in your codebase, fix that before cutting over. Everything environment-specific belongs in configuration.

<CodeGroup>
  ```javascript Node.js theme={null}
  const ENVIRONMENTS = {
    staging: {
      authorizeUrl: 'https://uni.staging.fluzapp.com/authorize',
      tokenExchangeUrl: 'https://uni.staging.fluzapp.com/token/exchange',
      tokenRefreshUrl: 'https://uni.staging.fluzapp.com/token/refresh',
      graphqlUrl: 'https://transactional-graph.staging.fluzapp.com/api/v1/graphql',
      redirectUri: 'https://staging.example.com/oauth/finalize',
    },
    production: {
      authorizeUrl: 'https://fluz.app/authorize',
      tokenExchangeUrl: 'https://fluz.app/token/exchange',
      tokenRefreshUrl: 'https://fluz.app/token/refresh',
      graphqlUrl: 'https://transactional-graph.fluzapp.com/api/v1/graphql',
      redirectUri: 'https://app.example.com/oauth/finalize',
    },
  };

  export const fluz = ENVIRONMENTS[process.env.FLUZ_ENV ?? 'staging'];

  // Credentials come from your secret store, keyed by the same environment name.
  export const credentials = {
    clientId: process.env.FLUZ_CLIENT_ID,
    clientSecret: process.env.FLUZ_CLIENT_SECRET,
    apiKey: process.env.FLUZ_API_KEY,
    apiSecret: process.env.FLUZ_API_SECRET,
  };
  ```

  ```python Python theme={null}
  import os

  ENVIRONMENTS = {
      "staging": {
          "authorize_url": "https://uni.staging.fluzapp.com/authorize",
          "token_exchange_url": "https://uni.staging.fluzapp.com/token/exchange",
          "token_refresh_url": "https://uni.staging.fluzapp.com/token/refresh",
          "graphql_url": "https://transactional-graph.staging.fluzapp.com/api/v1/graphql",
          "redirect_uri": "https://staging.example.com/oauth/finalize",
      },
      "production": {
          "authorize_url": "https://fluz.app/authorize",
          "token_exchange_url": "https://fluz.app/token/exchange",
          "token_refresh_url": "https://fluz.app/token/refresh",
          "graphql_url": "https://transactional-graph.fluzapp.com/api/v1/graphql",
          "redirect_uri": "https://app.example.com/oauth/finalize",
      },
  }

  FLUZ = ENVIRONMENTS[os.environ.get("FLUZ_ENV", "staging")]

  # Credentials come from your secret store, keyed by the same environment name.
  CREDENTIALS = {
      "client_id": os.environ["FLUZ_CLIENT_ID"],
      "client_secret": os.environ["FLUZ_CLIENT_SECRET"],
      "api_key": os.environ["FLUZ_API_KEY"],
      "api_secret": os.environ["FLUZ_API_SECRET"],
  }
  ```
</CodeGroup>

<Note>
  Default to staging, never to production. If a deployment loses its environment variable, you want it hitting the test environment, not moving real money.
</Note>

***

## Rebuild your app in the live portal

Work through [Create an OAuth App](/create-an-o-auth-app) again against `https://fluz.app/for-developers`. Then check every one of these — each is something people forget:

<Steps>
  <Step title="Reissue every credential">
    Production `client_id`, `client_secret`, `apiKey`, and `apiSecret` are all new values. Put them in your production secret store. Confirm no staging value survives in a production config.
  </Step>

  <Step title="Re-register redirect URIs against production hosts">
    Your production callback URL, in its exact canonical form. Then **remove any `localhost` or staging URIs** — a production app should not accept a redirect to a developer's laptop.
  </Step>

  <Step title="Re-point webhook URLs">
    At production endpoints that are publicly reachable, monitored, and alerting. Re-do the per-event subscriptions; they don't copy across. If you used a catch-all URL in staging, decide whether you actually want that in production.
  </Step>

  <Step title="Set Origin to your production domain">
    For embedded widgets, this must match the domain actually serving the page, or the widget won't load.
  </Step>

  <Step title="Re-select your scopes">
    Scope selections don't transfer. Walk your integration's API calls and confirm each one's scope is checked on the production app's Permissions tab. A missing scope is silently dropped, not rejected.
  </Step>

  <Step title="Finish the Overview tab">
    Name, subtitle, description, avatar, and logomark are what real users now see on a real consent screen deciding whether to give you access to their money. Placeholder text ships to production if you let it.
  </Step>

  <Step title="Confirm your app's status">
    Applications carry a status in the dashboard — an app sitting in review is not yet an app your customers can use. Confirm your production app is active before you announce anything.
  </Step>
</Steps>

***

## What behaves differently in production

Staging mirrors production's capabilities and transaction flows without real money. That's most of the surface, but not all of it.

|                     | Staging                              | Production                                                                 |
| :------------------ | :----------------------------------- | :------------------------------------------------------------------------- |
| Funds               | Simulated                            | Real. Transfers are irreversible on your side                              |
| KYC                 | Test paths with predictable outcomes | Real verification against real identity data, with real declines           |
| Card authorizations | Simulated                            | Live network authorizations and real declines                              |
| Settlement timing   | Immediate or simulated               | Real banking timelines                                                     |
| Funding sources     | Test bank cards and accounts         | Real cards and Plaid-linked accounts                                       |
| Failure modes       | The ones you chose to test           | Insufficient funds, expired cards, network declines, verification failures |

Three consequences worth planning for:

* **Idempotency stops being optional.** Every money-moving call needs a unique `idempotencyKey`, and widget tokens need a unique `jti`. In staging a duplicate is a nuisance; in production it's a double payment. See [Idempotency](/docs/idempotency-requests).
* **Your error handling gets exercised.** Test users don't get declined for insufficient funds or fail KYC in ways you didn't script. Every failure path needs a defined user-facing outcome before launch, not after.
* **Reconciliation matters.** Verify balances on both sides of a transfer rather than assuming success from a 200 response.

***

## Data hygiene, both directions

**Never put production data in staging.** No real customer details, no real financial information, no PII. Staging is for data created explicitly for testing.

The reverse also holds: don't carry test users, test funding sources, or test webhook payloads into production. Test artifacts in a live ledger are hard to unpick later, and some of them can't be deleted.

***

## Widget cutover

If you're shipping an [embedded widget](/developers/widgets), the same rules apply plus these:

* **Regenerate the embed code from the production app's Installation tab.** The `apiKey` baked into the snippet is environment-specific.
* **Sign `patToken`s with your production `apiSecret`**, server-side. Confirm the secret is loaded from your production secret store and that the token generator is not still pointing at a staging value.
* **Confirm `Origin`** matches your production domain exactly.
* **Re-check the transaction type.** Pay-In and Payout move money in opposite directions; verify the direction against a real transfer before you open it to users.

***

## Pre-launch checklist

<AccordionGroup>
  <Accordion title="Configuration">
    * Production app created, configured, and active
    * All four credentials reissued and stored in the production secret store
    * No staging or localhost redirect URIs remain on the production app
    * Webhook URLs point at production endpoints, with event subscriptions re-selected
    * Scopes re-selected and matched against your actual API calls
    * Overview tab complete — name, subtitle, description, avatar, logomark
  </Accordion>

  <Accordion title="Code">
    * No hard-coded hosts, keys, or redirect URIs anywhere in the codebase
    * Environment resolution defaults to staging
    * Both the OAuth host and the GraphQL host swapped
    * Idempotency keys generated per operation, not per session
    * Callback route is idempotent and validates `state`
    * Token refresh runs before expiry rather than reacting to a failure
  </Accordion>

  <Accordion title="Operations">
    * Webhook endpoint monitored, with alerting on failure to deliver or process
    * Logging captures request identifiers and idempotency keys, and never captures secrets, PANs, or PII
    * Someone owns the "user can't connect" and "transfer stuck" runbooks
    * You've run one real end-to-end transaction at the smallest possible amount, in both directions, and reconciled both ledgers
  </Accordion>

  <Accordion title="Rollout">
    * Internal users first, then a small cohort, then general availability
    * You can disable the integration without a code deploy — a feature flag, not a rollback
    * Re-authorization path built and tested, for when refresh tokens expire or users revoke
  </Accordion>
</AccordionGroup>

***

## Common cutover failures

| Symptom                                                | Cause                                                                             |
| :----------------------------------------------------- | :-------------------------------------------------------------------------------- |
| OAuth succeeds, then every API call is rejected        | GraphQL host still pointed at staging (or vice versa)                             |
| `/authorize` errors on the production URL              | Production app's redirect URI not registered, or `client_id` is the staging value |
| Token exchange fails immediately after a clean consent | Basic auth header built from staging `client_id`/`client_secret`                  |
| Widget won't render on the live site                   | `Origin` still set to the staging or localhost domain                             |
| Widget opens but the transaction is rejected           | `patToken` signed with the staging `apiSecret`                                    |
| A permission that worked in staging now fails          | Scopes weren't re-selected on the production app                                  |
| No webhooks in production                              | URLs weren't re-pointed, or event subscriptions weren't re-selected               |
| Consent screen shows a placeholder app name            | Production Overview tab never filled in                                           |
| Duplicate transfers                                    | Idempotency key reused or regenerated per retry                                   |

***

## Next steps

<CardGroup cols={2}>
  <Card title="Create an OAuth app" icon="sliders" href="/create-an-o-auth-app">
    Re-run every tab against your production app.
  </Card>

  <Card title="Grant flow" icon="user-check" href="/client-facing-o-auth-grant-flow">
    Verify the consent screen on production hosts.
  </Card>

  <Card title="Refresh an access token" icon="refresh-cw" href="/refresh-o-auth-access-token">
    Keep production connections alive without re-prompting.
  </Card>

  <Card title="API features" icon="sparkles" href="/features">
    Everything now running against real money.
  </Card>
</CardGroup>
