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.
| Header | Value |
|---|---|
client-id | Accordion’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"
}
]
}| Field | Required | Notes |
|---|---|---|
Orderid | Yes | |
CartID | Yes | With CustomerID, identifies the record |
CustomerID | Yes | A shared cart item arrives as one record per member, same CartID |
IBANumber | Yes | Must be a positive number and an existing member |
FlightDateTime | Yes | ISO 8601 with an offset — see Time zones |
locationCode | Yes | Accordion’s tunnel code, e.g. TAM, DCVA |
TotalMinutes | Yes | Minutes flown; decimals allowed |
RecordStatus | Yes | Created, Amended or Cancelled |
FirstName, LastName, Email, Phone, ProductName, Sharecode | No | Stored 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.
| Status | When | Body |
|---|---|---|
200 | Every record stored | { valid: true, accepted, rejected: 0, rejections: [] } |
422 | Some records rejected | { valid: false, accepted, rejected, rejections: [{ CartID, CustomerID, reason }] } |
422 | Envelope unusable (no records array, empty, a record missing a field) | { valid: false, message: "Invalid payload: …" } |
400 | No client-id, or no signature header | { valid: false, message } |
404 | Unknown client-id | { valid: false, message } |
403 | Signature does not match | { valid: false, message } |
500 | Unexpected error | { valid: false, message } |
Per-record rejection reasons:
reason | Meaning |
|---|---|
missing_field | A required field is null. A field left out entirely fails the whole delivery at the envelope check instead |
missing_iba_number | IBANumber is not a positive number |
invalid_status | RecordStatus is not one of the three values |
invalid_minutes | TotalMinutes is not a non-negative number |
invalid_timestamp | FlightDateTime has no offset or does not parse |
unknown_member | No member has that IBA number |
storage_error | Valid, 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;| Situation | Written by | status / error_msg |
|---|---|---|
| Rejected before the handler (auth, envelope) | shared/middlewares/req-validate.js | fail with the message returned |
| Reached the handler | shared/middlewares/audit-history.js, after the response is sent | success, 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; includescart_idandcustomer_idconnector.audit_history_failed— the history row could not be writtenconnector.req_validate.insert_history_failed— the same, on the validation path
Known limitations
- 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.
- Rejection reasons are not stored.
connector_history.error_msgholds 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. - Large deliveries lose their history row.
connector_history.bodyisTEXT(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 aconnector.audit_history_failedlog line remains. - The audit middleware’s comment overstates it. It says a handler that
forgets to set its audit status produces an
unknownrow; the code writes nothing. This is the mechanism behind limitation 1. - Personal data in history. Every history row keeps the whole delivery — names, emails and phone numbers — with no retention limit.
- No admin view. The admin connector report
(
/admin/reports/external/:type/history) acceptsaccordion, but the admin UI offers only Convergence and Fuse-Metrix. SQL is the only way to see the history. - The data is stored, not used. Nothing member-facing reads
connector_accordion_bookingyet. The repository has agetBookingsByMemberquery for a future staff view, with no route.
See also
- Business logic: Accordion integration rules — what is accepted, how cancellations and unmapped tunnels behave, and the product and staff sharecode list
- FuzeMetrix — the same HMAC scheme and shared connector middleware