Skip to content
In this section

Payees

Register and manage payees — sub-merchants, marketplace sellers, and payout recipients — via the REST CRUD endpoints and the GraphQL CRUD surface.

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! Contractor or Tax for payees you create; Itself (the seller's own payee) and fynex_platform are assigned by Fynex. Seller is a deprecated legacy value — do not send it
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 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:

{
  "id": 101,
  "legalEntityId": 77,
  "merchantId": "16ddc5f1-39ca-4b7c-9bfa-bf5779b8c117",
  "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.

curl -sS "https://api.fynex.ai/payments-api/v1/payees?limit=20&offset=0" \
  -H "Authorization: Bearer $FYNEX_TOKEN"
{ "payees": [ { "id": 101, "displayName": "Acme Supplies Ltd", "role": "Contractor", "status": "Active" } ], "total": 1, "limit": 20, "offset": 0 }

Get a payee

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.

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.

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

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

curl -sS -X DELETE "https://api.fynex.ai/payments-api/v1/payees/101" \
  -H "Authorization: Bearer $FYNEX_TOKEN"
{ "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.


Hosted payee onboarding contract (ONBOARD-01)

This is the contract for the planned hosted onboarding session. It is not a registered public route yet. Until the runtime session store, redirect handler, and webhook consumer implement these guarantees, integrations must use the existing payee verification routes (POST /payees/{payee_id}/verification) or vendor verification link route. No identity or money-moving endpoint is advertised by this contract-only slice.

Proposed operation

POST /payments-api/v1/payees/{payee_id}/onboarding-sessions
Authorization: Bearer <seller_sk_key>
Idempotency-Key: <stable-client-key>
Content-Type: application/json

{ "returnUrl": "https://merchant.example/payees/onboarding/complete" }

The authenticated seller owns the payee: a payee belonging to another seller is indistinguishable from an unknown payee and returns 404. The operation must not accept sellerAccountId, legalEntityId, or an identity-provider applicant id from the caller. It creates no payout method and moves no money.

The successful response is:

{
  "sessionId": "pos_01J...",
  "payeeId": 101,
  "onboardingUrl": "https://onboarding.fynex.ai/payee/pos_01J...",
  "expiresAt": "2026-09-15T12:00:00Z",
  "status": "pending"
}

returnUrl is optional only when the seller has a configured default. When provided it must be HTTPS, have no userinfo, resolve to a public address, and match the seller's allowed-return-hosts configuration. The service validates it before creating a session; it never reflects an unvalidated URL into a redirect.

Session, replay, and completion semantics

  • A session is owned by exactly one seller and payee, expires at expiresAt, and is unusable after expiry (410 onboardingSessionExpired). Expiry is server-enforced, not merely a UI timer.
  • Idempotency-Key is required and scoped to the seller and payee. The same key with an equivalent body returns the original session (200); reusing it with a different returnUrl returns 409 idempotencyConflict. A retried request never creates a second provider applicant or session.
  • Opening the URL is safe to repeat. The provider's hosted result is accepted only after a verified provider webhook updates the payee's verification state. A browser redirect is advisory and cannot enable payouts by itself.
  • On completion, the browser is redirected to the validated returnUrl with only opaque sessionId and status parameters. No identity document, provider token, or secret is placed in the URL. If no return URL is usable, the session ends on a Fynex-hosted status page.
  • The seller receives the existing payee verification webhook/event state transition. Webhook delivery is signed, retried until a 2xx, and must be consumed idempotently; duplicate events do not create a second transition.

The route remains intentionally absent from OpenAPI until these runtime properties have a durable implementation and authenticated integration tests.

GraphQL — Full CRUD

Auth: dashboard_session cookie — see GraphQL Authentication 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: PayeeInput!) Mutation PAYEES_CREATE
updatePayee(id: Int!, input: PayeeInput!) Mutation PAYEES_UPDATE
deletePayee(id: Int!) Mutation PAYEES_DELETE — staff surface only, not callable on /dashboard/graphql

PayeeInput 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 Contractor or Tax (Itself is reserved for the seller's own payee; Seller is deprecated)
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

PayeeInput 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 Contractor or Tax (Seller is deprecated)
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

Routing is not set through this mutation. splitRuleLine was removed from PayeeInput; it had been rejected with an error since 2026-08-26, so this table promised a field that could not be used. Configure an inactive split rule, preview the saved version, then activate it — and manage individual lines with upsertSplitRuleLineByPayee / deleteSplitRuleLineByPayee.


Create a payee — code samples

curl

# 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: PayeeInput!) { createPayee(input: $input) { id displayName role status } }",
    "variables": {
      "input": {
        "legalEntityId": 1,
        "merchantId": "42",
        "role": "Contractor",
        "displayName": "Acme Supplies Ltd",
        "email": "payments@acme.example.com",
        "businessName": "Acme Supplies Limited",
        "taxId": "GB123456789"
      }
    }
  }'

JavaScript (Apollo/fetch)

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: PayeeInput!) {
          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: 'Contractor',
  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

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: PayeeInput!) {
    createPayee(input: $input) {
      id
      displayName
      role
      status
    }
  }
"""
variables = {
    "input": {
        "legalEntityId": 1,
        "merchantId": "42",
        "role": "Contractor",
        "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

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

Get a single payee

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

Update a payee

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

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

Delete a payee

# 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 PayeeInput.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 — Obtain a dashboard_session cookie before calling any GraphQL mutation.
  • Payout Methods — Register bank accounts as payout destinations for a payee.
  • Payouts — Send money from a seller wallet to a payee