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

# Managing External Reference IDs

> Use your own user identifiers to drive the Fluz API — how to assign one, where it appears across OAuth, widgets, transfers, and webhooks, and how to choose one you won't regret.

An **external reference ID** is your own identifier for a Fluz user. It lets you operate the Fluz API using the IDs you already have in your system — without storing or passing Fluz's internal `userId` and `accountId` for every user.

When a user authorizes your application, you attach your identifier to that grant. Fluz stores the pairing between your identifier and the user's Fluz account, scoped to your application. From that point forward you can reference the user by your own ID when generating tokens, sending transfers, and matching webhook events.

The practical payoff: **you never have to build a Fluz-ID lookup table.** A webhook arrives, it carries your user ID, you route it. No join, no cache, no reconciliation job.

## One value, three field names

The same value appears under a different name depending on the surface. This is the most common source of confusion on this page, so it's worth memorizing before you start.

| Surface                               | Field name                      | Direction  |
| :------------------------------------ | :------------------------------ | :--------- |
| OAuth authorization URL               | `external_id` (query parameter) | You → Fluz |
| Widget pre-approved transaction token | `externalId` (JWT claim)        | You → Fluz |
| GraphQL API                           | `externalReferenceId`           | Both       |
| Webhook event payloads                | `externalReferenceId`           | Fluz → You |
| OAuth access tokens                   | embedded in the issued token    | Fluz → You |

Because it's embedded in the access token, the association survives token refreshes — you assign it once, at grant time, and it persists.

***

## Choosing an identifier

<Warning>
  **Never use PII.** No email addresses, phone numbers, or names.

  Two reasons, both concrete. First, they change — people switch emails and phone numbers, and your mapping breaks permanently because the value is immutable once set. Second, this value travels in **query strings** and **JWT claims**, which means it lands in browser history, referrer headers, proxy logs, and your own application logs. Don't put personal data there.
</Warning>

| Use                                        | Avoid                                      | Why                                           |
| :----------------------------------------- | :----------------------------------------- | :-------------------------------------------- |
| Your database primary key                  | Email address, phone number                | Mutable, and it's PII in a URL                |
| A UUID you mint per user                   | Sequential integers                        | Enumerable; also leaks your user count        |
| An opaque, prefixed ID like `usr_8f3d2a91` | An order ID, session ID, or transaction ID | Maps to one event, not one person — see below |

<Note>
  **The most expensive mistake is scoping it to the wrong thing.** An external reference ID identifies a *person*, permanently — not a transaction, a payout run, a session, or an order. If you pass a per-transaction identifier, the first transfer succeeds and the second one creates a second mapping to the same human, and you've forked one user into many with no way to merge them.

  If you find yourself generating a new value for each operation, you want an idempotency key, not an external reference ID.
</Note>

### The rules

| Property    | Rule                                                                                                                                                                           |
| :---------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Type        | Any string. Fluz does not impose a format.                                                                                                                                     |
| Uniqueness  | Must be unique per user **within your application**. One external reference ID maps to exactly one Fluz user.                                                                  |
| Scope       | Scoped to your OAuth client. The same Fluz user can carry a different external reference ID in another developer's application, and no other application can see or use yours. |
| Stability   | Immutable in practice. Once set on a grant, it is never overwritten.                                                                                                           |
| Environment | Mappings live with the application, so staging and production mappings are entirely separate. Nothing you create in staging exists in production.                              |

***

## Assigning one

### Standard OAuth applications

Append `external_id` to the authorization URL when you send the user to consent (see [Client-facing OAuth grant flow](/client-facing-o-auth-grant-flow)):

```text theme={null}
https://fluz.app/authorize?response_type=code&client_id=<CLIENT_ID>&redirect_uri=<REDIRECT_URI>&scopes=MAKE_DEPOSIT%20LIST_PAYMENT&state=<STATE>&external_id=usr_8f3d2a91
```

When the user completes the grant, Fluz records `usr_8f3d2a91` against that user's authorization of your application.

