Skip to content

Submit a chargeback's evidence packet to the issuer

POST
/v1/payment-disputes/{dispute_id}/evidence/submission
curl --request POST \
--url https://api.aerandil.com/v1/payment-disputes/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/evidence/submission \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "packet_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "submit_with_gaps": false }'

The house counters the chargeback with the evidence packet the kernel assembled (assemble_dispute_evidence). Refused before anything is asked of the processor when the issuer is not waiting on the house, the deadline has passed, the evidence already went (here or at the processor’s dashboard), the packet named is not the case’s current one, the processor refused this packet before, a source the packet pinned has changed since it was assembled, or the packet has gaps the request does not acknowledge (submit_with_gaps: true, recorded on the packet with who submitted). Then each document is rendered — the folio’s issued invoice as the document the guest was handed, otherwise a PDF statement of the pinned object — uploaded to Stripe (purpose=dispute_evidence, at most 4.5 MB in all), and the dispute updated with the packet’s text fields and the files, submitted. A processor’s refusal is an outcome, not an error: 200 with outcome: refused and Stripe’s words, recorded on the case; the retry is a new packet. A failure Stripe saved under the packet’s key is read back off the dispute: Stripe holding the packet’s files is the submission, holding nothing of it is the refusal (refusal.code: processor_saved_failure, assemble again). The packet is marked sending with its file ids before the dispute update goes, so a retry after a lost answer (a 503, a dropped connection) finishes that packet — the same files under the same key, Stripe’s saved answer recorded — even after Stripe’s webhook has counted the submission. Every processor call is keyed from the packet’s own id. W1 (2026-10-21): a packet with a Visa Compelling Evidence 3.0 section sends it as Stripe’s nested evidence[enhanced_evidence][visa_compelling_evidence_3][…] keys (the booking’s half and two prior charges, each prior’s payment checked against the version it was read at), the signed registration card, the guest’s messages and the key-card taps go as files of their own, and the documents’ pages are counted against the card’s network before anything is uploaded. An inquiry the issuer escalated to a chargeback takes this submission once more (one per phase). Move 12 (2026-10-22): the submission certifies the dispute-fee waiver from facts the kernel holds and stamps it on the case (waiver: covered or not, and the first condition missed) — a whole packet on time, a reason the packet defends (fraudulent, unrecognized, credit_not_processed, product_not_received, duplicate), a card not keyed by hand, Radar’s normal (Stripe’s own word on the disputed charge, read at every submission) with no elevated rating set aside, the account’s chargebacks under 0.5% of its charges over the rolling year (weighed once the year holds 1,000 charges), and room under its cap (25% of the year’s platform fees) for the case’s own two fees, or fewer than five cases taken; a covered case that is lost passes neither fee to the hotel. Move 13: a gap-free packet still unsent 48 hours before the deadline is submitted by the night audit through the same path, as system. 501 unless AERANDIL_STRIPE_ENABLED=1.

dispute_id
required
string format: uuid

The chargeback (payment_dispute) to counter.

Idempotency-Key
string
>= 1 characters

Opaque idempotency token (Stripe-shape). Repeated POSTs with the same key replay the original response within the cache TTL rather than creating a duplicate object.

Media typeapplication/json
object
packet_id

The packet the desk reviewed; refused when another assembly has replaced it.

string format: uuid
submit_with_gaps

Submit knowing the packet’s gaps; required when it has any.

boolean

The processor was asked and its answer recorded: the evidence submitted, or the processor’s refusal.

Media typeapplication/json
object
dispute_evidence_packet_id
required
string format: uuid
file_count
integer
idempotent_replay
boolean
outcome
required
string
Allowed values: submitted refused
packet_version
integer | null
past_due
boolean | null
payment_dispute_id
required
string format: uuid
refusal
object
code
string | null
message
string
submission_count
integer | null
submitted_at
string | null format: date-time
waiver

Move 12 (2026-10-22): the dispute-fee waiver, certified at this submission and stamped on the case — if the case is lost, neither of the network’s fees is passed to the hotel. null when the processor refused the packet.

