# Flow `externalId` contract (FLOW-09 / #3234)

**Status:** adopted contract; runtime implementation is pending the Flow aggregate.

`externalId` is the seller-provided join key for a Flow and its related
artifacts. It is not a globally unique identifier and it is not an internal
Flow ID.

## Identity and uniqueness

The uniqueness key is the tuple:

```text
(seller account, flow type, externalId)
```

- `seller account` is the authenticated seller scope. A value in one seller's
  namespace does not collide with the same value in another seller's namespace.
- `flow type` is the canonical Flow type, compared after the API's normal enum
  parsing. A value may be reused for a different Flow type in the same seller.
- `externalId` is compared as the submitted identifier after the API's normal
  validation/normalisation. The runtime implementation must document that
  normalisation before accepting writes; it must not silently trim or case-fold
  values as an undocumented compatibility rule.

A create request whose tuple already exists is a conflict, not a second Flow.
The API returns **HTTP `409 Conflict`** and includes the existing Flow's
canonical `flowId` in the error details. The response must not create or mutate
any Flow, event, invoice, or other linked artifact.

This collision response is also the retry/idempotency rule for `externalId`:
retrying a create with the same tuple converges on the existing `flowId`; it
must never create a duplicate. A separate request idempotency key, if added to
a Flow endpoint, cannot widen this uniqueness scope or override the conflict.

## Immutability and lifecycle

`externalId` is write-once. It is required on Flow creation, is returned on
reads, and cannot be changed by update, transition, close, or reopen operations.
The seller cannot reuse the tuple after a Flow closes: closure does not release
or recycle the uniqueness reservation. A replacement Flow must use a new
`externalId` (or a different Flow type).

The identifier may be copied into events and invoice metadata as a join
reference, but those copies do not establish identity and cannot be used to
change the source Flow.

## Runtime boundary

Runtime implementation is tracked separately in [FLOW-09b / #3309](https://github.com/fynex-ai/fynex-api/issues/3309). That follow-up is blocked on [FLOW-00 / #3225](https://github.com/fynex-ai/fynex-api/issues/3225), because the Flow aggregate and public endpoint do not exist yet.

This document is the contract for the future Flow aggregate only. There is currently no Flow aggregate, persistence constraint, handler, or public
money-moving endpoint in this repository. No implementation may claim that
`409`, immutability, or Flow-linked artifact propagation is live until the
aggregate, seller-scoped unique constraint, transaction boundary, and handlers
are implemented and covered by runtime tests.

The implementation follow-up must at minimum prove:

1. concurrent creates arbitrate on the seller/type/externalId unique constraint;
2. the losing create returns `409` with the winner's `flowId` and creates no
   linked rows;
3. update and lifecycle writes reject any externalId change before persistence;
4. a closed Flow still reserves its tuple; and
5. authenticated seller scope is part of every lookup and write.