Optional here, but strongly recommended. Adopting it later means backfilling grants one re-authorization at a time.

### Widget applications

<Warning>
  **Required.** All widget application types — deposit, payout, pay-in, virtual card, gift card catalog, bill pay, and external payout — require an external reference ID to establish a user session. Without one the request is rejected:

  ```text theme={null}
  External reference ID is required for <type> applications
  ```
</Warning>

For widgets, the value travels as the **`externalId` claim inside the signed pre-approved transaction token**, alongside the amount and transaction type. You generate it server-side; it is not a client-side init option. See [Set Up Your Server](/developers/setting-up-your-server).

```javascript theme={null}
import jwt from 'jsonwebtoken';
import { v4 as uuidv4 } from 'uuid';

const generatedToken = jwt.sign(
  {
    amount,
    apiKey: process.env.FLUZ_API_KEY,
    transactionType,           // DEPOSIT (Pay-In) or WITHDRAW (Payout)
    externalId: 'usr_8f3d2a91', // your user, stable across every session
    jti: uuidv4(),              // unique per transaction — not the same thing
  },
  process.env.FLUZ_API_SECRET,
  { expiresIn: '1 day' }
);
```

<Note>
  `externalId` and `jti` sit next to each other in the same token and answer different questions. `externalId` is *who* — stable for the life of the user. `jti` is *which transaction* — new every time. Reusing `jti` breaks idempotency; changing `externalId` forks your user.
</Note>

***

## Using one

### Address transfer destinations

When creating a wallet transfer to another Fluz account (see [Transfer to Another Fluz Wallet](/features/transfer-to-another-fluz-wallet)), identify the destination by your own ID instead of a Fluz account ID:

```graphql theme={null}
mutation {
  createTransfer(input: {
    idempotencyKey: "1f7c5a2e-9b14-4c6d-8e3f-2a90d4b7c1aa"
    amount: 25.00
    destination: { externalReferenceId: "usr_8f3d2a91" }
  }) {
    transferId
  }
}
```

Provide either `destination.accountId` or `destination.externalReferenceId` — never both. The destination user must have authorized your application, or the transfer is rejected.

### Match webhook events to your users

Webhook payloads carry `externalReferenceId`, so you can route events without a lookup table:

```json theme={null}
{
  "userId": "5070d5a1-d71a-4190-91b0-f116eec51771",
  "accountId": "9c2e1b44-7a3d-4f08-b6e5-d18a3c7f0e22",
  "externalReferenceId": "usr_8f3d2a91",
  "eventType": "DEPOSIT_COMPLETE",
  "amount": 100.00
}
```

<Warning>
  Handle the field being absent. `externalReferenceId` is omitted where the user's grant has no external reference ID associated, or where the event is flagged as private. A handler that assumes the field is always present will throw on those deliveries — and a webhook handler that throws is a webhook you didn't process.
</Warning>

See [Configure App Widget](/developers/configure-app-widget) for webhook setup.

### Getting user-scoped tokens

`generateUserAccessToken` does **not** accept an `externalReferenceId` — it identifies the user by `userId` and `accountId` (see [Generate a User Access Token](/recipes/generate-user-access-token)).

For users you reference by your own ID, use the OAuth flow instead. The grant already carries your identifier, and the tokens you get by exchanging the authorization code at `/token/exchange` are issued for that user with the association embedded. See [Exchange an OAuth Authorization Code](/exchange-an-o-auth-authorization-code).

***

## End to end

One user, one identifier, four surfaces.

