# Vendors

A **vendor** is a sub-merchant you onboard under your **platform** account in the merchant-of-record model: your buyers pay you, and you pass each vendor its share. Fynex screens every vendor from the details you supply, holds it to a cumulative payout limit until it completes verification, and gives you one payee per vendor to pay it through.

The vendor surface is available to **Platform** accounts only, using the platform's **own** API key. A merchant account, or a delegated dashboard session, receives `403 platformRequired`.

> [!NOTE]
> These endpoints are enabled per environment. If `POST /payments-api/v1/vendors` answers `404` on your account, ask support to enable vendor onboarding for you.

---

## Lifecycle

Every vendor response carries a `verification` block. `verification.status` is the one field to branch on:

| `verification.status` | Meaning | `payoutsEnabled` |
|---|---|---|
| `pending_fynex` | Fynex is screening (`reason: screening`) or reviewing. Nothing for you or the vendor to do. | `false` |
| `not_required` | Screening passed; no verification step is due. | `true`, under the cumulative limit |
| `required` | The vendor must complete a step. Fetch the link with `POST /vendors/{vendor_id}/verification-link` and hand it to the vendor. | `true` until the limit is reached |
| `in_progress` | The vendor has started the step. | unchanged |
| `complete` | Fully verified; the cumulative limit no longer applies. | `true` |
| `blocked` | Refused at screening (`reason: screening_refused`), declined at verification (`reason: declined`) or manually blocked (`reason: blocked`). | `false`, permanently |

Screening is **non-interactive**: the vendor is not contacted and uploads nothing. You supply the identity (company number + country, or name + date of birth + country) and Fynex screens it against sanctions, politically-exposed-person and adverse-media lists. It usually completes within the request; otherwise poll `GET /vendors/{vendor_id}` until `verification.status` leaves `pending_fynex`.

`paymentsEnabled` is always `false`: in the merchant-of-record model the vendor never takes a payment itself.

---

## Create a vendor

```http
POST /payments-api/v1/vendors
Authorization: Bearer <platform_sk_key>
Content-Type: application/json

{
  "externalRef": "vendor_8817",
  "entityType": "company",
  "displayName": "Blue Door Ceramics Ltd",
  "contactEmail": "owner@bluedoor.example",
  "country": "GB",
  "companyNumber": "09876543",
  "merchantCategoryCode": "5999",
  "storefrontUrl": "https://market.example/shops/blue-door",
  "address": { "street": "1 High Street", "city": "Edinburgh", "postalCode": "EH1 1AA" },
  "consent": { "sumsubPrivacyNoticeAccepted": true, "acceptedAt": "2026-09-03T12:00:00Z", "ip": "203.0.113.7", "noticeVersion": "2026-07" }
}
```

For an individual (sole trader), send `"entityType": "individual"` with a `person` object instead of `companyNumber`:

```json
"person": { "firstName": "Jane", "lastName": "Maker", "dob": "1990-04-12" }
```

`dob` is **required** (`YYYY-MM-DD`; the vendor must be at least 18). `displayName` is the registered name for a company and the trading name for an individual. `merchantCategoryCode` is the four-digit MCC describing what the vendor sells; it is required and screened. `storefrontUrl` is the vendor's page on your marketplace (https). `consent` is your attestation that the vendor accepted the verification provider's privacy notice on your side; it is required (`422 consentRequired`) once consent enforcement is on for your environment, and recorded whenever you send it.

```json
{
  "vendorId": "7c1f8a2e-6b3d-4e5f-9a0b-1c2d3e4f5a6b",
  "externalRef": "vendor_8817",
  "entityType": "company",
  "displayName": "Blue Door Ceramics Ltd",
  "paymentsEnabled": false,
  "payoutsEnabled": false,
  "payeeId": 101,
  "verification": { "status": "pending_fynex", "reason": "screening", "actionUrl": null, "actionExpiresAt": null },
  "payoutAllowance": { "currency": "GBP", "limitMinor": 10000, "usedMinor": 0, "remainingMinor": 10000,
                       "basis": "cumulative_lifetime_all_currencies", "enforcement": "monitored", "unlimited": false, "deferred": false },
  "nextTier": { "limitMinor": null, "unlimited": true,
                "requirements": [ { "code": "screening_passed", "status": "in_progress" },
                                  { "code": "identity_verification", "status": "not_started" },
                                  { "code": "agreement_signed", "status": "not_started" } ] },
  "createdAt": "2026-09-05T12:00:00Z",
  "updatedAt": "2026-09-05T12:00:00Z"
}
```

`201 Created` on creation. When the screening completes synchronously the verdict is applied before the response is written, so the first response is often already `not_required` with `payoutsEnabled: true`.

### Idempotency

`externalRef` is your identifier for the vendor and is unique within your platform. Re-sending an **equivalent** request (whitespace and letter case do not count) for the same reference returns the existing vendor with `200 OK` — safe to retry after a timeout. The same reference with a **different** body is `409 externalRefConflict`: you are reusing a reference for a different vendor.

### Responses

All errors use the standard envelope `{"error": "<code>"}`. Field-level validation failures name the field: `{"error": "person.dob: required; YYYY-MM-DD"}`.

