Ingest API¶
Applies to: e-Invoice · e-Way Bill · TDS · Which modules do I have?
Purpose¶
Document the endpoints an ERP uses to send documents into Complifly, and every response it can receive. This is the main integration surface and the one most integration effort is spent on.
Audience¶
Integration developers.
Prerequisites¶
- API Authentication, with a working machine credential
- A mapping template, if using the mapped endpoints — see Mapping Templates API
- The credential's GSTIN scope covering the registrations you will send for
Reference¶
Base path: /api/v1/ingest
The endpoints¶
| Method | Path | Purpose |
|---|---|---|
POST |
/invoice |
One document, already in the platform envelope |
POST |
/invoice/batch |
Up to 50 documents in the platform envelope |
POST |
/invoice/mapped/{templateId} |
One document in your ERP's own format |
POST |
/invoice/mapped/{templateId}/batch |
A batch in your ERP's own format |
GET |
/status/{docNo} |
Current status of a document |
POST |
/tds/deduction |
One TDS deduction |
POST |
/tds/deduction/batch |
A batch of deductions |
POST |
/tds/mapped/{templateId} |
A deduction in your own format |
Raw or mapped — choosing¶
Raw (/invoice) |
Mapped (/invoice/mapped/{templateId}) |
|
|---|---|---|
| Payload | The platform envelope | Your ERP's native shape |
| Translation | Your ERP does it | A mapping template does it |
| Changing a field mapping | An ERP code change | A configuration change |
| Suits | An ERP that can produce the envelope natively | Nearly everything else |
Prefer mapped. It moves field translation out of ERP code and into configuration that an implementation consultant can adjust without a development cycle. That difference is worth more over an integration's life than the small extra setup.
Sending one document¶
POST /api/v1/ingest/invoice/mapped/7
X-API-Key: cfly_...
Content-Type: application/json
{
"GSTNO": "29AAACW3775F000",
"DOCTYPE": 0,
"DOCNO": "INV-2026-0001",
"DOCDATE": "5/26/2026 12:00:00 AM",
"BUYER_GSTIN": "27AABCU9603R1ZM",
"LINES": [ { "SLNO": 1, "HSN": "8471", "QTY": 2, "RATE": 25000 } ]
}
The template translates field names, converts the date, and maps coded values — DOCTYPE: 0 to a tax invoice, for example.
Accepted:
{
"status": "ACCEPTED",
"invHdrId": 12345,
"docNo": "INV-2026-0001",
"gstin": "29AAACW3775F000",
"fiscalYear": "2026-27",
"itemsIngested": 1,
"message": "Invoice accepted."
}
Store invHdrId and docNo. They are how you correlate everything that follows.
Every rejection, and what to do¶
| HTTP | status |
Meaning | Retry? | Action |
|---|---|---|---|---|
401 |
— | Credential not accepted | Once, after refresh | See API Authentication |
403 |
REJECTED |
Supplier registration outside the credential's scope | No | Fix the scope, or send from the right credential |
409 |
DUPLICATE |
The document number already exists | Never | Terminal and permanent |
422 |
REJECTED |
Validation failed | No | The errors array names the fields |
429 |
— | Rate limited | Yes, after backoff | See Rate Limits and Errors |
500 |
ERROR |
Server error | Yes, with backoff | Escalate if it persists |
A duplicate response includes the existing invHdrId and where it was found, so your ERP can reconcile rather than treating it as an error to resolve manually.
Every refusal is recorded on the platform side before the response is sent, so a rejected document can always be accounted for — it is never silently discarded.
Validation failures¶
{
"status": "REJECTED",
"docNo": "INV-2026-0001",
"docType": "INV",
"errors": [
{ "Level": "HEADER", "InvoiceNo": "INV-2026-0001",
"Field": "BillTo_Pos", "Message": "Place of Supply is required",
"Severity": "ERROR" }
],
"requestId": "..."
}
Two validation passes run before acceptance: the document envelope is checked, and — on the IRN route — the full set of generation-time rules is brought forward to the door. That second pass is why a document accepted here will not usually be rejected later for a rule that could have been checked at ingest.
Surface Field to your users, not only Message. It is the stable identifier and the key to look up in Validation Error Codes.
Batch¶
Send an array; receive per-item results. The batch limit is 50. A batch is not a transaction — some items may be accepted while others are rejected, so read every result. Reading only the overall status code loses partial failures silently.
Checking status¶
GET /api/v1/ingest/status/INV-2026-0001
Returns the document's identity, its current status, and — once generated — the reference number and acknowledgement.
Use this to reconcile after a timeout. A timed-out request does not mean the document was not accepted; it means you do not know. Query status before resending, or the resend becomes a duplicate.
What happens after acceptance¶
Acceptance means the document is stored and valid — not that it has been registered with the government. Generation is a separate step, performed by a user or, where enabled, automatically.
| Status after ingest | Meaning |
|---|---|
PENDING |
On the IRN route, awaiting generation |
EWB_PENDING |
On the e-Way-Bill-only route |
Do not treat an ingest acceptance as compliance evidence. Wait for the generated status, either by polling status or — better — by consuming the Write-back API.
Validation¶
Test each of these in the sandbox before going live:
| Scenario | Expected | Your handling |
|---|---|---|
| Valid document | 201 ACCEPTED |
Store invHdrId |
| Missing required field | 422 with the field named |
Surface to a human; do not retry |
| Malformed registration number | 422 |
Same |
| Out-of-scope registration | 403 |
Report as a scope problem |
| Same document twice | 409 DUPLICATE |
Recognise as terminal |
| Batch with one bad item | Per-item results | Only the bad item is corrected; good ones are not resent |
| Batch of 51 | 422 |
Chunk to 50 |
| Invalid credential | 401 |
Refresh and retry once |
| Rate limit exceeded | 429 |
Back off |
| Timeout | Unknown | Query status before resending |
| Status query | Current state returned | Reconciliation works |
| Ambiguous date | Stored as intended | Verify with a date such as the fifth of March |
Troubleshooting¶
| Symptom | Cause | Action |
|---|---|---|
403 on every document |
Credential's GSTIN scope does not include the supplier | Check the scope, not the user permissions |
403 on some documents only |
Those belong to a registration outside scope | Identify which registration each document is for |
409 on a document never sent |
The ERP series restarted, or another system uses the same series | Numbers are permanently consumed. Fix the series |
422 naming a field the ERP does send |
Mapping rule targets the wrong path, or the value is empty | Inspect the mapped payload before blaming validation |
| Dates a month out | Date format token does not match the ERP's real format | Correct the template; re-test with an unambiguous date |
| All documents rejected after a template change | The template was changed without re-testing | Test template changes against real documents |
| Documents accepted but never registered | Acceptance is not generation | Generate, or enable automatic generation deliberately |
| Duplicates after a network problem | Blind resend on timeout | Query status first |
| Batch appears successful, documents missing | Only the overall code was read | Read every per-item result |
| Rejections not visible to the business | The ERP swallows them | Surface Field and Message to a person who can act |
Related Articles¶
- Mapping Templates API — building the translation
- Write-back API — receiving the outcome
- Validation Error Codes — interpreting
Field - Ingest Failures — the diagnostic tree
- Integration Guides — per-ERP playbooks