<Steps>
  <Step title="Your system already knows this person">
    User `usr_8f3d2a91` in your database clicks **Connect Fluz**.
  </Step>

  <Step title="Assign at consent">
    You redirect to `/authorize` with `external_id=usr_8f3d2a91`. They sign in, verify if needed, and approve your scopes. Fluz binds `usr_8f3d2a91` to their account, for your application only.
  </Step>

  <Step title="Exchange">
    Your callback exchanges the `code` for an `accessToken` and `refreshToken`. The association is embedded, so it survives every future refresh. You store the tokens against `usr_8f3d2a91` — no Fluz UUIDs in your schema.
  </Step>

  <Step title="Operate">
    You pay them out with `destination: { externalReferenceId: "usr_8f3d2a91" }`, using your own ID as the address.
  </Step>

  <Step title="Reconcile">
    The completion webhook arrives carrying `externalReferenceId: "usr_8f3d2a91"`. You route it straight to that user's record and mark the payout settled. No join, no lookup, no cache.
  </Step>
</Steps>

***

## Lifecycle

### Backfilling an existing grant

If a user authorized your application before you adopted external reference IDs, supply one on a subsequent authorization and Fluz backfills it onto the existing grant — provided the grant doesn't already carry one. An existing value is never overwritten.

### Re-authorizing with a different value

Because an existing value is never overwritten, passing a *different* `external_id` for a user who already has one does not change the mapping. Plan on the first value being permanent. If your user IDs are unstable, mint a dedicated immutable ID for Fluz rather than reusing something you might migrate.

### Deleting users on your side

Never recycle an identifier. If you hard-delete a user and later reissue the same primary key to a different person, that new person inherits the old mapping — and the old person's Fluz account. Use UUIDs, or a monotonic sequence you never reset.

***

## Validation rules and errors

| Scenario                                                             | Result                                                                               |
| :------------------------------------------------------------------- | :----------------------------------------------------------------------------------- |
| `externalReferenceId` not found for your application                 | `No user found with externalReferenceId <value>.`                                    |
| Transfer destination with both `accountId` and `externalReferenceId` | `Provide either destination.accountId or destination.externalReferenceId, not both.` |
| Transfer destination user has not authorized your application        | `Destination account has not authorized this application.`                           |
| Widget session created without an external reference ID              | `External reference ID is required for <type> applications`                          |

## Troubleshooting

| Symptom                                                             | Almost always                                                                                                                    |
| :------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------- |
| `No user found with externalReferenceId` for a user you know exists | Wrong environment — the mapping was created in staging and you're calling production, or the reverse                             |
| Same person appears as multiple Fluz users                          | A per-transaction or per-session value was passed instead of a per-user one                                                      |
| Value you passed on re-authorization didn't take effect             | The grant already carried one; existing values are never overwritten                                                             |
| Webhook handler crashes intermittently                              | Field absent on some deliveries — grants without an association, or events flagged private                                       |
| Widget session rejected                                             | Widget types require the `externalId` claim in the `patToken`; confirm it's in the signed payload, not just in your init options |
| Transfer rejected as unauthorized                                   | Destination user hasn't authorized your application, regardless of the mapping existing                                          |

***

## What an external reference ID is not

* **Not** a Fluz `userId` or `accountId`. Those are Fluz-issued UUIDs; this one is issued by you.
* **Not** an idempotency key. That's `idempotencyKey` on API calls and `jti` in widget tokens, and it's unique per operation. This is unique per person.
* **Not** the `state` parameter in the OAuth flow. `state` is per-authorization-attempt CSRF protection and is not stored.
* **Not** the external account identifiers that appear on withdrawal records or linked funding sources. Those reference banking and processor records, not users.

***

## Next steps

<CardGroup cols={2}>
  <Card title="Grant flow" icon="user-check" href="/client-facing-o-auth-grant-flow">
    Where you assign the identifier for OAuth apps.
  </Card>

  <Card title="Set up your server" icon="server" href="/developers/setting-up-your-server">
    Where you assign it for widget apps.
  </Card>

  <Card title="Transfer to another Fluz wallet" icon="arrow-left-right" href="/features/transfer-to-another-fluz-wallet">
    Addressing transfers by your own ID.
  </Card>

  <Card title="Configure app webhooks" icon="webhook" href="/developers/configure-app-widget">
    Receiving events that carry it back.
  </Card>
</CardGroup>
