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
sellerAccountIdandlegalEntityIdare derived from the token and are never read from the request body, and a payee belonging to another seller returns404(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/setupcreates 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
roleis immutable after creation. Supplying arolethat differs from the payee's current role returns400(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 samerole, 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
Disabledpayee. 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-Keyis required and scoped to the seller and payee. The same key with an equivalent body returns the original session (200); reusing it with a differentreturnUrlreturns409 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
returnUrlwith only opaquesessionIdandstatusparameters. 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.
splitRuleLinewas removed fromPayeeInput; 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 withupsertSplitRuleLineByPayee/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
deletePayeeis not exposed on/dashboard/graphql. A dashboard session calling it receivesCannot query field "deletePayee" on type "Mutation"; the mutation exists on the Fynex staff surface only. From your integration, use the RESTDELETE /payees/{payeeId}described above — it archives the payee and is reversible.
Warning
Where it is available, the GraphQL
deletePayeedoes not behave like the RESTDELETE. GraphQL performs a database soft-delete (setsdeleted_at), which hides the payee from subsequent reads and is not restorable through the API. The RESTDELETEinstead archives the payee (statusDisabled, still queryable, restorable viaPATCH {"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