# Payees

A **payee** is a registered counterparty under your seller account — a sub-merchant, marketplace seller, or any other recipient that is eligible to receive payouts and split shares. Every payout and split rule line targets a payee.

Payees have two API surfaces with different auth models:

| Surface | Auth | Operations |
|---------|------|------------|
| REST `/payments-api/v1/payees` | Bearer token | Full CRUD: list, get, create, update, delete |
| GraphQL `/dashboard/graphql` | `dashboard_session` cookie | Full CRUD: list, get, create, update, delete |

> [!NOTE]
> The REST surface is the integrator-facing API. Every REST payee operation is scoped to the seller account behind your Bearer token: the `sellerAccountId` and `legalEntityId` are derived from the token and are **never** read from the request body, and a payee belonging to another seller returns `404` (so payee IDs cannot be probed across sellers). The GraphQL surface is the backoffice/dashboard API (cookie-authenticated, legal-entity scoped) and exposes the same operations plus split-rule-line attachment.

---

## Payee fields (from `schema.graphql`)

### `type Payee`

| Field | Type | Description |
|-------|------|-------------|
| `id` | `Int!` | Numeric payee ID — use this as the `payeeId` when creating payout methods |
| `legalEntityId` | `Int!` | Legal entity this payee belongs to |
| `sellerAccountId` | `Int!` | Your seller account ID |
| `displayName` | `String` | Human-readable name shown in the dashboard |
| `role` | `PayeeRole!` | `Seller`, `Contractor`, or `Tax` |
| `status` | `PayeeStatus!` | `Active` or `Disabled` |
| `isActive` | `Boolean!` | Convenience flag derived from `status` |
| `email` | `String` | Contact email |
| `phone` | `String` | Contact phone |
| `businessName` | `String` | Legal business name |
| `businessType` | `String` | Type of business entity |
| `businessAddress` | `String` | Registered business address |
| `taxId` | `String` | Tax identification number |
| `payeeContractId` | `String` | Your internal contract reference |
| `wallets` | `[Wallet!]!` | Wallets assigned to this payee |
| `createdAt` | `Time!` | Creation timestamp |
| `updatedAt` | `Time!` | Last update timestamp |

---

## REST — Full CRUD

**Auth: Bearer token** (`Authorization: Bearer <token>`)

| Operation | Method & path |
|-----------|---------------|
| List payees | `GET /payments-api/v1/payees?limit={n}&offset={n}` |
| Get a payee | `GET /payments-api/v1/payees/{payee_id}` |
| Create a payee | `POST /payments-api/v1/payees` |
| Update a payee | `PATCH /payments-api/v1/payees/{payee_id}` |
| Delete a payee | `DELETE /payments-api/v1/payees/{payee_id}` |

Every operation is scoped to the seller account behind your Bearer token. `role` accepts the lowercase values `contractor` and `tax`.

