GET https://app.fire.rest/api/v1/external/orders/EXT-100234/cancellation-eligibility?locale=en
x-api-key: <your_api_key>
{
"success": true,
"data": {
"canCancel": true,
"outcome": "ALLOW",
"outcomeLabel": "Allow cancelling",
"code": null,
"reason": null,
"reasonDetail": null,
"source": "default",
"threshold": null,
"resolvedFrom": {
"orderStatus": "COMPLETED",
"paymentStatus": "SUCCEEDED",
"countryCode": "BR",
"fiscalStatus": "authorized",
"isFinalConsumer": "true",
"minutesSinceCreation": "12.4",
"minutesSinceAuthorization": "11.9"
}
}
}
{
"success": true,
"data": {
"canCancel": false,
"outcome": "DENY",
"outcomeLabel": "Do not allow",
"code": "ORDER_NOT_CANCELLABLE",
"reason": "The order is no longer in a cancellable state",
"reasonDetail": null,
"source": "baseline",
"threshold": null,
"resolvedFrom": {
"orderStatus": "CANCELLED",
"paymentStatus": "SUCCEEDED",
"countryCode": "BR",
"fiscalStatus": "cancelled",
"minutesSinceCreation": "94.2"
}
}
}
{
"success": true,
"data": {
"canCancel": false,
"outcome": "DENY",
"outcomeLabel": "Do not allow",
"code": "CANCELLATION_POLICY_DENIED",
"reason": "Prazo de anulação da SEFAZ vencido",
"reasonDetail": "A NFC-e autorizada só pode ser anulada em até 30 minutos. Depois disso é preciso abrir um chamado fiscal.",
"source": "account",
"threshold": {
"field": "minutesSinceAuthorization",
"threshold": 30,
"actual": 47.3
},
"resolvedFrom": {
"orderStatus": "COMPLETED",
"paymentStatus": "SUCCEEDED",
"countryCode": "BR",
"fiscalStatus": "authorized",
"isFinalConsumer": "false",
"govIdType": "CPF",
"minutesSinceCreation": "52.1",
"minutesSinceAuthorization": "47.3"
}
}
}
{
"success": false,
"error": "FORBIDDEN",
"message": "Vendor-scoped API key required (accountId binding missing)"
}
{
"success": false,
"error": "NOT_FOUND",
"message": "InjectedOrder not found: EXT-100234"
}
{
"success": false,
"error": "CONFLICT",
"code": "AMBIGUOUS_ORDER_REFERENCE",
"message": "External order id EXT-100234 matches 2 orders across vendors; cannot disambiguate"
}
Partner APIs
Cancellation eligibility
Ask whether an order can be cancelled — without cancelling it. The preflight for the cancellation button.
GET
/
api
/
v1
/
external
/
orders
/
{orderId}
/
cancellation-eligibility
GET https://app.fire.rest/api/v1/external/orders/EXT-100234/cancellation-eligibility?locale=en
x-api-key: <your_api_key>
{
"success": true,
"data": {
"canCancel": true,
"outcome": "ALLOW",
"outcomeLabel": "Allow cancelling",
"code": null,
"reason": null,
"reasonDetail": null,
"source": "default",
"threshold": null,
"resolvedFrom": {
"orderStatus": "COMPLETED",
"paymentStatus": "SUCCEEDED",
"countryCode": "BR",
"fiscalStatus": "authorized",
"isFinalConsumer": "true",
"minutesSinceCreation": "12.4",
"minutesSinceAuthorization": "11.9"
}
}
}
{
"success": true,
"data": {
"canCancel": false,
"outcome": "DENY",
"outcomeLabel": "Do not allow",
"code": "ORDER_NOT_CANCELLABLE",
"reason": "The order is no longer in a cancellable state",
"reasonDetail": null,
"source": "baseline",
"threshold": null,
"resolvedFrom": {
"orderStatus": "CANCELLED",
"paymentStatus": "SUCCEEDED",
"countryCode": "BR",
"fiscalStatus": "cancelled",
"minutesSinceCreation": "94.2"
}
}
}
{
"success": true,
"data": {
"canCancel": false,
"outcome": "DENY",
"outcomeLabel": "Do not allow",
"code": "CANCELLATION_POLICY_DENIED",
"reason": "Prazo de anulação da SEFAZ vencido",
"reasonDetail": "A NFC-e autorizada só pode ser anulada em até 30 minutos. Depois disso é preciso abrir um chamado fiscal.",
"source": "account",
"threshold": {
"field": "minutesSinceAuthorization",
"threshold": 30,
"actual": 47.3
},
"resolvedFrom": {
"orderStatus": "COMPLETED",
"paymentStatus": "SUCCEEDED",
"countryCode": "BR",
"fiscalStatus": "authorized",
"isFinalConsumer": "false",
"govIdType": "CPF",
"minutesSinceCreation": "52.1",
"minutesSinceAuthorization": "47.3"
}
}
}
{
"success": false,
"error": "FORBIDDEN",
"message": "Vendor-scoped API key required (accountId binding missing)"
}
{
"success": false,
"error": "NOT_FOUND",
"message": "InjectedOrder not found: EXT-100234"
}
{
"success": false,
"error": "CONFLICT",
"code": "AMBIGUOUS_ORDER_REFERENCE",
"message": "External order id EXT-100234 matches 2 orders across vendors; cannot disambiguate"
}
Answers whether an order can be cancelled, without cancelling anything. The point of sale calls it to show or hide the cancel button, and to explain to the cashier why when it cannot.
It runs the same policy service as the real cancellation, so the preflight and the outcome cannot disagree about the rules. The natural pair of this endpoint is Cancel order: this one asks, that one executes.
A
It was not always so. The credit note and the business day were checked separately inside the real cancellation, this endpoint skipped them, and a
orderId accepts any of the four ways an order can be named from outside: the external order id your channel generated when injecting it, the order_id that travelled in the injection payload, Fire’s order_code, and Fire’s internal UUID. Fire tries all of them within your key’s vendor. If the id matches more than one order in that scope, Fire refuses with 409 instead of guessing: answering about the wrong order would be worse than not answering.A
200 does not mean “yes”. The verdict travels in the body: this endpoint returns 200 even when the order cannot be cancelled, because “no” is the answer to the question, not an error. A non-200 status is a real error — authentication, unknown order — never a policy denial.Authentication
string
required
Your Fire API key with the
orders:read scope. The key must be vendor-scoped — keys without an account binding are rejected with 403. The tenant is derived from the key, never from the request.Path parameters
string
required
Order identifier. Accepts the external id, the payload’s
order_id, Fire’s order_code, or Fire’s internal UUID.Query parameters
string
default:"es"
es, en or pt. Language of reason, reasonDetail and outcomeLabel. It only affects Fire’s own rules, which carry their label in all three languages; the text of a rule configured by the account is theirs and comes back exactly as written, untranslated.GET https://app.fire.rest/api/v1/external/orders/EXT-100234/cancellation-eligibility?locale=en
x-api-key: <your_api_key>
Response
The verdict comes wrapped in the standard envelope:{ "success": true, "data": { ... } }.
boolean
The answer. It is the same verdict the real cancellation will give.
string
Raw policy result:
ALLOW or DENY. Today it is redundant with canCancel on purpose: it travels from day one so that if a third outcome ever appears, adding it does not break existing consumers.string
Display name of the outcome, in the requested language. It is the safety net when
reason is null: without it, a denial by an unnamed rule would reach the cashier without a single word to show.string | null
The reason, stable.
null when the order can be cancelled. This is the contract — decide in code with code, never by parsing reason. The possible codes are listed below.string | null
The name of the rule that decided, for humans. Fits in one line on a POS screen. It is editable and translatable text — not a contract.
string | null
The long note of that same rule, or
null. Separate from reason so the consumer decides how much space to give it: a POS paints one line, a detail screen can paint both. Also editable text, not a contract. Fire’s own rules carry no note, so a denial by a Fire rule always comes with reasonDetail: null — that is expected, not a bug. The note only appears on rules configured by the account.string
Where the decision came from:
baseline (a Fire rule), account (a rule configured by the account), or default (no rule matched — the order can be cancelled).object | null
Present only when the winning rule compared against a numeric threshold.
With this you can tell the cashier “over the limit by 17 minutes” without learning any new codes.
Show threshold
Show threshold
object
The evaluated context, as a map of field to value. It is the forensic receipt: it lets you reconstruct why this was decided even if the configuration changes afterwards.
A true here is the same true the POST /cancel gives
It was not always so. The credit note and the business day were checked separately inside the real cancellation, this endpoint skipped them, and a pending field announced those two omitted checks. That field is gone: both are policy rules now, and both endpoints run them.
One difference survives, and it is a legitimate race rather than a design gap: between asking and cancelling, the business day can close or someone else can fire a cancellation. Asking reserves nothing.
This endpoint answers for one order on purpose: resolving it costs two queries, and over a list that would be one per row.
Denial codes
The real cancellation returns the samecode when it denies for the same reason.
| Code | Source | What happened |
|---|---|---|
CANCELLATION_IN_PROGRESS | Fire rule | A cancellation is already in flight. |
FISCAL_ALREADY_CANCELLED | Fire rule | The fiscal document is already voided. |
ORDER_NOT_CANCELLABLE | Fire rule | The order is FORCE_CLOSED or CANCELLED. |
FISCAL_REPRESENTATION_NOT_VOIDED | Fire rule | There is an invoice and no credit note yet. Request the fiscal void first. |
BUSINESS_DAY_CLOSED | Fire rule | The order belongs to a closed business day, or to a day before the active one. |
CANCELLATION_POLICY_DENIED | account rule | A rule configured by the account denied it. The human-readable reason travels in reason. |
{
"success": true,
"data": {
"canCancel": true,
"outcome": "ALLOW",
"outcomeLabel": "Allow cancelling",
"code": null,
"reason": null,
"reasonDetail": null,
"source": "default",
"threshold": null,
"resolvedFrom": {
"orderStatus": "COMPLETED",
"paymentStatus": "SUCCEEDED",
"countryCode": "BR",
"fiscalStatus": "authorized",
"isFinalConsumer": "true",
"minutesSinceCreation": "12.4",
"minutesSinceAuthorization": "11.9"
}
}
}
{
"success": true,
"data": {
"canCancel": false,
"outcome": "DENY",
"outcomeLabel": "Do not allow",
"code": "ORDER_NOT_CANCELLABLE",
"reason": "The order is no longer in a cancellable state",
"reasonDetail": null,
"source": "baseline",
"threshold": null,
"resolvedFrom": {
"orderStatus": "CANCELLED",
"paymentStatus": "SUCCEEDED",
"countryCode": "BR",
"fiscalStatus": "cancelled",
"minutesSinceCreation": "94.2"
}
}
}
{
"success": true,
"data": {
"canCancel": false,
"outcome": "DENY",
"outcomeLabel": "Do not allow",
"code": "CANCELLATION_POLICY_DENIED",
"reason": "Prazo de anulação da SEFAZ vencido",
"reasonDetail": "A NFC-e autorizada só pode ser anulada em até 30 minutos. Depois disso é preciso abrir um chamado fiscal.",
"source": "account",
"threshold": {
"field": "minutesSinceAuthorization",
"threshold": 30,
"actual": 47.3
},
"resolvedFrom": {
"orderStatus": "COMPLETED",
"paymentStatus": "SUCCEEDED",
"countryCode": "BR",
"fiscalStatus": "authorized",
"isFinalConsumer": "false",
"govIdType": "CPF",
"minutesSinceCreation": "52.1",
"minutesSinceAuthorization": "47.3"
}
}
}
{
"success": false,
"error": "FORBIDDEN",
"message": "Vendor-scoped API key required (accountId binding missing)"
}
{
"success": false,
"error": "NOT_FOUND",
"message": "InjectedOrder not found: EXT-100234"
}
{
"success": false,
"error": "CONFLICT",
"code": "AMBIGUOUS_ORDER_REFERENCE",
"message": "External order id EXT-100234 matches 2 orders across vendors; cannot disambiguate"
}
What to build with each field
- Decide in code with
code.reasonandreasonDetailare editable, translatable text — never parse them. - Show
reasonon one line;reasonDetailis the paragraph, for screens with more room. Whenreasonisnullon a denial, fall back tooutcomeLabel. - Use
thresholdto render “over by N” messages generically, without knowing any rule in particular. - Keep
resolvedFromin your logs: it is the receipt that explains the verdict even after the account’s rules change.
Related
Cancel order
The other half of the pair: this endpoint asks, that one executes.
Get order
Read the order the verdict refers to.

