Skip to content

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