> [!TIP]
> **Onboarding a payee just to pay it?** `POST /payments-api/v1/payees/setup` creates a payee **and** its first payout method in a single atomic call, so you can pay a brand-new payee in two requests total. See the [2-call quick path](https://api.fynex.ai/payments-api/v2/docs#tag/payouts) in the Payouts guide.

### Payee response shape

All single-payee responses (`GET` / `POST` / `PATCH`) return the same object; the list endpoint wraps an array of these under `payees`:

```json
{
  "id": 101,
  "legalEntityId": 77,
  "sellerAccountId": 42,
  "displayName": "Acme Supplies Ltd",
  "role": "Contractor",
  "status": "Active",
  "isActive": true,
  "payeeContractId": "pc_001",
  "email": "finance@acme.example.com",
  "phone": "+447700900123",
  "businessName": "Acme Supplies Limited",
  "businessType": "limited_company",
  "businessAddress": "1 Example Street, London, GB",
  "taxId": "GB123456789",
  "createdAt": "2026-06-05T10:00:00Z",
  "updatedAt": "2026-06-05T10:00:00Z"
}
```

### List payees

```
GET /payments-api/v1/payees?limit={n}&offset={n}
```

Returns a paginated list of payees belonging to your seller account. `limit` (1–100, default 20) and `offset` (≥0) are tolerant — out-of-range or non-numeric values silently fall back to the defaults.

```bash
curl -sS "https://api.fynex.ai/payments-api/v1/payees?limit=20&offset=0" \
  -H "Authorization: Bearer $FYNEX_TOKEN"
```

```json
{ "payees": [ { "id": 101, "displayName": "Acme Supplies Ltd", "role": "Contractor", "status": "Active" } ], "total": 1, "limit": 20, "offset": 0 }
```

### Get a payee

```bash
curl -sS "https://api.fynex.ai/payments-api/v1/payees/101" \
  -H "Authorization: Bearer $FYNEX_TOKEN"
```

A payee that does not exist, or belongs to another seller, returns `404`.

### Create a payee

`sellerAccountId` and `legalEntityId` are **not** accepted in the body — they are derived from your token. A wallet in your seller account's currency is provisioned for the payee automatically.

```bash
curl -sS -X POST "https://api.fynex.ai/payments-api/v1/payees" \
  -H "Authorization: Bearer $FYNEX_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "displayName": "Acme Supplies Ltd",
    "role": "contractor",
    "payeeContractId": "pc_001",
    "email": "finance@acme.example.com",
    "businessName": "Acme Supplies Limited",
    "taxId": "GB123456789"
  }'
```

Returns `201 Created` with the payee object. A `payeeContractId` already in use within your seller account returns `409 Conflict`.

### Update a payee

`PATCH` is a partial update — send only the fields you want to change; omitted fields are left untouched.

```bash
curl -sS -X PATCH "https://api.fynex.ai/payments-api/v1/payees/101" \
  -H "Authorization: Bearer $FYNEX_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "displayName": "Acme Supplies (EU) Ltd", "email": "eu-finance@acme.example.com" }'
```

Returns `200 OK` with the updated payee object. A foreign/unknown `payee_id` returns `404`.

> [!NOTE]
> `role` is immutable after creation. Supplying a `role` that differs from the payee's current role returns `400` (`role cannot be changed after creation`) — the payee's wallet is provisioned by role at creation time and is not re-provisioned on update. Sending the same `role`, or omitting it, is fine.

#### Archive / restore via `status`

`status` accepts the lowercase values `active` and `disabled`. Setting it archives or restores the payee; `isActive` is kept in sync automatically (you do not send it):

```bash
# Archive (equivalent to DELETE, see below)
curl -sS -X PATCH "https://api.fynex.ai/payments-api/v1/payees/101" \
  -H "Authorization: Bearer $FYNEX_TOKEN" -H "Content-Type: application/json" \
  -d '{ "status": "disabled" }'

# Restore a previously archived payee
curl -sS -X PATCH "https://api.fynex.ai/payments-api/v1/payees/101" \
  -H "Authorization: Bearer $FYNEX_TOKEN" -H "Content-Type: application/json" \
  -d '{ "status": "active" }'
```

### Delete (archive) a payee

`DELETE` is **non-destructive and reversible**: it *archives* the payee by setting its status to `Disabled` (and `isActive` to `false`). The payee is **not** removed — a subsequent `GET /payees/{payee_id}` still returns it (now `Disabled`), and you can restore it at any time with `PATCH {"status":"active"}`.

```bash
curl -sS -X DELETE "https://api.fynex.ai/payments-api/v1/payees/101" \
  -H "Authorization: Bearer $FYNEX_TOKEN"
```

```json
{ "archived": true }
```

> [!WARNING]
> **Archiving a payee that is still referenced by an active split rule will cause its payouts to fail — by design.** The split/payout engine is fail-closed: it never routes funds to a `Disabled` payee. If an archived payee is still a target of an active split rule, the affected split executions and payouts **fail with an explanatory error** (funds are never partially routed or silently dropped) until you either restore the payee (`PATCH {"status":"active"}`) or remove it from the split rule. The split-rule lines themselves are left untouched by archiving, so restoring the payee cleanly resumes payouts.

Returns `200 OK`. Archiving an already-archived payee is a no-op that still returns `200`. A foreign/unknown `payee_id` returns `404`. `DELETE` is exactly equivalent to `PATCH {"status":"disabled"}` — use whichever fits your client.

---

## GraphQL — Full CRUD

**Auth: `dashboard_session` cookie** — see [GraphQL Authentication](https://api.fynex.ai/payments-api/v2/docs#tag/graphql-auth) for the login flow.

All five operations are available on `/dashboard/graphql`:

| Operation | Type | Permission |
|-----------|------|------------|
| `payees(limit, offset)` | Query | `PAYEES_READ` |
| `payee(id: Int!)` | Query | `PAYEES_READ` |
| `createPayee(input: CreatePayeeInput!)` | Mutation | `PAYEES_CREATE` |
| `updatePayee(id: Int!, input: UpdatePayeeInput!)` | Mutation | `PAYEES_UPDATE` |
| `deletePayee(id: Int!)` | Mutation | `PAYEES_DELETE` — **staff surface only**, not callable on `/dashboard/graphql` |

### `CreatePayeeInput` fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `legalEntityId` | `Int!` | Yes | Legal entity to associate this payee with |
| `merchantId` | `ID!` | Yes | Your seller account ID, as a GraphQL `ID` (`"42"` and `42` are both accepted) |
| `role` | `PayeeRole!` | Yes | `Seller`, `Contractor`, or `Tax` |
| `displayName` | `String!` | Yes | Human-readable payee name |
| `payeeContractId` | `String` | No | Your internal reference |
| `email` | `String` | No | Payee contact email |
| `phone` | `String` | No | Payee contact phone |
| `businessName` | `String` | No | Legal business name |
| `businessType` | `String` | No | Business entity type |
| `businessAddress` | `String` | No | Registered address |
| `taxId` | `String` | No | Tax identification number |
| `splitRuleLine` | `PayeeSplitRuleLineInput` | No | Attach a split rule allocation at creation time |

### `UpdatePayeeInput` fields

All fields are optional. Only fields you supply are updated; omit any field to leave it unchanged.

| Field | Type | Description |
|-------|------|-------------|
| `legalEntityId` | `Int!` | Legal entity (required if updating) |
| `displayName` | `String` | New display name |
| `role` | `PayeeRole` | `Seller`, `Contractor`, or `Tax` |
| `merchantId` | `ID` | Seller account ID |
| `email` | `String` | Contact email |
| `phone` | `String` | Contact phone |
| `businessName` | `String` | Legal business name |
| `businessType` | `String` | Business entity type |
| `businessAddress` | `String` | Registered address |
| `taxId` | `String` | Tax identification number |
| `payeeContractId` | `String` | Internal contract reference |
| `splitRuleLine` | `PayeeSplitRuleLineInput` | Update split allocation |

---

## Create a payee — code samples

#### curl

```bash
# First obtain the session cookie (see GraphQL Authentication guide)
curl -sc cookies.txt \
  -X POST https://api.fynex.ai/api/v1/login/dashboard \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "password": "your_password"}'

# Create the payee
curl -b cookies.txt \
  -X POST https://api.fynex.ai/dashboard/graphql \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation CreatePayee($input: CreatePayeeInput!) { createPayee(input: $input) { id displayName role status } }",
    "variables": {
      "input": {
        "legalEntityId": 1,
        "merchantId": "42",
        "role": "Seller",
        "displayName": "Acme Supplies Ltd",
        "email": "payments@acme.example.com",
        "businessName": "Acme Supplies Limited",
        "taxId": "GB123456789"
      }
    }
  }'
```

#### JavaScript (Apollo/fetch)

```js
const BASE = 'https://api.fynex.ai';

// Assumes login was already called and the cookie is present
async function createPayee(input) {
  const res = await fetch(`${BASE}/dashboard/graphql`, {
    method: 'POST',
    credentials: 'include',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      query: `
        mutation CreatePayee($input: CreatePayeeInput!) {
          createPayee(input: $input) {
            id
            displayName
            role
            status
          }
        }
      `,
      variables: { input },
    }),
  });
  const { data, errors } = await res.json();
  if (errors?.length) throw new Error(errors[0].message);
  return data.createPayee;
}

const payee = await createPayee({
  legalEntityId: 1,
  merchantId: '42',
  role: 'Seller',
  displayName: 'Acme Supplies Ltd',
  email: 'payments@acme.example.com',
  businessName: 'Acme Supplies Limited',
  taxId: 'GB123456789',
});
console.log(payee); // { id: 7, displayName: 'Acme Supplies Ltd', role: 'Seller', status: 'Active' }
```

#### Python

```python
import requests

BASE = "https://api.fynex.ai"
session = requests.Session()

# Login first
session.post(
    f"{BASE}/api/v1/login/dashboard",
    json={"email": "you@example.com", "password": "your_password"},
).raise_for_status()

mutation = """
  mutation CreatePayee($input: CreatePayeeInput!) {
    createPayee(input: $input) {
      id
      displayName
      role
      status
    }
  }
"""
variables = {
    "input": {
        "legalEntityId": 1,
        "merchantId": "42",
        "role": "Seller",
        "displayName": "Acme Supplies Ltd",
        "email": "payments@acme.example.com",
        "businessName": "Acme Supplies Limited",
        "taxId": "GB123456789",
    }
}
resp = session.post(
    f"{BASE}/dashboard/graphql",
    json={"query": mutation, "variables": variables},
)
resp.raise_for_status()
body = resp.json()
if "errors" in body:
    raise RuntimeError(body["errors"][0]["message"])
print(body["data"]["createPayee"])
# {'id': 7, 'displayName': 'Acme Supplies Ltd', 'role': 'Seller', 'status': 'Active'}
```

---

## Other operations

### List payees via GraphQL

```graphql
query ListPayees($limit: Int, $offset: Int) {
  payees(limit: $limit, offset: $offset) {
    id
    displayName
    role
    status
    email
    businessName
  }
}
```

### Get a single payee

```graphql
query GetPayee($id: Int!) {
  payee(id: $id) {
    id
    displayName
    role
    status
    email
    phone
    businessName
    businessAddress
    taxId
    payeeContractId
    wallets { id currencyCode }
    createdAt
  }
}
```

### Update a payee

```graphql
mutation UpdatePayee($id: Int!, $input: UpdatePayeeInput!) {
  updatePayee(id: $id, input: $input) {
    id
    displayName
    email
    updatedAt
  }
}
```

Pass only the fields you want to change in `$input`. `legalEntityId` is required in `UpdatePayeeInput` even when you are not changing it.

### Delete a payee

```graphql
# Not available on /dashboard/graphql — staff surface only. Shown for reference;
# sellers archive a payee with the REST DELETE below.
mutation DeletePayee($id: Int!) {
  deletePayee(id: $id)
}
```

Returns `true` on success.

> [!CAUTION]
> **`deletePayee` is not exposed on `/dashboard/graphql`.** A dashboard session calling it
> receives `Cannot query field "deletePayee" on type "Mutation"`; the mutation exists on the
> Fynex staff surface only. From your integration, use the REST `DELETE /payees/{payeeId}`
> described above — it archives the payee and is reversible.

> [!WARNING]
> Where it is available, the GraphQL `deletePayee` does **not** behave like the REST `DELETE`. GraphQL performs a database soft-delete (sets `deleted_at`), which **hides** the payee from subsequent reads and is **not** restorable through the API. The REST `DELETE` instead *archives* the payee (status `Disabled`, still queryable, restorable via `PATCH {"status":"active"}`). Ensure no active payout methods or split rule lines reference the payee before calling the GraphQL mutation.

---

## Common pitfalls

| Pitfall | Resolution |
|---------|------------|
| Calling `createPayee` (GraphQL) with a Bearer token | The `/dashboard/graphql` endpoint requires the `dashboard_session` cookie. Integrators should use the REST endpoints (Bearer token) instead. |
| Sending `sellerAccountId` / `legalEntityId` in a REST body | They are ignored — REST payees are always pinned to the seller account behind your Bearer token. |
| Forgetting `legalEntityId` on a GraphQL update | `UpdatePayeeInput.legalEntityId` is `Int!` — required even when not changing it. The REST `PATCH` has no such requirement. |
| Using an uppercase `role` on REST | REST accepts the lowercase values `contractor` and `tax`; anything else returns `400`. |
| Expecting REST `DELETE` to remove the payee | REST `DELETE` *archives* (status `Disabled`); the payee stays queryable and is restorable via `PATCH {"status":"active"}`. Only the GraphQL `deletePayee` hides the row (soft-delete). |

## See also

- **[GraphQL Authentication](https://api.fynex.ai/payments-api/v2/docs#tag/graphql-auth)** — Obtain a dashboard_session cookie before calling any GraphQL mutation.
- **[Payout Methods](https://api.fynex.ai/payments-api/v2/docs#tag/payout-methods)** — Register bank accounts as payout destinations for a payee.
- **[Payouts](https://api.fynex.ai/payments-api/v2/docs#tag/payouts)** — Send money from a seller wallet to a payee
