- v1.2 · current
- v1.1 · previous
- v1 · deprecated
- v0 · deprecated
You are reading the current (v1.2) contract for
order.completed. v1.2 adds data.fiscalRepresentation: the fiscal numbering the point of sale obtained before injecting the order. It always travels: null when numbering was not attempted, and populated when it was — numberingStatus tells you how it ended. Carrying content does not mean the document is authorized. Additive only — nothing you already read changed.The block carries the order’s current fiscal document: the fields that mean the same in every country at the top, the authority identifiers inside countryData in that country’s own vocabulary, and the previous documents in history. When a cancellation numbers, the credit note moves to the top and the invoice moves down into history — with compensates pointing at it.order.completed fires when an order is successfully injected and paid. It carries the V4 order snapshot as trigger.data — every field your flow needs to act on the order without calling back into Fire.
Trigger condition
Fire emitsorder.completed exactly once per order, the first time both of these are true at injection time:
order.status === "COMPLETED"order.paymentStatus === "SUCCEEDED"
PENDING payment, or that fail payment, never produce order.completed. Cancellations after completion produce a separate order.cancelled event — they do not retract order.completed.
What’s in trigger.data
trigger.data is the V4 order snapshot — the same object that’s persisted in flow_queue.trigger_data and exposed to your flow’s templates.
The top-level keys, in the order they appear:
Example — real production payload (BR, sanitized)
The example below is taken from a realflow_queue.trigger_data row (Brazilian sandbox tenant, KIOSK channel, dine-in service). PII fields are replaced with placeholders; all other fields and shapes are verbatim.
Money values are strings holding the amount as an integer scaled ×10,000 — four fixed decimals.
"229000" is 22.9 BRL, not 229,000. To read it, divide by 10,000.It is the scale FIRE stores with: it avoids floating-point drift when summing taxes across several integrations. Parse with a decimal library, never parseFloat.The exception is paymentMethods[].totalBill, which the source sometimes ships as a JSON number — handle both cases.Field reference
Top-level identifiers
string
Fire’s internal order UUID. Stable across deliveries; use together with
event.id for traceability.string | null
Short, human-readable code shown on receipts and KDS displays (e.g.
95K, OC-br-001). null when the channel doesn’t assign one.string
Business day this order belongs to, in
YYYY-MM-DD. Computed in store-local time, so an order placed at 01:00 may belong to the previous business day depending on the store’s day-end cutoff.string
The order ID as provided by the channel/aggregator on injection. Use it when reconciling with upstream systems (POS, aggregator dashboards).
string | null
ISO 8601 UTC timestamp of when the order was originally placed. Distinct from
event.createdAt, which is when the flow execution started.string
Always
"COMPLETED" for this event.string
Always
"SUCCEEDED" for this event.boolean
true if loyalty points were redeemed on this order.boolean
true if the customer accumulated loyalty points.boolean
true if any discount was applied.string
Free-text customer note for the whole order. Empty string when not set.
data.store
object
Snapshot of the store at the moment the order was completed.
data.client
object | null
Customer who placed the order.
null for fully anonymous channel orders. When the sale does not identify the buyer —in any country— client travels populated with the final-consumer marker: govIdType: "FINAL_CONSUMER" and govIdNumber in zeros. It does not arrive translated into each regime’s rule — the DIAN’s generic NIT, for instance, is resolved by the fiscal provider.data.payments
object
Money breakdown.
data.fulfillment
object
How the order is being delivered.
data.kds
object
Kitchen-display context.
data.device and data.operator
object
{ uid, name, platform, metadata.ip } — originating device. Fields may be null for non-physical channels.object
{ uid, name, session.uid } — staff/cashier who processed the order. All fields null for self-service channels (kiosk, web).data.orderLines
object[]
Ordered products. Fully camelCase (transformed by the V4 builder).
data.marketing, data.metadata, data.channel
object | null
Loyalty + coupons.
null in most countries today; reserved for future use.object
Free-form bag for order-level extras. Often
{}.object
{ uid, code, metadata }. code examples: KIOSK, APP, IFOOD, RAPPI.Fiscal data
Fiscal data is included only when the store has fiscal emission enabled (
store.storeFiscalConfig.enabled === true). For non-fiscal countries or stores without configuration, all three locations below are absent or null.order.completed carries fiscal information in three distinct locations. Each serves a different purpose:
1. data.store.storeFiscalConfig — emitter identity & provider config
Identifies the legal entity emitting the document and how to authenticate with the fiscal provider. Credentials are intentionally NOT here — the fiscal node fetches them per provider/account.
2. data.payments.metadata.fiscal — order-level fiscal aggregates
SEFAZ-style aggregate totals, ready to be forwarded to a fiscal provider (your fiscal provider in Brazil). Values are strings scaled ×10,000, same as the rest of the event.
3. data.orderLines[n].metadata.fiscal — per-line fiscal classification
Per-product fiscal codes. Used by the fiscal provider to classify each line on the document.
taxes[n].metadata (in payments.totals[].taxes[], orderLines[].price.totalPrice[].taxes[], and orderLines[].lineTotals[].taxes[]) with codes like cst, cBenef, cClassTrib, reducao, rateNominal, rateEffective.
Country variations
The example above is from a Brazilian store — the most complex case. The V4 shape is identical across countries, including the granulartaxes[] breakdown: every country populates payments.totals[].taxes[], orderLines[].price.totalPrice[].taxes[] and lineTotals[].taxes[] with its own local taxes in the same { base, name, rate, amount, metadata } shape. What differs per country is:
- Tax names — BR uses
ICMS,PIS,COFINS,IBS_UF,IBS_MUN,CBS; other countries carry their local taxes (e.g.IVA) under the same structure. metadatacodes inside each tax — BR carries SEFAZ codes (cst,cBenef,cClassTrib,reducao,rateNominal,rateEffective); other countries carry their own jurisdiction’s codes.- SEFAZ fiscal-document emission — Brazil only. The NF-e aggregate
payments.metadata.fiscal(vBC / vNF / vICMS …), the per-lineorderLines[].metadata.fiscal(ncm / cfop / csosn), and theorder.invoiced/order.reversedevents fire only for BR. Non-BR countries still carry theirtaxes[]breakdown, but these SEFAZ-specific blocks are absent.
The BR sample above is a real (anonymized) capture. The LATAM samples below are illustrative:
<...> are example IDs, and fields shown as null are genuinely not-applicable for that country (e.g. secondaryGovIdType — only BR has the Inscrição Estadual; metadata.fiscal — only BR emits SEFAZ documents).- Brazil (BR)
- Argentina (AR)
- Chile (CL)
- Colombia (CO)
- Ecuador (EC)
- Venezuela (VE)
Brazilian stores with
storeFiscalConfig.enabled === true carry the full fiscal payload — see the Fiscal data section above. Country markers:store.locationInfo.country.code: "BR"·name: "Brasil"·timezone: "America/Sao_Paulo"store.locationInfo.currencyCode: "BRL"store.storeFiscalConfig.govIdType: "CNPJ"(14 digits)store.storeFiscalConfig.secondaryGovIdType: "INSCRICAO_ESTADUAL"payments.totals[].currencyCode: "BRL",paymentMethods[].currencyCode: "BRL",orderLines[].selectedCurrency: "BRL"- Populated:
payments.metadata.fiscal(vBC / vNF / vICMS / vPIS / vCOFINS / vTotTrib …),orderLines[].metadata.fiscal(ncm / cfop / csosn),lineTotals[].taxes[](ICMS, PIS, COFINS, IBS_*)
Quick reference table
As more countries get a dedicated fiscal pipeline, their fiscal events will land as
fiscal.*.{cc} (e.g. fiscal.authorized.co, fiscal.authorized.ec). Until then, only order.completed and order.cancelled fire for non-BR stores — fiscal blocks remain null / empty.data.policy and data.lastKnown
Every order event carries these two blocks — not just this one. They were added
together with the deferred-payment cycle and are added in v1.1, additive: existing
consumers keep working unchanged.
order.opened,
the event where these blocks matter most.
Handler example
Common pitfalls
- Decimals as strings × 10000.
payments.totals[0].total === "229000"means 22.9 BRL. Use a decimal library; neverparseFloat. - Casing is mixed in
payments.totals[]and parts ofpaymentMethods[]. Read bothcurrencyCodeandcurrencyCodedefensively. The V4 builder transforms most of the snapshot but passes payment objects through. fulfillment.deliverymay be present even for non-delivery services with placeholder zeroes. Always branch onfulfillment.service.code.clientmay be a populated “FINAL_CONSUMER” placeholder in any country — it is notnull. TreatgovIdType === "FINAL_CONSUMER"as anonymous for analytics.event.idis the flow execution ID, not the order ID. Useevent.idfor idempotency (it changes per delivery), andorderIdfor business key.- Multi-tenant routing. Use
store.account.uid,store.vendor.uid, andstore.codeto route to the right tenant in your system, even though Fire already scopes the flow on its side.
Related events
order.cancelled
Fires when this order is later cancelled.
order.invoiced
Brazil only — fires once SEFAZ authorizes the order’s fiscal document.
data.fiscalRepresentation
The fiscal numbering the point of sale obtained before injecting the order:
it charges, requests the identifiers, prints the receipt, and only then injects.
That is why it travels on the order and not in a separate fiscal event — by the
time the order is born, this already happened.
The key always travels. It arrives as null when numbering was not
attempted — aggregators, countries without fiscal representation, or merchants
with numbering turned off — and carries the block when it was.
Carrying the block means numbering was attempted, not that it succeeded:
numberingStatus tells you how the attempt ended, and failure why, when it did
not end well.
Branch on the value, not on the key’s presence:
lastKnown.fiscal is for, and that is what does move.
What does replace it is a new document. The block carries the order’s
current fiscal document. While there was only one, it was always the invoice;
when a cancellation produces a credit note, the note is what sits on top —
documentType says which one — and the invoice moves down into history,
whole and with its own authority identifiers. It is not lost: it moves.
compensates points at it by number, so the relationship stays explicit.
When numbering fails
A sale can be charged and end up with no fiscal receipt. That case travels too, and you must handle it: the identifiers come backnull and the reason is
in failure.
failure.scope:
TECHNICAL— print “pending” and carry on. It may resolve on its own.FUNCTIONAL— some data is wrong and retrying will not fix it. Needs correction.
lastKnown.fiscal.sourceEvent now reports real provenance. It used to be
derived from the status, so a processing seeded at injection was reported as
fiscal.callback even though no callback had occurred. That case now says
order.injected. If you branch on this field, account for the new value.
