Custom ERP¶
Applies to: e-Invoice · e-Way Bill · Which modules do I have?
Purpose¶
Give a developer building an integration for an in-house or unlisted system a concrete build order, a worked call at each step, and an unambiguous definition of done.
Audience¶
Integration developers working with a system that has no named guide.
Prerequisites¶
- The Integration Pattern — read in full first. This page is that pattern, made concrete
- API Documentation
- Sandbox credentials
- The ability to make outbound HTTPS calls from the source system, or from something adjacent to it
Steps¶
Each step is verifiable on its own. Building them out of order produces a system where a failure could be in any of several places at once.
1. Authenticate¶
Obtain a machine credential (Machine-to-Machine Auth) and confirm it works before writing anything else:
GET /api/v1/integration/health
X-API-Key: cfly_...
Verify: any response — even 404 NO_SUBSCRIPTION — proves authentication works. Only 401 means the credential is wrong.
2. Produce a raw payload¶
Have the source system emit its own shape, unmodified. Do not rename fields toward NIC's names; the translation belongs in step 3, where changing it does not need a release of your ERP.
{
"invoice_no": "SI/2026/001",
"invoice_date": "2026-05-26",
"seller_gst": "29AAACW3775F000",
"buyer_gst": "27AABCU9603R1ZM",
"buyer_name": "Example Buyer Pvt Ltd",
"buyer_state_code": "27",
"doc_kind": "SALE",
"total": 59000,
"items": [
{ "sr": 1, "name": "Laptop", "hsn": "8471",
"qty": 2, "unit_price": 25000, "line_total": 50000, "gst_rate": 18 }
]
}
Verify: the sample is raw output, not tidied by hand, and covers at least one multi-line document.
3. Build the mapping template¶
Follow Build a Mapping Template — create the template in Administration → ERP Mapping, load the sample from step 2, and drag each ERP field onto its NIC counterpart.
For the payload above that means, at minimum:
| ERP field | NIC field | Rule type | Setting |
|---|---|---|---|
invoice_no |
DocDtls.No |
direct | — |
invoice_date |
DocDtls.Dt |
date | ERP format YYYY-MM-DD — not the pre-selected DD/MM/YYYY |
doc_kind |
DocDtls.Typ |
lookup | SALE → INV, plus every other kind the system emits |
seller_gst |
SellerDtls.Gstin |
direct | — |
buyer_gst |
BuyerDtls.Gstin |
direct | — |
buyer_state_code |
BuyerDtls.Pos |
direct | — |
items[0].hsn |
ItemList[0].HsnCd |
direct | Mapped once; every line expands from it |
Choose the lookup default so an unrecognised doc_kind fails rather than silently becoming a tax invoice.
Verify: the mapped payload is correct field by field in the transform preview. Dates are not proved there — the preview does not convert them — so confirm those in step 4.
4. Send one document¶
POST /api/v1/ingest/invoice/mapped/12
X-API-Key: cfly_...
Content-Type: application/json
The template ID is the Template #N badge from step 3. Complifly detects the payload shape itself: { header, items[] }, a top-level ItemList[], or a flat single-line record.
Verify: 201, an invHdrId returned and stored — and DocDtls.Dt on the stored record matches the date you sent. Use a date past the 12th of the month so a day-month swap cannot hide.
5. Handle every failure class¶
Give each response its own path. This is the step most often left half-built.
201 -> store invHdrId, mark sent
422 -> store errors, flag for a human, DO NOT retry
409 -> reconcile against the returned identifier, DO NOT retry
403 -> alert an administrator: scope problem
401 -> refresh credential, retry once
429 -> back off, retry
5xx -> back off, retry, bounded
timeout -> GET /status/{docNo} before deciding
Verify: force each response and confirm the correct path runs — including that nothing unretryable is retried.
6. Consume write-back¶
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
Verify: a duplicate event is processed once, and a consumer killed mid-batch resumes with nothing lost or repeated.
7. Add reconciliation¶
For every document the source system believes it sent in the last period, query status. Resend only those absent. Run it after any outage, and on a schedule.
Verify: simulate an outage, run it, and confirm both systems agree afterwards with no duplicates created.
8. Add monitoring¶
Alert from the source system as well as from Complifly — an integration that has stopped looks, from Complifly, exactly like a quiet business day. At minimum: documents sent falling to zero in business hours, rejection rate above baseline, and the age of documents still awaiting a reference number.
Verify: each alert has actually fired in a test, and reaches a named person.
9. Test the edge cases¶
Export, reverse charge, zero-rated, discount, rounding, multi-line, credit note, debit note. These are the ones customers do not volunteer and integrations always meet eventually — ask for them by name.
Verify: each has been run in the sandbox and recorded.
10. Volume test¶
Verify: a realistic daily load completes inside its window, without tripping rate limits.
Definition of done¶
An integration is finished when all of these are true. Not when documents go through.
| Requirement | Why |
|---|---|
| Every failure class takes its own path | The most common shortfall |
| Nothing unretryable is retried | Protects everyone's rate-limit headroom |
| Timeouts reconcile before resending | Prevents duplicates, which are permanent |
| The write-back consumer is idempotent | Delivery is at-least-once |
| Acknowledgement follows durable storage | Otherwise events are lost on a crash |
| Reconciliation exists and has been run | Five-minute recovery instead of a day |
| Monitoring alerts a person | An integration that stops silently is the worst outcome |
| Rejections reach a human daily | A rejection nobody sees is a document that never becomes compliant |
| Edge cases tested | They will occur |
| Documented for the customer | They must operate it without you |
Validation¶
Run each scenario in the sandbox and record the result:
| Scenario | Expected |
|---|---|
| Valid document | Accepted, identifier stored |
| Missing required field | 422, fields surfaced, no retry |
| Same number twice | 409, no retry |
| Out-of-scope registration | 403, administrator alerted |
| Invalid credential | 401, refreshed, retried once |
| Rate limit | 429, backed off, succeeded |
| Server error | Retried with backoff, bounded |
| Timeout | Status queried, no duplicate created |
| Duplicate event | Processed once |
| Unknown event type | Logged and skipped |
| Consumer restart | Resumes from the cursor, nothing lost or repeated |
| Simulated outage | Reconciliation restores agreement |
| Daily volume | Completes within the window, without rate limiting |
Troubleshooting¶
| Symptom | Cause | Action |
|---|---|---|
401 on every call |
Credential wrong, or clock drift | Check both |
403 on every document |
Credential scope excludes the supplier registration | Fix the scope |
422 naming a field you send |
Mapping targets the wrong path, or the value is empty | Inspect the mapped payload |
| Dates a month out | Format token describes NIC's format instead of the ERP's | Test with an unambiguous date past the 12th |
| Documents route wrongly | Lookup default too permissive | Make an unrecognised value fail |
| Only the first line appears | Payload is flat, or lines sit under a key that is neither items nor ItemList |
Post one of the three detected shapes |
| Duplicates after an incident | Blind resend on timeout | Reconcile first |
| Events processed twice | Consumer not idempotent | Deduplicate on the event identifier |
| Events lost after a crash | Acknowledged before committing | Reverse the order |
| Nobody noticed a stoppage | No monitoring | Alert on documents sent falling to zero |
Related Articles¶
- The Integration Pattern
- Build a Mapping Template — step 3, screen by screen
- Ingest API · Write-back API
- Mapping Templates API
- Rate Limits and Errors