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

# Mark Invoice as Paid API

> Record a payment (full or partial) against one or more approved invoices

## Authentication

All requests require an API key in the request headers.

**Headers:**

```
Authorization: Api-Key YOUR_API_KEY
Content-Type: application/json
x-idempotency-key: YOUR_UNIQUE_REQUEST_ID
```

<Warning>
  `x-idempotency-key` is **required** on this endpoint, unlike most other write APIs where it is optional. This endpoint records a payment, so a retried request without a fresh key risks applying the same payment twice. Generate a new UUID (or similar unique value) per logical payment attempt, and reuse the same key only when retrying that exact attempt.
</Warning>

The permission required on your API key is **Payment** with the **Create** action enabled.

## Request

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

### Body Parameters

| Parameter      | Type   | Required | Description                                                                                      |
| -------------- | ------ | -------- | ------------------------------------------------------------------------------------------------ |
| `resourceType` | string | Yes      | The type of resource being marked as paid. Only `INVOICE` is currently supported                 |
| `resources`    | array  | Yes      | List of invoices to mark as paid (minimum 1 item). See [Resource Object](#resource-object) below |

### Resource Object

Each entry in `resources` represents one invoice payment:

| Parameter     | Type   | Required | Description                                                                                                                                                                                                    |
| ------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `slug`        | string | Yes      | The invoice to mark as paid — this is the same value returned as `id` by the [Invoice Creation](/apis/invoice-creation), [Invoice List](/apis/invoice-list), and [Invoice Details](/apis/invoice-details) APIs |
| `amount`      | number | Yes      | Amount being settled. Must be greater than 0, and cannot exceed the invoice's remaining due amount. Send an amount less than the total due to record a **partial payment**                                     |
| `utr`         | string | No       | UTR / bank reference number for the payment (max 64 characters)                                                                                                                                                |
| `narration`   | string | No       | Free-text note stored against the payment record (max 500 characters)                                                                                                                                          |
| `paymentDate` | string | No       | Date the payment was made, in ISO-8601 (`YYYY-MM-DD`) format. Defaults to the current date if omitted                                                                                                          |

<Info>
  Only invoices in `APPROVED` state can be marked as paid. Invoices in any other state will be rejected — see [Error Responses](#error-responses).
</Info>

### Sync vs. Async Processing

* **Batches of up to 100 invoices** are processed **synchronously** — the response contains a per-invoice outcome for every item in `resources`.
* **Batches larger than 100 invoices** are processed **asynchronously**. The response immediately returns `status: "PENDING"` with a `bulkActionSlug`. Poll the [Batch Status API](/apis/mark-as-paid-batch-status) with that slug to track progress.

<Info>
  Only one asynchronous invoice mark-as-paid batch can run per organization at a time — this applies across **both** this API and Pazy's internal web app. If a batch (started via either) is still processing, a new async batch request returns a `409 MAP_BATCH_IN_PROGRESS` error. Synchronous (≤100 item) requests are not affected by this restriction.
</Info>

### Accounting Sync Behavior

Payments recorded via this API are never automatically pushed to your connected accounting platform (Tally, Zoho Books, Oracle Fusion, etc.) — there's no way for an external caller to supply the ledger/account mapping such a sync would need. Each resulting payment record's accounting sync state is always `SKIPPED`. If you need the payment reflected in your accounting platform, sync it through your platform's own existing mechanism.

## Code Examples

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.pazy.io/v1.0/payment/mark-as-paid \
    -H "Authorization: Api-Key YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -H "x-idempotency-key: 3f29f2b0-6b8b-4e21-9b0a-9e6f1e2d4c31" \
    -d '{
      "resourceType": "INVOICE",
      "resources": [
        {
          "slug": "invoice_identifier_1",
          "amount": 15000,
          "utr": "UTR123456789",
          "narration": "Paid via NEFT",
          "paymentDate": "2024-03-15"
        },
        {
          "slug": "invoice_identifier_2",
          "amount": 5000
        }
      ]
    }'
  ```

  ```javascript JavaScript (Fetch API) theme={null}
  const response = await fetch('https://api.pazy.io/v1.0/payment/mark-as-paid', {
    method: 'POST',
    headers: {
      'Authorization': 'Api-Key YOUR_API_KEY',
      'Content-Type': 'application/json',
      'x-idempotency-key': crypto.randomUUID()
    },
    body: JSON.stringify({
      resourceType: 'INVOICE',
      resources: [
        {
          slug: 'invoice_identifier_1',
          amount: 15000,
          utr: 'UTR123456789',
          narration: 'Paid via NEFT',
          paymentDate: '2024-03-15'
        },
        {
          slug: 'invoice_identifier_2',
          amount: 5000
        }
      ]
    })
  });

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

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

  url = "https://api.pazy.io/v1.0/payment/mark-as-paid"
  headers = {
      "Authorization": "Api-Key YOUR_API_KEY",
      "Content-Type": "application/json",
      "x-idempotency-key": str(uuid.uuid4())
  }

  payload = {
      "resourceType": "INVOICE",
      "resources": [
          {
              "slug": "invoice_identifier_1",
              "amount": 15000,
              "utr": "UTR123456789",
              "narration": "Paid via NEFT",
              "paymentDate": "2024-03-15"
          },
          {
              "slug": "invoice_identifier_2",
              "amount": 5000
          }
      ]
  }

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

## Success Response

**HTTP Status:** `200 OK`

### Synchronous Response (≤ 100 invoices)

| Field          | Type    | Description                                                                                                   |
| -------------- | ------- | ------------------------------------------------------------------------------------------------------------- |
| `ok`           | boolean | Indicates whether the request was successful                                                                  |
| `data.status`  | string  | Always `"DONE"` for a synchronous batch                                                                       |
| `data.results` | array   | One entry per requested resource, in the same order as `resources`. See [Result Object](#result-object) below |

#### Result Object

| Field    | Type   | Description                                                                                        |
| -------- | ------ | -------------------------------------------------------------------------------------------------- |
| `slug`   | string | The invoice this result corresponds to — matches the `slug` (invoice `id`) you sent in the request |
| `status` | string | `DONE`, `ERROR`, or `SKIPPED` — see [Result Statuses](#result-statuses)                            |
| `reason` | string | Present when `status` is `ERROR` or `SKIPPED` — see [Result Statuses](#result-statuses)            |

#### Result Statuses

| Status    | `reason`                     | Meaning                                                                                                                                                                            |
| --------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DONE`    | —                            | Payment recorded successfully                                                                                                                                                      |
| `ERROR`   | `NOT_FOUND`                  | No invoice matches the given `slug` for this organization                                                                                                                          |
| `ERROR`   | `INVALID_STATE`              | The invoice is not in `APPROVED` state                                                                                                                                             |
| `ERROR`   | `PLATFORM_PAYMENT_IN_FLIGHT` | A payment for this invoice is already in progress through Pazy's own payment rails and hasn't settled yet                                                                          |
| `ERROR`   | `AMOUNT_EXCEEDS`             | `amount` is greater than the invoice's remaining due amount                                                                                                                        |
| `SKIPPED` | `LOCK_HELD`                  | Another mark-as-paid action is already in progress for this invoice (e.g. a concurrent request, or an action taken in the web app at the same moment). Safe to retry shortly after |

<Info>
  One resource failing does not stop the others — every entry in `resources` is attempted independently, and each gets its own result. If **every** resource in the batch ends in `ERROR` or `SKIPPED`, the response is instead sent as `422` with `ok: false` (see [Error Responses](#error-responses)), even though the payload shape is otherwise identical.
</Info>

### Response Example — synchronous, mixed outcomes

```json theme={null}
{
  "ok": true,
  "data": {
    "status": "DONE",
    "results": [
      { "slug": "invoice_identifier_1", "status": "DONE" },
      { "slug": "invoice_identifier_2", "status": "ERROR", "reason": "AMOUNT_EXCEEDS" }
    ]
  }
}
```

### Asynchronous Response (> 100 invoices)

| Field                 | Type    | Description                                                                                                     |
| --------------------- | ------- | --------------------------------------------------------------------------------------------------------------- |
| `ok`                  | boolean | Indicates whether the request was successful                                                                    |
| `data.status`         | string  | Always `"PENDING"` for an asynchronous batch                                                                    |
| `data.bulkActionSlug` | string  | Identifier for this batch. Pass it to the [Batch Status API](/apis/mark-as-paid-batch-status) to track progress |

### Response Example — asynchronous

```json theme={null}
{
  "ok": true,
  "data": {
    "status": "PENDING",
    "bulkActionSlug": "bulk_action_identifier"
  }
}
```

## Error Responses

### Missing Idempotency Key

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

```json theme={null}
{
  "ok": false,
  "error": {
    "code": "IDEMPOTENCY_KEY_REQUIRED",
    "message": "x-idempotency-key header is required for mark-as-paid requests"
  }
}
```

### Duplicate Idempotency Key

If the same `x-idempotency-key` is reused for a repeat request within 5 minutes of the original, the request is rejected before any processing happens. Note this error uses a different response shape than the rest of this API:

**HTTP Status:** `409 Conflict`

```json theme={null}
{
  "error": "Duplicate request: idempotency key already used"
}
```

### Validation Errors

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

```json theme={null}
{
  "ok": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed: /resourceType: must be equal to one of the allowed values"
  }
}
```

```json theme={null}
{
  "ok": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed: /resources: must NOT have fewer than 1 items"
  }
}
```

### Batch Already In Progress

**HTTP Status:** `409 Conflict`

```json theme={null}
{
  "ok": false,
  "error": {
    "code": "MAP_BATCH_IN_PROGRESS",
    "message": "A mark-as-paid batch is already processing for this organization"
  }
}
```

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

### All Resources Failed or Skipped

**HTTP Status:** `422 Unprocessable Entity`

```json theme={null}
{
  "ok": false,
  "message": "AMOUNT_EXCEEDS",
  "data": {
    "status": "DONE",
    "results": [
      { "slug": "invoice_identifier_1", "status": "ERROR", "reason": "AMOUNT_EXCEEDS" }
    ]
  }
}
```

## Best Practices

* Always send a unique `x-idempotency-key` per payment attempt — reuse it only when retrying the exact same attempt (e.g. after a timeout), never for a genuinely new payment
* Check every entry in `data.results` rather than assuming the whole batch succeeded — a `200` response can still contain individual `ERROR`/`SKIPPED` entries
* To record a partial payment, send an `amount` less than the invoice's remaining due — you can call this API again later with the remaining balance
* For batches over 100 invoices, store the returned `bulkActionSlug` and poll the [Batch Status API](/apis/mark-as-paid-batch-status) rather than assuming completion
* Since only one async batch runs per organization at a time (shared with the web app), avoid submitting large batches back-to-back — wait for the previous one to finish
* Use the `id` value returned by the [Invoice List](/apis/invoice-list) or [Invoice Details](/apis/invoice-details) APIs as the `slug` for each resource — it's a stable identifier, not a numeric database ID
