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.
The GET /demo endpoint runs a full reconciliation against a real sample dataset — no account, no API key, no credit card required. It uses an anonymised Shopify export and Delhivery remittance file covering Feb–Mar 2026, with every flag type represented.
Evaluating for your agency? Call GET /demo to see exactly what a reconciliation response looks like before writing any integration code. The dataset includes MISSING, SHORT_PAID, OVERDUE, RTO_ANOMALY, MINOR_DIFF, OVERPAID, and RECONCILED rows so you can build and test your downstream flag-handling logic end-to-end.
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_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "shopify": "Name,Email,Financial Status,Paid at,Fulfillment Status,...\n#1001,...", "courier": "AWB No,Order ID,COD Amount,Remitted Amount,Remittance Date,...\nDEL123,...", "courierName": "delhivery", "brand": "brand_nykaa_01" }'
Example response
{
"results": [
{
"awb": "DEL1234567890",
"order_id": "#1082",
"cod_amount": 899,
"remitted": 0,
"shortfall": 899,
"days_overdue": 18,
"flag": "MISSING",
"flag_reason": "Not remitted",
"status": "fulfilled",
"match_mode": "awb"
},
{
"awb": "DEL9876543210",
"order_id": "#1097",
"cod_amount": 1499,
"remitted": 999,
"shortfall": 500,
"days_overdue": 0,
"flag": "SHORT_PAID",
"flag_reason": "Shortfall detected",
"status": "fulfilled",
"match_mode": "awb"
},
{
"awb": "DEL5551234567",
"order_id": "#1043",
"cod_amount": 599,
"remitted": 599,
"shortfall": 0,
"days_overdue": 0,
"flag": "RECONCILED",
"flag_reason": "Fully remitted",
"status": "fulfilled",
"match_mode": "awb"
}
// ... 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 | string | required | Full text content of the Shopify orders export CSV. UTF-8 encoded. Maximum 50,000 rows. |
| courier | string | required | Full text content of the courier remittance CSV. Supported couriers: Delhivery, Shiprocket, XpressBees, Shadowfax, Ekart, Ecom Express. Maximum 50,000 rows. |
| courierName | string | optional | Courier identifier string (e.g. delhivery, shiprocket). Always stored in audit history so you can filter and group runs by courier. On Enterprise plans, also used to save and re-use detected column mappings across runs — skipping re-detection on subsequent calls for the same courier. Has no column-mapping effect on Agency plan. |
| brand | string | optional | Your brand label for this run (e.g. "brand_nykaa_01" or "Nykaa Fashion"). Stored in audit history so you can filter runs by brand. Defaults to "default" if omitted. |
| 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. |
Response fields
| Field | Type | Description |
|---|---|---|
| results | array | Array of reconciliation result objects, one per order row. Always present on a 200 response. |
| duplicate | boolean | Present and true when an identical run was detected within the last 30 seconds. Match is on: same user + brand + courierName + identical row and flag counts. Results are still returned and the run is not counted against your monthly limit. |
| run_id | string | UUID identifying this reconciliation run. Included when a duplicate is detected. Use for support queries. |
| results[].awb | string | Courier AWB / tracking number. |
| results[].order_id | string | Shopify order name (e.g. #1082). |
| results[].cod_amount | number | COD amount collected on delivery, in rupees (₹). |
| results[].remitted | number | Amount the courier has remitted so far, in rupees (₹). 0 if not yet remitted. |
| results[].shortfall | number | Unrecovered amount in rupees (₹). Positive = courier owes money. Negative = courier overpaid. 0 if fully reconciled. |
| results[].days_overdue | integer | Days past the expected remittance window. Non-zero only on OVERDUE and MISSING rows. |
| results[].flag | string | Reconciliation status. One of: RECONCILED MISSING SHORT_PAID OVERDUE RTO_ANOMALY MINOR_DIFF OVERPAID |
| results[].flag_reason | string | Human-readable explanation for the flag (e.g. "Shortfall detected", "Not remitted"). Useful for display in client-facing dashboards without needing to map flag codes yourself. |
| results[].status | string | Shopify fulfilment status for this order (e.g. fulfilled, unfulfilled). Useful for filtering RTO and cancelled orders. |
| results[].match_mode | string | How this row was matched between the two files. awb = matched on tracking number (preferred). order_id = matched on Shopify order ID (fallback when AWB not present in Shopify export). |
POST /generate-api-key
Generates a new live API key for your agency account. Requires an active Agency plan subscription. Any previously issued key for your account is immediately deactivated — there is always only one active key per account.
This endpoint must be called with a Supabase session token, not a pt_live_* API key. Use your dashboard session from the browser. The returned key is shown exactly once — store it securely. Calling this endpoint rotates your key immediately.
Request body
No request body required. The authenticated user's email is used to identify the account.
Response
{
"key": "pt_live_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", // full key — shown once, store immediately
"prefix": "pt_live_a1b2c" // safe prefix for display / identification
}
All pt_live_* keys are passed as a Bearer token in subsequent API calls. Only the SHA-256 hash of the key is stored — PayTrace cannot recover a lost key. If you lose it, call this endpoint again to rotate.
POST /reconcile/batch
Process multiple brands in a single call. Results will be delivered via webhook or polled with GET /status/{batch_id}. Ideal for agencies running weekly reconciliations across many brands.
This endpoint is not yet available. Batch processing is on the roadmap for Agency plan users. In the meantime, call POST /reconcile once per brand in sequence. Email help@paytrace.in to register your interest and get notified on launch.
| Field | Type | Required | Description |
|---|---|---|---|
| jobs | array | required | Array of reconciliation jobs. Each job has the same fields as POST /reconcile plus a required brand. 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 batch job. Will return queued, processing, or complete, with the full results payload once done. Available when POST /reconcile/batch launches.
Not yet available. This endpoint will be released alongside POST /reconcile/batch. The current POST /reconcile endpoint is synchronous and returns results directly in the response body.
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 JSON object with a single error field containing a human-readable message string.
{ "error": "Plain English description of what went wrong" }
Rate limits.
Rate limits are per API key, enforced on a rolling 60-second window. Exceeding the limit returns a 429 and blocks the key for 5 minutes.
No rate-limit headers are sent. The API does not return X-RateLimit-* headers. If you receive a 429, respect the Retry-After header value (seconds) before retrying. For scripted multi-brand workflows, add a 0.5s pause between calls to stay well within limits.
GET /demo limits
GET /demo is rate-limited per IP: 20 requests/minute, with a 60-second block on breach. No auth required. Returns Retry-After on a 429.
HTTP/1.1 429 Too Many Requests Retry-After: 300 // seconds blocked (5 minutes on breach) { "error": "Rate limit exceeded" }
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 — fetching Shopify exports per brand, pulling courier remittances, running reconciliations sequentially and surfacing flags in a Notion or Google Sheet for client review.
import requests, time PAYTRACE_KEY = "pt_live_your_key_here" BASE_URL = "https://api.paytrace.in" def run_weekly_reconciliation(brands): # brands = [{ brand, shopify_csv, courier_csv, courierName }, ...] all_flagged = [] for b in brands: resp = requests.post( f"{BASE_URL}/reconcile", headers={"Authorization": f"Bearer {PAYTRACE_KEY}"}, json={ "shopify": b["shopify_csv"], "courier": b["courier_csv"], "courierName": b["courierName"], "brand": b["brand"], } ) resp.raise_for_status() results = resp.json()["results"] # Collect actionable flags for this brand flagged = [ {**r, "brand": b["brand"]} for r in results if r["flag"] in ("MISSING", "SHORT_PAID", "RTO_ANOMALY", "OVERDUE") ] all_flagged.extend(flagged) # Be a good API citizen — small pause between brands time.sleep(0.5) return all_flagged
Webhooks.
Outbound webhooks — where PayTrace POSTs results to your endpoint on job completion — are planned for release alongside the batch endpoint.
Not yet available for API users. The current POST /reconcile endpoint is synchronous — results come back in the response body within 800ms–2s, so no polling or webhook is needed for single-brand runs. Email help@paytrace.in to register interest.
When webhooks ship, the payload schema will look like this:
{
"event": "reconciliation.complete",
"job_id": "recon_a7f2c1d9_1712345678",
"brand": "brand_nykaa_01",
"results": [ /* same as /reconcile response results array */ ],
"signature": "sha256=abc123..." // HMAC-SHA256 of payload body using your API key
}
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.