Integrate COD reconciliation
into your ops stack.
A single POST endpoint reconciles any Shopify export against any courier remittance file. It returns structured flags, shortfall amounts, and overdue timelines your team can act on immediately.
Sandbox key
Try the API without subscribing. Free, no credit card
/reconcile endpoint
Full request shape, params, response schema
Flag reference
What MISSING, SHORT_PAID, OVERDUE, RTO mean
Try it before you subscribe.
Your sandbox key works against a read-only dataset of 500 real-but-anonymised COD orders. Every flag type is represented so you can test your integration end-to-end before committing to a plan.
Evaluating for your agency? The sandbox key passes any CSV. You can test with a sample Shopify export and a sample Delhivery remittance file. The response will be structured identically to production. Use X-Sandbox: true to confirm you're hitting the sandbox endpoint.
Bearer token, nothing else.
Every request must include your API key as a Bearer token in the Authorization header. Live keys are generated post-subscription from the Agency → API panel.
Authorization: Bearer pt_live_your_key_here Content-Type: application/json
Keep your live key secret. It has full access to all brands under your agency account. Never expose it in client-side code or public repos. Rotate it from the API panel if compromised. The old key is instantly invalidated.
Reconcile in one call.
Pass your Shopify orders export and the courier remittance file as CSV strings. The API parses, matches, and returns a structured array of every order with its reconciliation status.
curl -X POST https://api.paytrace.in/reconcile \ -H "Authorization: Bearer pt_sandbox_demo_k3y_8f2a1c9d4e7b0f5a" \ -H "Content-Type: application/json" \ -d '{ "courier": "delhivery", "shopify_csv": "Name,Email,Financial Status,Paid at,Fulfillment Status,...\n#1001,...", "courier_csv": "AWB No,Order ID,COD Amount,Remitted Amount,Remittance Date,...\nDEL123,...", "brand_id": "brand_nykaa_01" }'
Example response
{
"ok": true,
"job_id": "recon_a7f2c1d9_1712345678",
"meta": {
"courier": "delhivery",
"brand_id": "brand_nykaa_01",
"shopify_rows": 312,
"courier_rows": 298,
"matched": 289,
"unmatched": 9,
"processed_at": "2025-04-06T10:32:11Z"
},
"summary": {
"total_cod_expected": 2845, // in rupees (₹2,845.00)
"total_remitted": 2612,
"total_shortfall": 233,
"flags": {
"MISSING": 4,
"SHORT_PAID": 7,
"OVERDUE": 3,
"RTO_ANOMALY": 1,
"MINOR_DIFF": 2,
"RECONCILED": 272
}
},
"results": [
{
"awb": "DEL1234567890",
"order_id": "#1082",
"cod_amount": 899, // rupees (₹899.00)
"remitted": 0,
"shortfall": 899,
"days_overdue": 18,
"flag": "MISSING",
"status": "fulfilled"
},
{
"awb": "DEL9876543210",
"order_id": "#1097",
"cod_amount": 1499,
"remitted": 999,
"shortfall": 500,
"days_overdue": 0,
"flag": "SHORT_PAID",
"status": "fulfilled"
},
{
"awb": "DEL5551234567",
"order_id": "#1043",
"cod_amount": 599,
"remitted": 599,
"shortfall": 0,
"days_overdue": 0,
"flag": "RECONCILED",
"status": "fulfilled"
}
// ... remaining results
]
}
POST /reconcile
The primary endpoint. Accepts a Shopify CSV and a courier remittance CSV. Reconciles them order-by-order and returns every flag with shortfall and timeline data.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| shopify_csv | string | required | Full text content of the Shopify orders export CSV. UTF-8 encoded. |
| courier_csv | string | required | Full text content of the courier remittance CSV. Supported couriers: Delhivery, Shiprocket, XpressBees, Ecom Express. |
| courier | string | required | Courier identifier. One of: delhivery shiprocket xpressbees ecom_express |
| brand_id | string | optional | Your internal brand identifier. Returned as-is in the response and used to associate results with a brand in the agency dashboard. |
| overdue_threshold_days | integer | optional | Days after delivery before an unremitted COD is flagged OVERDUE. Defaults to 7. Range: 1–30. |
| minor_gap_threshold | integer | optional | Shortfall in rupees below which a discrepancy is flagged MINOR_DIFF instead of SHORT_PAID. Defaults to 2 (₹2). Range: 0–10. |
| webhook_url | string (url) | optional | POST callback URL when processing completes. Useful for large files processed asynchronously. See Webhooks → |
Response fields
| Field | Type | Description |
|---|---|---|
| ok | boolean | Always present. true on success, false on error. |
| job_id | string | Unique run identifier. Use with GET /status/{job_id} for async polling. |
| meta.shopify_rows | integer | Number of rows parsed from the Shopify CSV. |
| meta.courier_rows | integer | Number of rows parsed from the courier CSV. |
| meta.matched | integer | Orders successfully matched between both files by AWB or order ID. |
| summary.total_cod_expected | integer | Total COD amount expected across all orders, in rupees (₹). |
| summary.total_shortfall | integer | Total unrecovered amount across all flagged orders, in rupees (₹). |
| summary.flags | object | Count of each flag type in the results. See Flag reference → |
| results[].awb | string | Courier AWB / tracking number. |
| results[].order_id | string | Shopify order name (e.g. #1082). |
| results[].cod_amount | integer | COD amount collected on delivery, in rupees (₹). |
| results[].remitted_amount | integer | Amount the courier has remitted so far, in rupees (₹). 0 if not yet remitted. |
| results[].shortfall | integer | cod_amount − remitted_amount, in rupees (₹). 0 if fully reconciled. |
| results[].days_overdue | integer | Days past the expected remittance window. 0 if within window or reconciled. |
| results[].flag | string | Reconciliation status. One of: RECONCILED MISSING SHORT_PAID OVERDUE RTO_ANOMALY MINOR_DIFF |
POST /reconcile/batch
Process multiple brands in a single call. Returns a batch_id immediately; results are delivered via webhook or polled with GET /status/{batch_id}. Ideal for agencies running weekly reconciliations across many brands.
| Field | Type | Required | Description |
|---|---|---|---|
| jobs | array | required | Array of reconciliation jobs. Each job has the same fields as POST /reconcile plus a required brand_id. Max 20 per batch. |
| webhook_url | string (url) | optional | Called once all jobs complete. Payload contains the full batch results array. |
{
"ok": true,
"batch_id": "batch_b2f1a9c3_1712345678",
"jobs": 5,
"status": "queued",
"estimated_completion_ms": 12000
}
GET /status/{job_id}
Poll the status of an async reconciliation or batch job. Returns status queued, processing, or complete. The full results payload is included once done.
Polling interval: We recommend polling every 2 seconds. Most jobs complete in under 3s. For large files (10,000+ rows) allow up to 15s. Results are cached for 24 hours after completion.
Flag types: what each means.
Every order in the results array carries exactly one flag. Here's what each flag means and what action it implies for your clients.
Error codes.
All errors return a consistent JSON envelope with a machine-readable code, a human-readable message and a hint to help resolve the issue fast.
Rate limits.
Rate limits are per API key, counted on a rolling 60-second window. Headers on every response show your current usage.
X-RateLimit-Limit: 120 X-RateLimit-Remaining: 87 X-RateLimit-Reset: 1712345738 // Unix timestamp Retry-After: 14 // seconds (only present on 429)
Supported couriers.
PayTrace auto-detects column names from common export formats. These are the accepted values for the courier parameter and the columns we look for in each report.
Courier not listed? Email help@paytrace.in with a sample remittance CSV. We typically add support within 48 hours. You can also pass "courier": "custom" with explicit column overrides. See custom courier docs →
Agency integration pattern.
Most agencies integrate PayTrace into their weekly ops cadence. They fetch Shopify exports per brand, pull courier remittances, run the batch endpoint and surface flags in a Notion or Google Sheet for client review.
import requests, csv, time PAYTRACE_KEY = "pt_live_your_key_here" BASE_URL = "https://api.paytrace.in" def run_weekly_reconciliation(brands): # brands = [{ id, shopify_csv, courier_csv, courier }, ...] # 1. Fire batch job resp = requests.post( f"{BASE_URL}/reconcile/batch", headers={"Authorization": f"Bearer {PAYTRACE_KEY}"}, json={ "jobs": [ { "brand_id": b["id"], "courier": b["courier"], "shopify_csv": b["shopify_csv"], "courier_csv": b["courier_csv"], } for b in brands ], "webhook_url": "https://your-agency.com/hooks/paytrace" } ) batch_id = resp.json()["batch_id"] # 2. Poll until done (or wait for webhook) while True: status_resp = requests.get( f"{BASE_URL}/status/{batch_id}", headers={"Authorization": f"Bearer {PAYTRACE_KEY}"} ).json() if status_resp["status"] == "complete": break time.sleep(2) # 3. Collect and triage results per brand results = status_resp["results"] actionable = [ r for r in results if r["flag"] in ("MISSING", "SHORT_PAID", "RTO_ANOMALY") ] return actionable
Webhooks.
Pass a webhook_url in any reconciliation call and we'll POST the full results to your endpoint when processing completes. No polling needed.
{
"event": "reconciliation.complete",
"job_id": "recon_a7f2c1d9_1712345678",
"brand_id": "brand_nykaa_01",
"summary": { /* same as /reconcile response summary */ },
"results": [ /* same as /reconcile response results */ ],
"signature": "sha256=abc123..." // HMAC-SHA256 of payload body
}
Verify webhook signatures. Compare the signature header against an HMAC-SHA256 of the raw request body using your API key as the secret. Reject any webhook that fails verification. We retry failed deliveries 3 times over 30 minutes.
White-label reports.
Agency plan accounts can configure a white-label profile. Your agency name and email appear on all exported Excel reports and dispute letters instead of "PayTrace". Set it once via the dashboard or via API.
Agency plan feature. White-label branding is available on the Agency plan (₹3,999/month). Configure it from Agency → White-label Settings in the dashboard, or by passing white_label: { agency_name, agency_email } in any reconcile call to override per-brand.