Debugging
Every 4xx / 5xx response carries either anerror_code (in JSON envelopes) or a WWW-Authenticate reason (on empty-body 401s). Use this table to triage. See Pagination + errors for the full envelope catalogue.
Auth errors
| Code | Where | Cause | Fix |
|---|---|---|---|
MISSING_API_KEY | Discovery 401 (JSON) | No X-API-Key header on /restaurants* or /locations*. | Add -H "X-API-Key: $API_KEY". |
INVALID_API_KEY | Discovery 401 (JSON) | Key expired, revoked, or wrong environment. | Confirm you’re using the key you received for the environment you’re calling. Don’t try to use a staging key against production or vice versa; the prefix isn’t a guarantee, but the server-side binding is. If the key is the right one and still rejected, request a fresh one via Support. |
invalid_token | OAuth 401 (WWW-Authenticate header) | Malformed JWT, expired, or signed by the wrong issuer. | Refresh the token. If refresh also fails, restart the OAuth flow from Step 1. |
insufficient_scope | OAuth 403 (WWW-Authenticate header, empty body) | Token is valid but missing the scope this route needs. The route exists; this is not a 404. | Re-authorize with the required scope. read:profile gates /users/me + /users/me/status; read:checkins gates /check_ins* + /users/me/check_ins; read:wallets gates /users/me/wallets. |
invalid_grant | /oauth/token 400 | Refresh token already used, or authorization code expired/replayed. | Refresh tokens are single-use; the most recent refresh is authoritative. If you replayed a stale one, restart the OAuth flow. |
invalid_client | /oauth/token 400/401 | client_id / client_secret mismatch. | Re-check the credentials in your onboarding email. The secret is backend-only; confirm it’s not getting truncated or surrounded by whitespace. |
Payment errors
| Code | Where | Cause | Fix |
|---|---|---|---|
payment0030 | POST /payment_intents/{id}/confirm 400 | Member doesn’t hold enough FLY. | Check wallet balance before confirm. Surface a “top up” prompt or fall back to a different funding source. |
paymentIntent0003 | confirm / cancel / refund 400 | Wrong-state transition (e.g. confirming an already-canceled intent). | Read the current status before acting. The state machine is one-way: pending → paid → refunded, or pending → canceled, or pending → expired. |
resource_not_found (with flynet_merchant_id) | POST /payment_intents 404 | Merchant id not visible to your partner app. | Confirm the flynet_merchant_id from your onboarding email exactly matches what you’re sending. If it does and you still get 404, route via Support. |
Routing and request-shape errors
The 401 shape depends on the route, not the header you sent
A common gotcha: which 401 envelope you see is determined by the route family’s gating filter, not by which credential you happened to send.| Route you called | Header you sent | What you get back |
|---|---|---|
/restaurants* or /locations* | nothing | 401 JSON MISSING_API_KEY |
/restaurants* or /locations* | invalid X-API-Key | 401 JSON INVALID_API_KEY |
/restaurants* or /locations* | Authorization: Bearer … (wrong scheme) | 401 JSON MISSING_API_KEY (the OAuth bearer is ignored; the route’s filter only knows API keys) |
/users/me/*, /check_ins*, /memberships, /payment_intents/* | nothing | 401 empty body + WWW-Authenticate: Bearer |
/users/me/*, /check_ins*, /memberships, /payment_intents/* | invalid bearer | 401 empty body + WWW-Authenticate: Bearer error="invalid_token"… |
/users/me/*, /check_ins*, /memberships, /payment_intents/* | valid bearer, wrong scope | 403 empty body + WWW-Authenticate: Bearer error="insufficient_scope" |
/users/me/*, /check_ins*, /memberships, /payment_intents/* | X-API-Key: … (wrong scheme) | 401 empty body (the API key is ignored; the route’s filter only knows bearers) |
403 (rather than 401) means the token is valid but lacks the
scope the route needs, not a credential-type problem.
Silent failures worth knowing
These don’t return errors, but they don’t do what you’d expect either.- Unknown query parameters on
/check_insare silently ignored. A wrong name likerestaurant_id=instead ofrestaurant=is treated as no filter supplied — as isuser=, which is no longer a supported filter (the feed is anonymized). Since an unfiltered/check_insis valid, you won’t get an error; you’ll get the full unfiltered set, which is more rows than you expected. /membershipsignores an unrecognized filter param. A typo likerestaurant_id=instead ofrestaurant=returns the unfiltered membership set rather than an error.- Filter params on
/restaurants(cohort,cuisine) and/locations(restaurant,neighborhood,payments_enabled,is_club) are accepted but not implemented server-side. Filter client-side until they ship. Restaurant.cohortis currently a free-form string (the enum was removed). Don’tswitchon a fixed value set.Coordinate { latitude: 0.0, longitude: 0.0 }indicates missing geocoding, not a real point. Filter these out before placing map markers.- Member routes are subject-scoped to the token.
/users/me/*returns the authenticated member’s own data: there’s no UUID in the path and you can’t read another member’s data with the token. The legacy/users/{id}/*routes are gone (they now 404). See OAuth → Step 3 for the model.
Still stuck?
Contact Support and include:- The request URL and your headers (redacted)
- The status code and response body you got
- What you expected
- The time of the call (UTC) so the team can trace it
X-Request-Id header, paste that too.