object
cap_case_floor
integer | null
cap_cases
integer | null
cap_limit
One of:
object
amount
required

Exact decimal as a string. Money is never a float — binary floating point cannot hold 0.10.

string format: decimal
currency
required
string format: iso-4217
cap_used
One of:
object
amount
required

Exact decimal as a string. Money is never a float — binary floating point cannot hold 0.10.

string format: decimal
currency
required
string format: iso-4217
certified
required
boolean
chargebacks_in_year
integer | null
charges_in_year
integer | null
condition

The first condition not met (WAIVER_REFUSALS).

string | null
Allowed values: evidence_gaps submitted_late reason_not_defended hand_keyed card_unknown radar_not_normal radar_unknown radar_unread elevated_rating_overridden no_merchant_account dispute_ratio cap_exhausted
decided_at
required
string format: date-time
pledged
One of:
object
amount
required

Exact decimal as a string. Money is never a float — binary floating point cannot hold 0.10.

string format: decimal
currency
required
string format: iso-4217
radar_risk_level

Stripe Radar’s rating of the payment at authorisation, verbatim (outcome.risk_level on the disputed intent’s latest charge, read at certification; never the kernel’s own assessment).

string | null
ratio_charge_floor

The charges the year must hold before the ratio is weighed (1,000); under it dispute_ratio is never the condition.

integer | null
refusal

The same, in words.

string | null
Example
{
"outcome": "submitted",
"waiver": {
"condition": "evidence_gaps"
}
}
Cache-Control
string

Always no-store. Object and link state is live data and is never cacheable.

The bearer authenticated but does not hold the scope this operation requires.

Media typeapplication/json

Structured error wrapper. Every non-2xx response from any Aerandil endpoint serialises to this shape; the HTTP status carries the failure class (4xx/5xx), the body adds a stable code for programmatic dispatch plus a human-readable message.

object
error
required
object
code
required

Stable, programmatic error code (e.g. unauthorized, forbidden, not_found, invalid_body, invalid_url, precondition_failed, no_secondary_key). Clients dispatch on code, not on the message (which may be localised or contextualised in future versions).

string
instance_id

Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.

string
message
required

Human-readable explanation of the failure.

string
parameters

Per-error structured detail. The shape varies by code (e.g. forbidden_action carries {"action": "..."}; precondition_failed carries {"current_version": N}).

Examplegenerated
{
"error": {
"code": "example",
"instance_id": "example",
"message": "example",
"parameters": "example"
}
}

No such object in the caller’s tenant — which is also the answer when it exists in another tenant, by design.

Media typeapplication/json

Structured error wrapper. Every non-2xx response from any Aerandil endpoint serialises to this shape; the HTTP status carries the failure class (4xx/5xx), the body adds a stable code for programmatic dispatch plus a human-readable message.

object
error
required
object
code
required

Stable, programmatic error code (e.g. unauthorized, forbidden, not_found, invalid_body, invalid_url, precondition_failed, no_secondary_key). Clients dispatch on code, not on the message (which may be localised or contextualised in future versions).

string
instance_id

Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.

string
message
required

Human-readable explanation of the failure.

string
parameters

Per-error structured detail. The shape varies by code (e.g. forbidden_action carries {"action": "..."}; precondition_failed carries {"current_version": N}).

Examplegenerated
{
"error": {
"code": "example",
"instance_id": "example",
"message": "example",
"parameters": "example"
}
}

evidence_already_submitted, dispute_not_awaiting_evidence, evidence_deadline_passed, no_evidence_packet, evidence_packet_superseded, evidence_packet_refused, evidence_packet_stale (assemble again), evidence_gaps_unacknowledged (its parameters.gaps lists them), dispute_not_on_the_rail (the case carries no processor reference), evidence_submission_unconfirmed (Stripe failed the update and the dispute could not be read back; the packet stays on its way), evidence_too_many_pages (W1: the documents render to more pages than the card’s network takes — fewer than 50 in all, 19 for Mastercard or a card whose network is not known; parameters carries page_count, page_limit, card_brand and each document’s pages; nothing reached the processor), or idempotency_in_flight (an earlier request under the same Idempotency-Key is still running).

