The Integration Pattern¶
Applies to: e-Invoice · e-Way Bill · Which modules do I have?
Purpose¶
Give the build order every ERP integration follows, once, so each ERP-specific page describes only what is genuinely specific to it. Work through the steps in sequence: each one is verifiable on its own, and building them out of order produces a system where a failure could be in any of several places at once.
Audience¶
Integration developers and implementation consultants. Every step below assumes ERP-side development access.
Prerequisites¶
- API Documentation read in full
- Sandbox credentials
- A raw sample payload per document type, exported from the ERP
Steps¶
1. Understand the shape before you build¶
The integration exists so that a document raised in the ERP becomes legally compliant without anyone re-keying it, and so the ERP holds the compliance outcome against its own record.
| Without the integration | With it |
|---|---|
| Manual export, upload and re-key | Automatic |
| Compliance state lives only in Complifly | Also in the ERP, against the invoice |
| Errors found at filing time | Found at ingest, on the day |
| Effort scales with volume | Effort is roughly constant |
The flow it produces:
ERP document posted
|
v
[1] ERP emits its native payload
|
v
[2] POST /api/v1/ingest/invoice/mapped/{templateId}
| with a machine credential
v
[3] Complifly: authenticate -> transform -> validate ->
duplicate check -> route -> store
|
+--> rejected: ERP records the failure, a person acts
|
v
[4] Accepted. invHdrId returned; ERP stores it
|
v
[5] IRN generated (by a user, or automatically)
|
v
[6] Write-back event emitted
|
v
[7] ERP pulls the event, stores the outcome, acknowledges
Stages 1 to 4 are synchronous. Stages 5 to 7 are not. The ERP must not block waiting for a reference number — it arrives later, through the write-back feed.
Verify before moving on: everyone building this agrees which system holds which fact, and that a reference number is not available at the moment of posting.
2. Obtain a machine credential and prove it works¶
One credential per ERP, never shared, scoped to the registrations that ERP sends for. Full procedure in Machine-to-Machine Auth; the API view is in API Authentication.
Prove it with a low-risk call before writing anything else:
GET /api/v1/integration/health
X-API-Key: cfly_...
Verify before moving on: any response other than 401 proves authentication works — even 404 NO_SUBSCRIPTION. Only 401 means the credential is wrong.
3. Emit a raw payload from the ERP¶
Have the ERP produce its own native shape, unmodified. Do not rename fields toward NIC's names and do not tidy values by hand — the translation belongs in the mapping template, where it can be changed without an ERP release.
{
"GSTNO": "29AAACW3775F000",
"DOCTYPE": 0,
"DOCNO": "INV-2026-0001",
"DOCDATE": "5/26/2026 12:00:00 AM",
"BUYER_GSTIN": "27AABCU9603R1ZM",
"BUYER_NAME": "Example Buyer Pvt Ltd",
"BUYER_STATE": "27",
"TOTAL": 59000,
"LINES": [
{ "SLNO": 1, "DESC": "Laptop", "HSN": "8471",
"QTY": 2, "RATE": 25000, "AMOUNT": 50000, "GSTRATE": 18 }
]
}
The fields every integration must ultimately supply, whatever the ERP calls them:
| Group | Fields |
|---|---|
| Document | Number, date, type, supply type, transaction type |
| Supplier | Registration, legal name, address, location, postal code, state code |
| Buyer | Registration, legal name, address, location, postal code, state code, place of supply |
| Values | Assessable value, total value |
| Per line | Serial number, description, goods-or-service flag, classification code, unit price, total, assessable amount, rate, total line value |
| Dispatch-from | Only when goods leave from a different address — but once any part is used, the rest becomes required |
The dispatch-from rule catches people out. It is optional as a block and mandatory as a whole: supply one field of it and you must supply the rest.
Verify before moving on: the sample is raw ERP output, and includes at least one multi-line document.
4. Build the mapping template¶
Map each ERP field onto its NIC counterpart, converting date formats and translating code values as you go.
Do this in the product, not in code. Build a Mapping Template is the screen-by-screen procedure — create the template, load your ERP's fields from the sample, drag them onto NIC fields, and set each rule to direct, date or lookup. Mapping Templates API covers the same ground for teams that would rather build templates programmatically.
Note the template ID it gives you. It goes in the ingest URL at step 5.
Verify before moving on: the mapped payload is correct field by field, checked against the transform preview — not merely that the document was accepted. Dates need a document actually posted, dated past the 12th of a month.
5. Send one document and handle every response class¶
POST /api/v1/ingest/invoice/mapped/12
X-API-Key: cfly_...
Content-Type: application/json
Accepted:
{ "status": "ACCEPTED", "invHdrId": 12345, "docNo": "INV-2026-0001",
"gstin": "29AAACW3775F000", "fiscalYear": "2026-27", "itemsIngested": 1 }
Rejected:
{ "status": "REJECTED", "docNo": "INV-2026-0001", "docType": "INV",
"errors": [ { "Level": "HEADER", "Field": "BillTo_Pos",
"Message": "Place of Supply is required", "Severity": "ERROR" } ],
"requestId": "…" }
Give every response class its own path in the ERP:
| Response | ERP behaviour |
|---|---|
201 ACCEPTED |
Store invHdrId; mark as sent |
422 REJECTED |
Store the errors; flag for a human; do not retry |
409 DUPLICATE |
Reconcile against the returned identifier; do not retry |
403 |
Alert an administrator — a scope problem, not a data problem |
401 |
Refresh the credential; retry once |
429 |
Back off; retry |
5xx |
Back off; retry, bounded |
| Timeout | Query status before resending |
The 422 row is where integrations most often fall short. A rejection that only reaches a log file is a rejection nobody acts on, and the document silently never becomes compliant.
Verify before moving on: force each response class and confirm it takes the correct path — not just that the happy path works.
6. Retry correctly¶
Exponential backoff with jitter, bounded, plus a circuit breaker. Full policy in Rate Limits and Errors.
Never retry 422, 409 or 403. They cannot succeed, and they consume the rate-limit headroom genuine traffic needs.
Verify before moving on: an induced 422 is attempted exactly once, and a bounded retry storm cannot outlive the incident that caused it.
7. Consume the write-back feed¶
Outcomes reach the ERP through the write-back feed, on a schedule:
GET /api/v1/integration/events?since={cursor}&limit=100
-> for each event, if not already processed (by event_id): store, mark processed
POST /api/v1/integration/events/ack { "event_ids": [...] }
-> only after the outcomes are durably committed
-> store the new cursor
{ "event_id": "…", "event_pk": 4821, "event_type": "IRN_GENERATED",
"gstin": "29AAACW3775F000", "doc_no": "INV-2026-0001", "irn": "…" }
Delivery is at-least-once, so the consumer must be idempotent, and acknowledgement must follow durable storage — acknowledge first and a crash loses the event permanently. Detail in Webhooks and Events.
Verify before moving on: a duplicate event is processed once, and a consumer killed mid-batch resumes from its cursor with nothing lost or repeated.
8. Build reconciliation before you need it¶
Three distinct failure classes, with three distinct recoveries:
| Class | Recovery |
|---|---|
| A document failed | Correct it in the ERP and resend under the same number if it was never accepted, or a new number if it was accepted and registered |
| The integration stopped | On restart, reconcile: for every document the ERP believes it sent since the outage, query status and resend only those absent |
| Write-back was missed | Resume from the stored cursor. If events were dead-lettered, an administrator replays them |
Build the reconciliation query into the integration from the start. It is the difference between a five-minute recovery and a day of manual comparison.
Verify before moving on: simulate an outage, run reconciliation, and confirm both systems agree afterwards with no duplicates created.
9. Test in this order¶
| Stage | What to test | Where |
|---|---|---|
| Unit | Payload construction, date formatting | ERP development |
| Mapping | Every field, using raw ERP samples | Sandbox |
| Happy path | One document end to end, ingest to write-back | Sandbox |
| Edge cases | Export, reverse charge, zero-rated, discount, rounding, multi-line, credit note, debit note | Sandbox |
| Failure paths | Every response class in the step 5 table | Sandbox |
| Volume | A realistic daily batch | Sandbox |
| Recovery | Simulate an outage; reconcile | Sandbox |
| Business acceptance | Real users, real documents | Test environment |
Edge cases are the ones customers do not volunteer and integrations always meet eventually. Ask for them by name.
Verify before moving on: every row above has been run and recorded, edge cases included.
10. Secure the integration¶
| Concern | Control |
|---|---|
| Credential storage | A secret manager. Never in code or in source control |
| Transport | HTTPS with certificate validation. Never disable it to work around a proxy |
| Scope | One credential per ERP, scoped to its registrations only |
| Rotation | Scheduled, owned, and rehearsed |
| Logging | Never log credentials. Take care with personal data in payloads |
| Least privilege | The credential needs ingest and write-back only, not administration |
| Inbound path | Pull-based write-back needs no inbound path to the ERP — prefer it where the network policy is restrictive |
Verify before moving on: no credential appears in source control or a log, and a rotation has been rehearsed rather than merely scheduled.
11. Monitor from both sides¶
Monitor from the ERP side as well as from Complifly. An integration that has stopped sending looks identical, from Complifly, to a quiet business day.
| Signal | Alert when |
|---|---|
| Documents sent per period | Falls to zero during business hours |
| Rejection rate | Rises above the normal baseline |
| Documents awaiting a reference number | Age exceeds the reporting window's safety margin |
| Write-back backlog age | Rising — the number that matters, not the count |
| Consecutive failures | Threshold reached |
| Credential expiry | Approaching |
The third row is the one with a deadline attached. A document sitting unregistered is on a statutory clock, and when the clock runs out there is no remedy.
Verify before moving on: each alert has been fired in anger, not merely configured, and reaches a named person.
Validation¶
| Check | Pass condition |
|---|---|
| Mapped payload inspected | Every field verified, not just acceptance |
| Every error class handled | Each takes the correct path in the ERP |
| Nothing unretryable is retried | Verified by forcing each response |
| Timeout reconciliation works | Status queried before resend |
| Write-back consumer is idempotent | A duplicate event is processed once |
| Outage recovery works | Simulated, and both systems agree afterwards |
| Monitoring alerts fire | Tested, not merely configured |
| A person owns rejections | Named, with a process |
| Credentials in a secret manager | Verified |
| Documentation handed over | The customer can operate it unaided |
Troubleshooting¶
| Symptom | Cause | Action |
|---|---|---|
| Works in testing, fails on real documents | Cleaned samples used | Use raw ERP output |
| A wave of failures weeks after go-live | A first edge case | Test edge cases before go-live |
| Rejections invisible to the business | Only logged | Surface them to a person who can act |
| Duplicates after an outage | Blind resend | Reconcile by status first |
| ERP missing outcomes | Write-back disabled, no subscription, or the consumer stopped | Check in that order |
| Events processed twice | Consumer not idempotent | Deduplicate on the event identifier |
| Systems disagree on what was processed | No reconciliation | Build it |
| A brief outage becomes long | Unbounded retries | Bound them and add a circuit breaker |
| Nobody noticed the integration stopped | No ERP-side monitoring | Alert on documents sent falling to zero |
| Documents unregistered past the window | Nobody watching the age of pending documents | Alert on it. There is no remedy after the window closes |
Related Articles¶
- Build a Mapping Template — step 4, screen by screen
- Ingest API · Write-back API
- Mapping Templates API
- Rate Limits and Errors
- Custom ERP — applying this pattern to any system