Skip to content

Write-back API

Applies to: ERP Write-back · Which modules do I have?

Purpose

Document how an ERP receives compliance outcomes — reference numbers, e-Way Bills, cancellations — so that the ERP stays in step with the compliance record without polling document status one at a time.

Audience

Integration developers.

Prerequisites

  • API Authentication, with a working machine credential
  • ERP_WRITEBACK_ENABLED set for the relevant registrations. It is off by default
  • An event subscription configured for your credential's registrations

Reference

Base path: /api/v1/integration

Two properties to design around

Emission is not retrospective. A tenant with write-back off stores no outbound events at all. Enabling it starts emission from that moment and does not backfill history. Seeding an ERP with existing compliance state is a separate exercise.

Delivery is at-least-once. An event may be delivered more than once. Your consumer must be idempotent on event_id. This is not a defensive nicety — it will happen.

The endpoints

Method Path Purpose
GET /events Pull pending events
POST /events/ack Acknowledge processed events
GET /health Feed health, including the backlog age

Pulling events

GET /api/v1/integration/events?since=0&limit=100&types=IRN_GENERATED,EWB_GENERATED
X-API-Key: cfly_...
Parameter Meaning
since Cursor. Pass the previous response's next_since
limit Maximum events. Clamped to a supported range
types Optional comma-separated filter

Response:

{
  "events": [
    {
      "event_id": "…",
      "event_pk": 4821,
      "event_type": "IRN_GENERATED",
      "gstin": "29AAACW3775F000",
      "doc_no": "INV-2026-0001",
      "irn": "…",
      "delivery": { "attempts": 1 }
    }
  ],
  "count": 1,
  "next_since": 4821,
  "has_more": false,
  "subscriptions": [ { "subscription_id": 12, "gstin": "29AAACW3775F000" } ]
}

An unknown value in types narrows the result rather than erroring. That is deliberate: a consumer rolling out a new filter can never break its own feed with a typo — but it also means a filter typo produces silence, not an error. Check count against expectation when introducing a filter.

Acknowledging

POST /api/v1/integration/events/ack
Content-Type: application/json

{ "event_ids": ["…", "…"] }

Response reports how many were acknowledged and how many were not recognised:

{ "acked": 2, "unknown": 0, "unknown_ids": [] }

Acknowledge only after your ERP has durably stored the outcome. An event acknowledged before it is committed is an event that is lost if your process then fails — the platform has been told you have it.

An identifier belonging to another tenant simply does not match. It acknowledges nothing rather than acknowledging someone else's event, so a guessed identifier is harmless.

Event types

All 23 types this build emits are listed below. The list is checked against the same module the product itself reads — services/integration/eventTypes.js — and npm run docs:check:complete fails if this page and that module disagree. A type absent from a consumer's handling is a branch nobody wrote, so the whole catalogue is published rather than a representative sample.

The change column is the delta an event carries. It is not the full picture — every event except DOC_REJECTED also carries a state block holding the document's complete compliance state as of that moment, and the processing rules below explain why you apply state rather than change.

e-Invoice

Event Fires when change carries
IRN_GENERATED NIC returns a reference number irn, ack_no, ack_dt, signed_qr_available, gsp, was_duplicate
IRN_FAILED NIC refuses generation error_code, error_message, class, retryable, revision_no, gsp, fix_hint, report_by
IRN_CANCELLED A cancellation is accepted irn, cancel_dt, cancel_reason, cancel_remark
IRN_CANCEL_FAILED A cancellation is refused, usually past the 24-hour window irn, error_code, error_message

IRN_FAILED is the most consequential event in this feed, and the one most often missed — a consumer written around the success path has no branch for it, and it means the government refused the document. Three of its fields decide what you do next: retryable is true only for TRANSIENT and INDETERMINATE outcomes, so a false value means resubmitting the same payload will fail again; report_by is the last date this document can still be reported at all; and fix_hint is a plain-language hint that legitimately returns empty for error codes the help registry does not yet answer.

