Skip to Content
IntegrationsAccordion

Accordion

Accordion send a daily batch of flights completed by IBA members at US iFLY tunnels. The data comes from their Convergence extracts. The feed is one-way: we validate and store each record, and nothing is sent back except the response to the delivery.

Unlike FuzeMetrix, Convergence and Tunn3l, this is not a check-in integration. It has no member-facing effect yet — see Known limitations.

Endpoint

POST https://api.tunnelflight.com/api/external/accordion

Accordion call it once a day at 07:30 Pacific: 14:30 UTC while Pacific Daylight Time applies, 15:30 UTC after the clocks go back.

Authentication

The same HMAC scheme as the other connectors.

HeaderValue
client-idAccordion’s client id from the connector table
authorization (or token)HMAC-SHA256 of the JSON body, hex-encoded, keyed with the client’s secret
crypto.createHmac('SHA256', clientSecret).update(JSON.stringify(body)).digest('hex')

Payload

A batch envelope, not a single booking:

{ "records": [ { "Orderid": 900001, "CartID": 990001, "CustomerID": 990001, "IBANumber": 12345, "FirstName": "Sam", "LastName": "Example", "Email": "sam@example.com", "Phone": "", "FlightDateTime": "2026-09-28T18:30:00-07:00", "locationCode": "TAM", "ProductName": "01 Minute Block", "Sharecode": "01BLOCK", "TotalMinutes": 15, "RecordStatus": "Created" } ] }
FieldRequiredNotes
OrderidYes
CartIDYesWith CustomerID, identifies the record
CustomerIDYesA shared cart item arrives as one record per member, same CartID
IBANumberYesMust be a positive number and an existing member
FlightDateTimeYesISO 8601 with an offset — see Time zones
locationCodeYesAccordion’s tunnel code, e.g. TAM, DCVA
TotalMinutesYesMinutes flown; decimals allowed
RecordStatusYesCreated, Amended or Cancelled
FirstName, LastName, Email, Phone, ProductName, SharecodeNoStored as sent

Responses

Accordion decide success from the HTTP status and read the body only on failure, retrying once on any non-2xx. So a delivery with any rejected record answers 422, not 200 — a 200 carrying rejections would never be read, and those records never resent. The accepted records are stored either way, and because storage is idempotent the retry re-applies them harmlessly.

StatusWhenBody
200Every record stored{ valid: true, accepted, rejected: 0, rejections: [] }
422Some records rejected{ valid: false, accepted, rejected, rejections: [{ CartID, CustomerID, reason }] }
422Envelope unusable (no records array, empty, a record missing a field){ valid: false, message: "Invalid payload: …" }
400No client-id, or no signature header{ valid: false, message }
404Unknown client-id{ valid: false, message }
403Signature does not match{ valid: false, message }
500Unexpected error{ valid: false, message }

Per-record rejection reasons:

reasonMeaning
missing_fieldA required field is null. A field left out entirely fails the whole delivery at the envelope check instead
missing_iba_numberIBANumber is not a positive number
invalid_statusRecordStatus is not one of the three values
invalid_minutesTotalMinutes is not a non-negative number
invalid_timestampFlightDateTime has no offset or does not parse
unknown_memberNo member has that IBA number
storage_errorValid, but the database refused it (our problem, not theirs)

Idempotency

Records are upserted on (CartID, CustomerID). CartID alone is not unique: a cart item shared by two members arrives as two records with the same CartID. A replay or an amendment restates the row; first_seen_at never changes and last_seen_at moves on every restatement.

Time zones

Accordion send every flight on a Pacific clock, whichever tunnel it was at. Convergence converts the tunnel’s local time to Pacific, and Accordion pass the value through unchanged. The offset follows daylight saving by flight date: -07:00 during Pacific Daylight Time, -08:00 after it — so a delivery on 2 November that includes 31 October flights still carries -07:00 for those.

We apply the offset on arrival and store UTC in flight_at_utc, keeping the original offset in flight_offset for audit. Tunnel-local time is derived from the tunnel’s own time zone when read, never from the sender’s clock. A timestamp with no offset is rejected rather than guessed.

Storage

connector_accordion_booking holds one row per (CartID, CustomerID), never deleted. Alongside the payload fields it stores the resolved member_id and tunnel_id, flight_at_utc and flight_offset, is_withdrawn, and first_seen_at / last_seen_at.

Tunnel codes resolve through connector_tunnel_mapping.external_code (vendor = 'accordion'). The 36 codes were seeded by hand in migration 0013_accordion_connector.sql, matched on city and state because none match our names textually (their DCVA is our Loudoun iFLY). Five are discontinued locations kept for the year-to-date backfill. Miami, Oceanside and Wilmington are absent from Accordion’s export altogether.

Logging and audit

connector_history gets one row per request, including the full request body:

SELECT id, status, error_msg, ips, created_at, LENGTH(body) AS body_bytes FROM connector_history WHERE vendor = 'accordion' ORDER BY created_at DESC LIMIT 10;
SituationWritten bystatus / error_msg
Rejected before the handler (auth, envelope)shared/middlewares/req-validate.jsfail with the message returned
Reached the handlershared/middlewares/audit-history.js, after the response is sentsuccess, or fail with N of M records rejected

Structured error logs (JSON to stdout, for Datadog):

  • accordion.store_failed — one record could not be written; includes cart_id and customer_id
  • connector.audit_history_failed — the history row could not be written
  • connector.req_validate.insert_history_failed — the same, on the validation path

Known limitations

  1. An unexpected error leaves no trace. If something throws in the service (for example the database dropping during the member lookup), the controller returns 500 without logging, and no history row is written because the audit status was never set. Accordion see a 500 and retry once; we have no record of it.
  2. Rejection reasons are not stored. connector_history.error_msg holds only the count (3 of 20 records rejected). Which records were rejected, and why, is returned to Accordion and nowhere else — it has to be re-derived from the stored body.
  3. Large deliveries lose their history row. connector_history.body is TEXT (64 KB), roughly 190 records. Daily volumes fit, but a catch-up batch after missed days would overflow it; the bookings still store, and only a connector.audit_history_failed log line remains.
  4. The audit middleware’s comment overstates it. It says a handler that forgets to set its audit status produces an unknown row; the code writes nothing. This is the mechanism behind limitation 1.
  5. Personal data in history. Every history row keeps the whole delivery — names, emails and phone numbers — with no retention limit.
  6. No admin view. The admin connector report (/admin/reports/external/:type/history) accepts accordion, but the admin UI offers only Convergence and Fuse-Metrix. SQL is the only way to see the history.
  7. The data is stored, not used. Nothing member-facing reads connector_accordion_booking yet. The repository has a getBookingsByMember query for a future staff view, with no route.

See also

Last updated on