Media typeapplication/json

Structured error wrapper. Every non-2xx response from any Aerandil endpoint serialises to this shape; the HTTP status carries the failure class (4xx/5xx), the body adds a stable code for programmatic dispatch plus a human-readable message.

object
error
required
object
code
required

Stable, programmatic error code (e.g. unauthorized, forbidden, not_found, invalid_body, invalid_url, precondition_failed, no_secondary_key). Clients dispatch on code, not on the message (which may be localised or contextualised in future versions).

string
instance_id

Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.

string
message
required

Human-readable explanation of the failure.

string
parameters

Per-error structured detail. The shape varies by code (e.g. forbidden_action carries {"action": "..."}; precondition_failed carries {"current_version": N}).

Examplegenerated
{
"error": {
"code": "example",
"instance_id": "example",
"message": "example",
"parameters": "example"
}
}

invalid_body, evidence_too_large (the documents come to more than Stripe takes), or idempotency_key_mismatch (the Idempotency-Key was used for another request).

Media typeapplication/json

Structured error wrapper. Every non-2xx response from any Aerandil endpoint serialises to this shape; the HTTP status carries the failure class (4xx/5xx), the body adds a stable code for programmatic dispatch plus a human-readable message.

object
error
required
object
code
required

Stable, programmatic error code (e.g. unauthorized, forbidden, not_found, invalid_body, invalid_url, precondition_failed, no_secondary_key). Clients dispatch on code, not on the message (which may be localised or contextualised in future versions).

string
instance_id

Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.

string
message
required

Human-readable explanation of the failure.

string
parameters

Per-error structured detail. The shape varies by code (e.g. forbidden_action carries {"action": "..."}; precondition_failed carries {"current_version": N}).

Examplegenerated
{
"error": {
"code": "example",
"instance_id": "example",
"message": "example",
"parameters": "example"
}
}

No card rail is configured on this deployment.

Media typeapplication/json

Structured error wrapper. Every non-2xx response from any Aerandil endpoint serialises to this shape; the HTTP status carries the failure class (4xx/5xx), the body adds a stable code for programmatic dispatch plus a human-readable message.

object
error
required
object
code
required

Stable, programmatic error code (e.g. unauthorized, forbidden, not_found, invalid_body, invalid_url, precondition_failed, no_secondary_key). Clients dispatch on code, not on the message (which may be localised or contextualised in future versions).

string
instance_id

Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.

string
message
required

Human-readable explanation of the failure.

string
parameters

Per-error structured detail. The shape varies by code (e.g. forbidden_action carries {"action": "..."}; precondition_failed carries {"current_version": N}).

Examplegenerated
{
"error": {
"code": "example",
"instance_id": "example",
"message": "example",
"parameters": "example"
}
}

rail_unavailable — Stripe could not be reached (a transport failure, a first-time 5xx, a 429) or still holds the packet’s key in flight. Nothing is lost: the packet is on its way, and the same request again finishes it under the same keys.

Media typeapplication/json

Structured error wrapper. Every non-2xx response from any Aerandil endpoint serialises to this shape; the HTTP status carries the failure class (4xx/5xx), the body adds a stable code for programmatic dispatch plus a human-readable message.

object
error
required
object
code
required

Stable, programmatic error code (e.g. unauthorized, forbidden, not_found, invalid_body, invalid_url, precondition_failed, no_secondary_key). Clients dispatch on code, not on the message (which may be localised or contextualised in future versions).

string
instance_id

Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.

string
message
required

Human-readable explanation of the failure.

string
parameters

Per-error structured detail. The shape varies by code (e.g. forbidden_action carries {"action": "..."}; precondition_failed carries {"current_version": N}).

Examplegenerated
{
"error": {
"code": "example",
"instance_id": "example",
"message": "example",
"parameters": "example"
}
}
Retry-After
integer

Seconds to wait before retrying the same request (RFC 9110 §10.2.3).