error_message is always populated; error_code is not. When class is TRANSIENT — the GSP or the NIC portal was unreachable, timed out, or returned a 5xx, and automatic retries were exhausted — there is no NIC error code, so error_code is null while error_message explains what could not be reached and retryable is true. Read error_message and class on every IRN_FAILED; treat error_code as present only for a DATA_ERROR or STATUTORY outcome.

e-Way Bill

Event Fires when change carries
EWB_GENERATED A bill is raised ewb_no, ewb_dt, ewb_valid_till, irn_linked, gsp
EWB_FAILED NIC refuses the bill error_message, irn_linked, gsp — plus error_code on one of two paths, see below
EWB_CANCELLED A bill is cancelled ewb_no, cancel_dt, cancel_reason, cancel_remark
EWB_PARTB_UPDATED Vehicle or transport details change ewb_no, vehicle_no, trans_mode, trans_doc_no, trans_doc_dt, from_place, reason_code, reason_remark, version, new_valid_till
EWB_EXTENDED Validity is extended ewb_no, new_valid_till, remaining_distance, reason
EWB_REJECTED The counterparty rejects the bill ewb_no, rejected_by, rejected_at
EWB_STATUS_CHANGED Marked received, or marked active again ewb_no, from, to
EWB_TRANSPORTER_UPDATED The assigned transporter changes ewb_no, transporter_id
EWB_EXPIRED Validity lapses without extension ewb_no, expired_at, extended_times

Verify: EWB_FAILED is emitted from two call sites with different change shapes. The standalone e-Way Bill path includes error_code; the path taken when a bill is attempted straight after an IRN emits only error_message, irn_linked and gsp. Treat error_code as optional on this type and read error_message in both cases. H1

EWB_EXPIRED deserves attention for the opposite reason to IRN_FAILED: nobody triggers it. A scheduled job emits it when validity lapses, so it arrives with no corresponding user action in your ERP and no request that caused it.

Consolidated e-Way Bill

Emitted per member document. A trip sheet covering forty invoices produces forty events, one against each document your system originated — not one event describing the trip sheet.

Event Fires when change carries
CEWB_GENERATED This document's bill is folded into a trip sheet cewb_no, trip_sheet_no, vehicle_no, member_ewb_no, member_count
CEWB_REGENERATED The trip sheet is regenerated — breakdown, re-route cewb_no, previous_cewb_no, trip_sheet_no, vehicle_no
CEWB_PARTB_UPDATED Consolidated Part-B is updated on its own cewb_no, vehicle_no, from_place, from_state, trans_doc_no, trans_doc_dt, trans_mode, reason_code

Multi-vehicle movement

For a consignment split across several vehicles on one leg. Also fanned out per document.

Event Fires when change carries
MULTIVEH_INITIATED A multi-vehicle group is opened group_no, reason_code, total_qty, uqc
MULTIVEH_VEHICLE_ADDED A leg is added to the group group_no, leg_seq, vehicle_no, qty_in_vehicle, uqc
MULTIVEH_VEHICLE_CHANGED A vehicle is swapped within the group group_no, leg_seq, old_vehicle_no, new_vehicle_no, old_trans_doc_no, new_trans_doc_no, reason_code, reason_remark

Document lifecycle

Event Fires when change carries
DOC_REJECTED A document is refused at ingest — validation, routing or GSTIN scope reason, errors[]
DOC_REVISED A failed document is corrected in Complifly's workbench rather than by your ERP revision_no, source, changed_by, change_reason, changed_fields

DOC_REJECTED is the one event with no document behind it. It carries document.inv_hdr_id: null and a rejection_id in its place, and no state block at all — a document refused before acceptance has no compliance state to report. A consumer that assumes a non-null document anchor will throw on this type and on no other. It also always carries event_seq: 1: repeat rejections are counted on the rejection row rather than re-emitted.

DOC_REVISED tells you about something your ERP did not do. It fires when someone corrected the document inside Complifly, which means your copy is now the stale one. Treat it as a prompt to re-read, never as an echo of your own change.

