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 Shiprocket 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 Pro → 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 Pro 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. |
| cycle_guidance | object | Metadata about the remittance-timing calculation, not a per-row result. offset_days (integer): the number of days used to decide when an unmatched order counts as overdue; offset_source ("explicit" or "default"): whether that came from your own Settings override or the platform default; batch_as_of (ISO date): the most recent order date in this batch, the reference point every "days overdue" figure is measured against; courier_coverage_complete (boolean): true once your courier file's own dates prove every order's full remittance window was actually checked (see the next field), making any remaining AWAITING_NEXT_CYCLE rows resolve directly to MISSING/OVERDUE instead of waiting on a day-count guess; courier_data_through (ISO date or null): the latest date actually present in your courier file; guaranteed_coverage_days (integer): how far past batch_as_of your courier file needs to reach for coverage to count as complete (14, or your own overdue_threshold_days if you've set it higher); awaiting_next_cycle_count (integer): how many rows are still waiting on more remittance data. |
| 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 AWAITING_NEXT_CYCLE. Treat unrecognized values as non-final/no-action rather than erroring. New statuses may be added over time. |
| 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 account. Requires an active Pro 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 — this is enforced server-side, so a leaked live key can never be used to mint or rotate a fresh one. 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 Pro 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 →
Pro 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.
PayTrace can POST a summary to your endpoint automatically after every reconciliation run — whether that run came from a live POST /reconcile call or from an hourly Shopify autopilot pass. This is a real, shipped feature, not on the roadmap — but it's configured from the dashboard, not from the API request itself, and it's available on the Enterprise plan only (not Pro).
Configure, don't call. Turn this on from Enterprise → Alerts in the dashboard: set your endpoint URL and a signing secret. There is no field on POST /reconcile that enables or overrides this per-request — it fires automatically for every run once configured, on both API-triggered and autopilot-triggered reconciliations.
Delivery: one POST per completed run, with a 5-second timeout and one automatic retry. A non-2xx response from your endpoint counts as a failed delivery. The payload is a run summary, not the full results array:
{
"brand": "brand_nykaa_01",
"run_at": "2026-09-28T06:00:00.000Z",
"flagged_count": 14,
"amount_risk": 18420,
"run_id": "recon_a7f2c1d9_1712345678"
}
X-PayTrace-Sig: hex-encoded HMAC-SHA256 of the raw request body
Not signed with your API key. X-PayTrace-Sig is computed with the separate signing secret you set alongside the webhook URL in Enterprise → Alerts — verify against that secret, not your pt_live_* key.
White-label reports.
Both Pro and Enterprise accounts can configure a white-label profile. Your agency name and email appear on exported Excel reports and dispute letters instead of "PayTrace". This is a dashboard setting — there is no request field on POST /reconcile that sets or overrides it.
Pro plan (₹3,999/month): configure it from Pro → White-label Settings in the dashboard. This is saved per-browser, not synced to your account server-side — set it again if you switch devices or clear site data.
Enterprise plan: white-label name and email are saved server-side on your account and apply consistently everywhere, including the Excel audit exports and dispute letters generated from the Enterprise dashboard.