Mark Invoice as Paid API
curl --request POST \
--url https://api.pazy.io/v1.0/payment/mark-as-paidimport requests
url = "https://api.pazy.io/v1.0/payment/mark-as-paid"
response = requests.post(url)
print(response.text)const options = {method: 'POST'};
fetch('https://api.pazy.io/v1.0/payment/mark-as-paid', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.pazy.io/v1.0/payment/mark-as-paid",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.pazy.io/v1.0/payment/mark-as-paid"
req, _ := http.NewRequest("POST", url, nil)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.pazy.io/v1.0/payment/mark-as-paid")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.pazy.io/v1.0/payment/mark-as-paid")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
response = http.request(request)
puts response.read_bodyPayment APIs
Mark Invoice as Paid API
Record a payment (full or partial) against one or more approved invoices
Mark Invoice as Paid API
curl --request POST \
--url https://api.pazy.io/v1.0/payment/mark-as-paidimport requests
url = "https://api.pazy.io/v1.0/payment/mark-as-paid"
response = requests.post(url)
print(response.text)const options = {method: 'POST'};
fetch('https://api.pazy.io/v1.0/payment/mark-as-paid', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.pazy.io/v1.0/payment/mark-as-paid",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.pazy.io/v1.0/payment/mark-as-paid"
req, _ := http.NewRequest("POST", url, nil)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.pazy.io/v1.0/payment/mark-as-paid")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.pazy.io/v1.0/payment/mark-as-paid")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
response = http.request(request)
puts response.read_bodyAuthentication
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
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.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 below |
Resource Object
Each entry inresources 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, Invoice List, and 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 |
Only invoices in
APPROVED state can be marked as paid. Invoices in any other state will be rejected — see Error Responses.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 abulkActionSlug. Poll the Batch Status API with that slug to track progress.
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.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 alwaysSKIPPED. If you need the payment reflected in your accounting platform, sync it through your platform’s own existing mechanism.
Code Examples
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
}
]
}'
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();
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()
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 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 |
reason | string | Present when status is ERROR or SKIPPED — see 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 |
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), even though the payload shape is otherwise identical.Response Example — synchronous, mixed outcomes
{
"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 to track progress |
Response Example — asynchronous
{
"ok": true,
"data": {
"status": "PENDING",
"bulkActionSlug": "bulk_action_identifier"
}
}
Error Responses
Missing Idempotency Key
HTTP Status:400 Bad Request
{
"ok": false,
"error": {
"code": "IDEMPOTENCY_KEY_REQUIRED",
"message": "x-idempotency-key header is required for mark-as-paid requests"
}
}
Duplicate Idempotency Key
If the samex-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
{
"error": "Duplicate request: idempotency key already used"
}
Validation Errors
HTTP Status:400 Bad Request
{
"ok": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed: /resourceType: must be equal to one of the allowed values"
}
}
{
"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
{
"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
{
"ok": false,
"error": {
"code": "MISSING_CREDENTIALS",
"message": "Missing Credentials"
}
}
{
"ok": false,
"error": {
"code": "INVALID_API_KEY",
"message": "Invalid API Key"
}
}
Permission Errors
HTTP Status:403 Forbidden
{
"ok": false,
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Permission check failed - PERMISSION_CHECK_FAILED"
}
}
All Resources Failed or Skipped
HTTP Status:422 Unprocessable Entity
{
"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-keyper 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.resultsrather than assuming the whole batch succeeded — a200response can still contain individualERROR/SKIPPEDentries - To record a partial payment, send an
amountless 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
bulkActionSlugand poll the Batch Status API 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
idvalue returned by the Invoice List or Invoice Details APIs as theslugfor each resource — it’s a stable identifier, not a numeric database ID
Was this page helpful?