Housekeeping

Neither of these follows a user action. Both are safe to apply and wrong to notify on.

Event Fires when change carries
STATE_RESYNC The reconciler finds your copy diverged from ours, or an operator replays an event reason — one of state_diverged, no_event_emitted, operator_replay — plus previous_event_type or original_event_type
STATE_BACKFILL An explicit historical seeding run is_backfill: true, backfill_run_id

Suppress notifications for both. Apply the state quietly: no toast, no workflow trigger, no outbound mail. A backfill of two years of documents that sends one email per document is a support incident of its own making.

Log STATE_RESYNC distinctly, though. It means your copy had drifted and we corrected it, so a rising count is evidence that something in your consumer is dropping events.

Documented elsewhere, but never emitted

Two types appear in older design material and no code path emits either. They are named here so an integrator does not build a branch waiting for an event that never arrives, and does not read their absence as a defect:

Type Why it is absent
DOC_SUPERSEDED The underlying succession mechanism — a cancelled document replaced by a new number — is not built
CEWB_SUPERSEDED Left unwired deliberately. It would double-report the action CEWB_REGENERATED already covers

Neither appears in the API's own type list, so a subscription filter cannot select them either.

Handling an unknown type

Log it, acknowledge it, and move on. New types are added over time, and a consumer that fails on an unrecognised type breaks on an upgrade it was not part of. Never leave an unknown event unacknowledged — it will be redelivered indefinitely.

Health

GET /health returns feed health per subscription and in aggregate. The number that matters is the age of the oldest pending event. A rising backlog age means your consumer has stopped, or has stopped acknowledging — a count alone will not tell you that, because a steady count can hide a stuck head-of-queue.

Monitor it. See Key Metrics and Thresholds.

Push delivery

Where configured, events are pushed to an ERP endpoint instead of pulled, with retry, circuit breaking and a dead-letter path. The circuit opens after consecutive failures and closes after a cooldown, so a failing endpoint does not consume the dispatcher indefinitely.

Push and pull carry the same events and the same guarantees. Pull is simpler to operate and does not require an inbound path to the ERP.

Validation

Scenario Expected Your handling
First pull Events returned, or an empty list Cursor stored
Cursor advances Second pull returns only newer events No reprocessing
Acknowledgement works acked matches what you sent Cursor and acknowledgement both correct
Duplicate delivery The same event arrives twice Processed once
Unknown event type An unrecognised type appears Logged and skipped, not fatal
No subscription 404 NO_SUBSCRIPTION Reported as a configuration problem, not a transient one
Type filter Filtered results count matches expectation — a typo produces silence, not an error
Health check Backlog age returned Monitored and alerted
Consumer stops Backlog age rises Alert fires
Acknowledgement ordering Acknowledge only after durable storage An interrupted process reprocesses rather than losing an event

Test the duplicate-delivery case explicitly. It is the single most important property of a write-back consumer and the easiest to leave untested.

Troubleshooting

Symptom Cause Action
No events ever Write-back disabled for the registration Check the flag first. It is off by default
No events for older documents Emission is not retrospective Expected. Seeding is a separate exercise
404 NO_SUBSCRIPTION No subscription for the credential's registrations Configure one
Events stop arriving Consumer stopped, or the cursor is stuck Check your own process before escalating
Backlog age rising, count steady The head of the queue is stuck Investigate the oldest event specifically
Same event repeatedly Not being acknowledged, or acknowledgement failing Check the acknowledgement response
Events processed twice Consumer not idempotent Deduplicate on event_id
Events lost after a crash Acknowledged before durable storage Acknowledge only after committing
Filter returns nothing A typo in types narrows silently Check the values against the event-type table
Push events stop after a burst of failures The circuit breaker opened Expected protection. Fix the endpoint; delivery resumes after cooldown
Dead-lettered events disappeared Dead-letter retention shorter than your reaction time Raise the retention. See Environment Variables
ERP out of step after an outage Events dead-lettered while it was down Replay from the administration surface. See Write-back Failures