> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pazy.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Vendor Update API

> Update one or more fields of an existing vendor — name, contact, owner, address, payment routing, and custom (flex) fields

## Authentication

All requests require an API key in the request headers.

**Headers:**

```
Authorization: Api-Key YOUR_API_KEY
Content-Type: application/json
```

## Request

**Content-Type:** `application/json`

### Path Parameters

| Parameter  | Type   | Required | Constraints      | Description                                      |
| ---------- | ------ | -------- | ---------------- | ------------------------------------------------ |
| `vendorId` | string | Yes      | 1-100 characters | Slug / unique identifier of the vendor to update |

### Body Parameters

The request body has a single top-level key, `vendorData`. All fields inside `vendorData` are optional — only the fields you include will be considered for update. Fields you omit are left untouched.

Two schema rules apply to the whole payload:

* **At least one field is required.** `vendorData` must contain a minimum of one property; `{"vendorData": {}}` is rejected with `400 VALIDATION_ERROR`.
* **Unknown keys are rejected.** `vendorData`, `address`, `communications`, and each `customFields` entry all reject properties that are not listed below with `400 VALIDATION_ERROR` — they are not silently ignored.

| Parameter                   | Type   | Required | Constraints               | Description                                                                                                                                                                            |
| --------------------------- | ------ | -------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `vendorData`                | object | Yes      | At least 1 property       | Container for the partial update payload                                                                                                                                               |
| `vendorData.name`           | string | No       | 1-255 characters          | Vendor display name. Also renames the underlying vendor user record (the last word becomes the last name, everything before it the first name).                                        |
| `vendorData.email`          | string | No       | Valid `email` format      | Vendor email address. Replaces the vendor's active email identifier. If the email's domain changes, the vendor logo is re-fetched from the new domain.                                 |
| `vendorData.phone`          | string | No       | `+{country_code}{number}` | Vendor phone number, e.g. `+919876543210`. Must include the `+` and country code — a value without it is rejected per-field with `INVALID_NUMBER`.                                     |
| `vendorData.owner`          | string | No       | 1-100 characters          | New owner — the user id / slug of a user in your organization. If the new owner was a watcher on this vendor, that watcher entry is removed (a user cannot be both owner and watcher). |
| `vendorData.address`        | object | No       | Known keys only           | Vendor address. Merged with the existing address. See [Address Fields](#address-fields) below.                                                                                         |
| `vendorData.communications` | object | No       | Known keys only           | Payment confirmation email recipients. Merged per list. See [Communications Fields](#communications-fields) below.                                                                     |
| `vendorData.customFields`   | object | No       | Slug-keyed map            | Flex fields to set, keyed by field slug. See [Custom Fields](#custom-fields) below.                                                                                                    |

#### Address Fields

`vendorData.address` accepts the following properties. All are optional and **merged with the existing address** — any key you omit (or send empty) keeps its current value, so you only need to send the parts that change. The one exception is `country`, which falls back to `India` rather than the existing value when the vendor is `DOMESTIC`.

| Field          | Type   | Description                                                                                                                                                       |
| -------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `addressLine1` | string | Address line 1. Stored encrypted.                                                                                                                                 |
| `addressLine2` | string | Address line 2. Stored encrypted.                                                                                                                                 |
| `city`         | string | City. Validated against known city names — an unknown value rejects the whole address with `INVALID_ADDRESS`.                                                     |
| `state`        | string | State code. Resolved against the state list for the given `locality`; an unresolvable code rejects the whole address with `INVALID_ADDRESS`.                      |
| `pinCode`      | string | Postal / PIN code. For `DOMESTIC` this is validated as an Indian PIN code; for `INTERNATIONAL` it is accepted as-is.                                              |
| `zipCode`      | string | Accepted by the schema for convenience, but **not persisted** — send `pinCode` to actually change the postal code.                                                |
| `country`      | string | Country name. Validated against known country names. When omitted, `DOMESTIC` vendors default to `India` and `INTERNATIONAL` vendors keep their existing country. |
| `locality`     | string | `DOMESTIC` or `INTERNATIONAL`. Selects the validation rules used for `state`, `pinCode`, and the `country` default.                                               |

<Note>
  Address validation is all-or-nothing: the first invalid field rejects the entire `address` update with a single `INVALID_ADDRESS` error whose message names the offending field (for example, `Given address is not valid. Invalid state code`). No partial address is written.
</Note>

#### Communications Fields

`vendorData.communications` configures who receives payment confirmation emails. Each list contains user ids / slugs from your organization.

| Field | Type             | Description                         |
| ----- | ---------------- | ----------------------------------- |
| `to`  | array of strings | "To" recipients (user ids / slugs)  |
| `cc`  | array of strings | "CC" recipients (user ids / slugs)  |
| `bcc` | array of strings | "BCC" recipients (user ids / slugs) |

Behavior to be aware of:

* **Each list is replaced, not appended to.** Sending `to: ["user_a"]` makes `user_a` the only "To" recipient.
* **Merged per list.** Lists you omit — or send as an empty array — keep their current recipients. There is no way to clear a list through this endpoint.
* **Duplicates are removed** within each list.
* **All ids are validated together.** If any id across `to`, `cc`, and `bcc` is not a user in your organization, the whole `communications` update is rejected with `INVALID_USER_IN_COMMUNICATIONS` and the offending ids are returned in `wrongUserSlugs`.

#### Custom Fields

`vendorData.customFields` is a map keyed by the **flex-field slug** configured for your organization. Each value is an object requiring both `type` and `value`:

| Field   | Type             | Required | Description                                                                                                  |
| ------- | ---------------- | -------- | ------------------------------------------------------------------------------------------------------------ |
| `type`  | string           | Yes      | Flex-field type. Must match the field's configured type. One of `VARCHAR`, `DATETIME`, `DROPDOWN`, `NUMERIC` |
| `value` | string \| number | Yes      | Value to set — see the per-type table below                                                                  |

Expected `value` per `type`:

| `type`     | Expected `value`                                                                                 | Rejected with           |
| ---------- | ------------------------------------------------------------------------------------------------ | ----------------------- |
| `VARCHAR`  | Any text                                                                                         | —                       |
| `DATETIME` | Date string in `YYYY-MM-DD`                                                                      | `INVALID_DATE_INPUT`    |
| `NUMERIC`  | A number (numeric strings are accepted and coerced)                                              | `INVALID_NUMERIC_INPUT` |
| `DROPDOWN` | The **id of the tag value** to select, not its label — must be an integer belonging to that slug | `INVALID_VALUE_ID`      |

Each slug is validated and applied independently, so one bad entry does not block the others — check `context.updates.customFields.<slug>` for each. Notes:

* The `expense-head` slug cannot be updated through this endpoint and always returns `TAG_NOT_ALLOWED`.
* Use the Tags APIs to look up valid slugs and, for `DROPDOWN`, the tag value ids to send.

### Full Request Body Example

Every supported field in one payload — in practice, send only the ones you want to change:

```json theme={null}
{
  "vendorData": {
    "name": "Acme Suppliers",
    "email": "accounts@acmesuppliers.com",
    "phone": "+919876543210",
    "owner": "<user-slug>",
    "address": {
      "addressLine1": "24, Industrial Estate",
      "addressLine2": "Off Hosur Road",
      "city": "Bengaluru",
      "state": "KA",
      "pinCode": "560068",
      "country": "India",
      "locality": "DOMESTIC"
    },
    "communications": {
      "to": ["<user-slug>"],
      "cc": ["<user-slug>", "<user-slug>"],
      "bcc": []
    },
    "customFields": {
      "vendor-category": { "type": "DROPDOWN", "value": 4821 },
      "internal-code": { "type": "VARCHAR", "value": "ACME-2024" },
      "contract-expiry": { "type": "DATETIME", "value": "2027-03-31" },
      "credit-days": { "type": "NUMERIC", "value": 45 }
    }
  }
}
```

## Code Examples

<CodeGroup>
  ```bash cURL theme={null}
  curl -X PATCH https://api.pazy.io/v1.0/vendor/<vendorId> \
    -H "Authorization: Api-Key YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "vendorData": {
        "name": "Acme Suppliers",
        "email": "vendor@example.com",
        "phone": "+919876543210"
      }
    }'
  ```

  ```javascript JavaScript (Fetch API) theme={null}
  const response = await fetch('https://api.pazy.io/v1.0/vendor/<vendorId>', {
    method: 'PATCH',
    headers: {
      'Authorization': 'Api-Key YOUR_API_KEY',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      vendorData: {
        name: 'Acme Suppliers',
        email: 'vendor@example.com',
        phone: '+919876543210'
      }
    })
  });

  const result = await response.json();
  ```

  ```python Python (requests) theme={null}
  import requests

  url = "https://api.pazy.io/v1.0/vendor/<vendorId>"
  headers = {
      "Authorization": "Api-Key YOUR_API_KEY",
      "Content-Type": "application/json"
  }
  payload = {
      "vendorData": {
          "name": "Acme Suppliers",
          "email": "vendor@example.com",
          "phone": "+919876543210"
      }
  }

  response = requests.patch(url, headers=headers, json=payload)
  result = response.json()
  ```
</CodeGroup>

## Success Response

**HTTP Status:** `200 OK`

The endpoint always returns `200 OK` when the request itself is valid — even when individual fields fail to update. Check `ok` first, then inspect `context.updates.<field>` to see the per-field outcome. A `200` with `ok: false` means the write itself failed and nothing was applied — see [Update Failed](#update-failed).

**Response Fields:**

| Field                            | Type           | Description                                                                                   |
| -------------------------------- | -------------- | --------------------------------------------------------------------------------------------- |
| `ok`                             | boolean        | Indicates whether the request was processed                                                   |
| `update`                         | boolean        | Indicates that the update operation ran                                                       |
| `error`                          | object \| null | Top-level error if the entire request failed (`null` when per-field results are returned)     |
| `context`                        | object         | Container for per-field results                                                               |
| `context.updates`                | object         | Map keyed by the field name you sent in `vendorData`                                          |
| `context.updates.<field>.update` | boolean        | `true` if that field was updated; `false` if it was rejected or skipped                       |
| `context.updates.<field>.error`  | object         | Present only when `update` is `false`. Contains a `code` and `message` describing the reason. |

Some fields attach extra metadata when they fail:

| Field            | Failure code                     | Extra fields returned                                |
| ---------------- | -------------------------------- | ---------------------------------------------------- |
| `name`           | `DUPLICATE_VENDOR_NAME`          | `isDuplicate: true`, `duplicateVendor: { id, name }` |
| `communications` | `INVALID_USER_IN_COMMUNICATIONS` | `wrongUserSlugs: [...]`                              |

### Response Example — Successful Multi-Field Update

```json theme={null}
{
  "ok": true,
  "update": true,
  "error": null,
  "context": {
    "updates": {
      "name": { "update": true },
      "email": { "update": true },
      "phone": { "update": true }
    }
  }
}
```

### Response Example — Mixed Success and Failures

```json theme={null}
{
  "ok": true,
  "update": true,
  "error": null,
  "context": {
    "updates": {
      "name": {
        "update": false,
        "error": {
          "code": "DUPLICATE_VENDOR_NAME",
          "message": "Vendor with the same name already exist"
        },
        "isDuplicate": true,
        "duplicateVendor": {
          "id": "<otherVendorId>",
          "name": "Acme Trading"
        }
      },
      "phone": {
        "update": false,
        "error": {
          "code": "SAME_VALUE_GIVEN",
          "message": "Phone is same as the current one"
        }
      },
      "owner": { "update": true }
    }
  }
}
```

### Response Example — `communications` with Unknown Users

```json theme={null}
{
  "ok": true,
  "update": true,
  "error": null,
  "context": {
    "updates": {
      "communications": {
        "update": false,
        "error": {
          "code": "INVALID_USER_IN_COMMUNICATIONS",
          "message": "Users in communications do not exist in the organization"
        },
        "wrongUserSlugs": ["<unknownUserId>"]
      }
    }
  }
}
```

### Response Example — `customFields` Mixed Outcome

```json theme={null}
{
  "ok": true,
  "update": true,
  "error": null,
  "context": {
    "updates": {
      "customFields": {
        "fasttrack": { "update": true },
        "test-upload": { "update": true },
        "wrong-tag": {
          "update": false,
          "error": {
            "code": "INVALID_TAG",
            "message": "Invalid tag key"
          }
        },
        "hinge": {
          "update": false,
          "error": {
            "code": "INVALID_TAG_TYPE",
            "message": "Tag type is of 'DATETIME' but received VARCHAR"
          }
        }
      }
    }
  }
}
```

## Per-Field Error Codes

| Field                                                                                 | Error code                       | When it occurs                                                                                                                                                                                    |
| ------------------------------------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`                                                                                | `DUPLICATE_VENDOR_NAME`          | Another vendor in the same organization already uses this name. Response includes `duplicateVendor.id` and `duplicateVendor.name`. Suppressed if your organization allows duplicate vendor names. |
| `name`, `email`, `phone`, `owner`, `address`, `communications`, `customFields.<slug>` | `SAME_VALUE_GIVEN`               | The submitted value matches the existing value — nothing changed                                                                                                                                  |
| `phone`                                                                               | `INVALID_NUMBER`                 | Phone number does not follow `+{country_code}{number}`                                                                                                                                            |
| `owner`                                                                               | `INVALID_OWNER`                  | Owner user id does not exist in your organization                                                                                                                                                 |
| `address`                                                                             | `INVALID_ADDRESS`                | A field in the address failed validation. The message names the reason: `Invalid state code`, `Invalid city name`, `Invalid pin-code`, or `Invalid country name`.                                 |
| `communications`                                                                      | `INVALID_USER_IN_COMMUNICATIONS` | One or more of the `to` / `cc` / `bcc` slugs is not a valid user. Invalid ids are returned in `wrongUserSlugs`.                                                                                   |
| `customFields.<slug>`                                                                 | `INVALID_TAG`                    | The slug does not match any flex field configured for the organization                                                                                                                            |
| `customFields.<slug>`                                                                 | `TAG_NOT_ALLOWED`                | The slug is `expense-head`, which cannot be updated through this endpoint                                                                                                                         |
| `customFields.<slug>`                                                                 | `INVALID_VALUE_ID`               | For `DROPDOWN`: the value id is unknown, not an integer, or not linked to that tag                                                                                                                |
| `customFields.<slug>`                                                                 | `INVALID_TAG_TYPE`               | The `type` sent does not match the configured flex-field type                                                                                                                                     |
| `customFields.<slug>`                                                                 | `INVALID_DATE_INPUT`             | For `DATETIME`: value is not in `YYYY-MM-DD`                                                                                                                                                      |
| `customFields.<slug>`                                                                 | `INVALID_NUMERIC_INPUT`          | For `NUMERIC`: value is not a finite number                                                                                                                                                       |

## Error Responses

### Validation Error

**HTTP Status:** `400 Bad Request`

```json theme={null}
{
  "ok": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed: /vendorData/email: must match format \"email\""
  }
}
```

Returned before any field is processed when the body does not match the schema — a bad `email` format, a `name` outside 1-255 characters, an empty `vendorData`, an unknown property, a `customFields` entry missing `type` or `value`, or a `locality` other than `DOMESTIC` / `INTERNATIONAL`.

### Update Failed

**HTTP Status:** `200 OK`

```json theme={null}
{
  "ok": false,
  "update": false,
  "error": {
    "errorCode": "UPDATE_FAILED"
  }
}
```

Returned when writing the changes fails. The whole update is rolled back as a single transaction — **no field is applied**, including ones that validated successfully. Note that this envelope carries `errorCode` (not `code`) and has no `context`, so check `ok` before reading `context.updates`. Safe to retry.

### Vendor Not Found

**HTTP Status:** `404 Not Found`

```json theme={null}
{
  "ok": false,
  "error": {
    "code": "VENDOR_NOT_FOUND",
    "message": "Vendor not found"
  }
}
```

### Access Denied

**HTTP Status:** `403 Forbidden`

```json theme={null}
{
  "ok": false,
  "error": {
    "code": "ACCESS_DENIED",
    "message": "Access denied: You can only view vendors you own"
  }
}
```

Returned when the caller is neither an admin / bookkeeper nor the vendor's owner.

### Authentication Errors

**HTTP Status:** `401 Unauthorized`

```json theme={null}
{
  "ok": false,
  "error": {
    "code": "MISSING_CREDENTIALS",
    "message": "Missing Credentials"
  }
}
```

```json theme={null}
{
  "ok": false,
  "error": {
    "code": "INVALID_API_KEY",
    "message": "Invalid API Key"
  }
}
```

### Permission Errors

**HTTP Status:** `403 Forbidden`

```json theme={null}
{
  "ok": false,
  "error": {
    "code": "INSUFFICIENT_PERMISSIONS",
    "message": "Permission check failed - PERMISSION_CHECK_FAILED"
  }
}
```

### Internal Error

**HTTP Status:** `500 Internal Server Error`

```json theme={null}
{
  "ok": false,
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "Internal error"
  }
}
```

## Best Practices

* Send only the fields you want to change — omitted fields are left untouched
* Always inspect `context.updates.<field>` for the outcome; a `200 OK` does **not** guarantee every field was applied
* For `phone`, normalize to E.164 (`+{country_code}{number}`) before sending — values without `+` are rejected with `INVALID_NUMBER`
* For `address`, the API merges with the existing record. Send only the keys you want to change rather than the full address — and use `pinCode`, not `zipCode`, to change the postal code
* For `communications`, each list you send **replaces** that list. Fetch the current recipients first and send the full intended list, rather than just the ids you are adding
* For `customFields`, the `type` you send must exactly match the field's configured type — a mismatch is rejected per-field with `INVALID_TAG_TYPE`
* For `DROPDOWN` custom fields, send the tag **value id**, not the displayed label — a label is rejected with `INVALID_VALUE_ID`
* Use the [Vendor Details API](/apis/vendor-details) to fetch the current state, and the [Vendor Audit Trail API](/apis/vendor-audit-trail) to inspect change history after updates