| Status | `error` | When |
|--------|---------|------|
| `201` | — | Vendor created |
| `200` | — | Idempotent replay of an equivalent request |
| `400` | `<field>: <reason>` | A field is missing or malformed |
| `401` | | Missing or invalid Bearer token |
| `403` | `platformRequired` | Your account is not an active platform, or the credential is a delegated dashboard session rather than the platform's own key |
| `409` | `externalRefConflict` | `externalRef` already refers to a vendor created with a different request |
| `422` | `invalidCountry` | `country` is not an ISO 3166-1 alpha-2 code |
| `422` | `countryNotSupported` | A real country Fynex does not onboard vendors from |
| `422` | `mccProhibited` | The merchant category is prohibited |
| `422` | `vendorScreeningRefused` | This vendor cannot be onboarded through the API. Contact support if you believe this is wrong |
| `422` | `consentRequired` | Consent enforcement is on and `consent.sumsubPrivacyNoticeAccepted` was not `true` |
| `429` | `dailyVendorLimitReached` | Daily vendor creation limit for your platform; retry after 24 hours |
| `503` | `screeningUnavailable` | Screening is temporarily unavailable. Nothing was created; retry with the same `externalRef` |

The vendor mutation routes also have their own per-platform rate-limit bucket, separate from the general API limit; a `429` with an empty-body `Retry-After` header is that bucket.

---

## Read vendors

```http
GET /payments-api/v1/vendors/{vendor_id}
GET /payments-api/v1/vendors?limit=20&offset=0
```

The list is newest first and paginated like `/payees` (`limit` 1–100, default 20). A vendor that belongs to another platform, or does not exist, is `404 vendorNotFound`.

---

## Verification link

When a vendor's `verification.status` becomes `required` — you receive `MarketplaceVendorVerificationRequired` if you subscribed to the vendor events, and `GET /vendors/{vendor_id}` shows it — request a link and hand it to the vendor:

```http
POST /payments-api/v1/vendors/{vendor_id}/verification-link
Authorization: Bearer <platform_sk_key>
```

```json
{ "actionUrl": "https://…", "actionExpiresAt": "2026-09-06T12:00:00Z" }
```

The vendor opens the link and completes verification with the provider; you are not involved in the flow itself. Every call mints a fresh link (the previous one stays valid until its own expiry), so call it again if the vendor lost theirs. The link is bearer-like: send it over a channel you trust and do not log it.

| Status | `error` | When |
|--------|---------|------|
| `200` | — | Link minted |
| `403` | `platformRequired` | Not the platform's own key |
| `404` | `vendorNotFound` | Not your vendor |
| `409` | `verificationNotRequired` | No step is due for this vendor |
| `503` | `screeningUnavailable` | The provider could not mint a link; retry |

As the vendor progresses, `verification.status` moves through `in_progress` and `pending_fynex` to `complete` (or `blocked`), and the matching `MarketplaceVendorVerification*` events fire.

## Paying a vendor

Each vendor has exactly one **payee** (`payeeId`), owned by your platform account. Use it exactly like any other payee:

1. Attach bank details: `POST /payees/{payee_id}/payout-methods` — see [Payout methods](/payments-api/v2/docs/payout-methods.md).
2. Route the vendor's share: name the payee in a split rule — see [Splits](/payments-api/v2/docs/splits.md).
3. Pay out: `POST /payouts` from the payee's wallet — see [Payouts](/payments-api/v2/docs/payouts.md).

The payee is **disabled while `verification.status` is `pending_fynex`** and stays disabled if the vendor is `blocked`: a split naming it is refused at payment time, so no funds can accrue to an unscreened vendor. It is enabled the moment the screening passes.

### The cumulative payout limit

A screened vendor may receive up to **£100 (or equivalent) in cumulative payouts** before it must complete full verification. `payoutAllowance` shows where the vendor stands: `limitMinor` and `usedMinor` in GBP minor units (payouts in other currencies count at the current reference rate), `remainingMinor` what is left, and `enforcement` what happens at the boundary — `monitored` means a crossing is detected after the fact and verification is requested; `held_for_review` means a payout that would cross the limit waits for Fynex compliance. `deferred: true` means the total could not be computed right now (a payout currency has no current rate); nothing is enabled or disabled by it.

When a vendor crosses the limit Fynex asks it to verify and notifies you (`verification.status: required`). Vendors that complete verification show `unlimited: true` and no longer have a cumulative limit.

`nextTier` says exactly what lifts the limit: three requirements — `screening_passed` (Gate 1, automatic), `identity_verification` (the vendor completes verification via the link above) and `agreement_signed` (the vendor signs the seller agreement Fynex sends at the crossing) — each `complete`, `in_progress` or `not_started`. Once all three are complete the vendor is unlimited and `nextTier.requirements` is empty.

---

## What vendors are not

- A vendor is **not** a seller account you can authenticate as. It has no API key and no dashboard login; you operate it through your platform key.
- A vendor is **not** a payee you create yourself. Creating a payee directly (`POST /payees`) gives you a payout counterparty without screening; creating a vendor gives you a screened sub-merchant *with* a payee. Use vendors for sub-merchants whose goods or services your buyers pay you for.
