Skip to main content
Prep time: ~5 minutes
You need: curl, an OAuth access token, and an API key. (/users/me/* resolves the subject from your JWT server-side, so no member ID is needed.)
This is the wire-level tour — three raw calls so you can see exactly what the API returns. When you build, use the TypeScript SDK instead of hand-rolling these requests: @flynetdev/core handles both credentials, OAuth, errors, and pagination, and @flynetdev/react adds drop-in components. Building with an AI agent? Start at Build with AI.

First course: set credentials

Two credentials are not interchangeable. Discovery routes (/restaurants*, /locations*) take the API key. Member-scoped routes (/users/me/*, /check_ins*, /memberships, /payment_intents*) take an OAuth access token. See OAuth for the full flow and API keys for the server-to-server side.
export API_BASE_URL="https://api.staging.blackbird.xyz/flynet/v1"
export API_KEY="bb_test_..."        # from your onboarding email; paste exactly as received
export ACCESS_TOKEN="..."           # from /oauth/token; see OAuth concept
Tasting note - /users/me/* is canonical for member-context calls. The JWT subject is resolved server-side; no UUID parameter needed. Decode the sub claim from your access token only if you need the member’s UUID for your own state.

Second course: list restaurants

The simplest authenticated call. Pure server-to-server: no member context, no OAuth.
curl -sS "$API_BASE_URL/restaurants?page=0&page_size=5" \
  -H "X-API-Key: $API_KEY"
Expected (abbreviated, first restaurant only):
{
  "restaurants": [
    {
      "id": "fa6b9307-3051-4246-9d8a-1565efe7a6c9",
      "object": "restaurant",
      "name": "a16z Cafe",
      "cuisine": [],
      "cohort": "qsr",
      "asset": { "preview_1x": "https://...", "web_2x": "https://...", "full_3x": "https://..." }
    }
  ],
  "pagination": { "total_count": 269, "current_page": 0, "next_page": 1, "page_size": 5 }
}
total_count shifts as the seed list grows. Treat the exact number as illustrative. A 401 from this route is JSON: {"status":401,"message":"...","error_code":"MISSING_API_KEY"}, not the empty-body 401 that OAuth routes return. Discovery routes reject OAuth bearer tokens.

Main course: fetch your wallets

Switches to OAuth. The subject is resolved from your JWT: /users/me/wallets returns the wallets of the token holder, no UUID in the path. Every Blackbird member has a MEMBERSHIP wallet and a SPENDING wallet, auto-provisioned on their first OAuth completion.
curl -sS "$API_BASE_URL/users/me/wallets" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Expected:
{
  "wallets": [
    { "object": "user_wallet", "wallet_type": "MEMBERSHIP", "address": "0x6441FCaBB1bA6b26301e04beC1147E0fF2ee239b" },
    { "object": "user_wallet", "wallet_type": "SPENDING",   "address": "0xecb241bA29D219a753E37bEE253236e2Fbd1Fa45" }
  ]
}
This route needs the read:wallets scope. Wrong scope returns 403 with WWW-Authenticate: Bearer error="insufficient_scope": the route exists, your token just isn’t authorized for it. A 401 (rather than 403) returns an empty body with a WWW-Authenticate: Bearer header: the cause sits in the header, not in JSON.

Final course: your check-in history

Your own visit history, subject resolved from the JWT. Each row embeds the full location, restaurant, and neighborhood, plus visit_number and timestamps: one call powers a complete feed row.
curl -sS "$API_BASE_URL/users/me/check_ins?page=0&page_size=10" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Expected (abbreviated, one row):
{
  "check_ins": [
    {
      "id": "{uuid}",
      "object": "check_in",
      "visit_number": 47,
      "created_at": "2026-04-17T14:31:17Z",
      "ended_at": "2026-04-17T16:02:40Z",
      "location": { "object": "location", "name": "Anton's West Village", "restaurant": { "name": "Anton's" }, "neighborhood": { "name": "West Village" } }
    }
  ],
  "pagination": { "total_count": 562, "current_page": 0, "next_page": 1, "page_size": 10 }
}
This route needs the read:checkins scope. Wrong scope returns 403 with WWW-Authenticate: Bearer error="insufficient_scope": the route still exists, your token just isn’t authorized for it.
Tasting note - Pagination is zero-indexed. Start with page=0. Timestamps are ISO 8601 strings.
Tasting note - The collection route /check_ins accepts optional filters ([restaurant, location, created_after, created_before]) — there is no user filter; the feed is anonymized. A bare GET /check_ins is valid and returns the full set. Unknown query params are silently ignored: a wrong name like restaurant_id= instead of restaurant= is treated as no filter supplied. created_after / created_before accept ISO 8601 only.

Staging fixtures

Working staging UUIDs you can paste into the interactive API playground’s “Try It” buttons. These resources exist in staging seed data and are stable.
ResourceUUIDNotes
User (Ekow)13a014d0-31de-474b-89f9-d9a32b0d42b8562 check-ins; 1 industry tag; MEMBERSHIP + SPENDING wallets. Member routes resolve the subject from your token (/users/me/*), so this UUID is reference only; it’s not a path parameter.
Restaurant (Anton’s)14339db3-2e7a-42c4-aa98-4c0fb18679eb57 check-ins; populated asset
Location (Anton’s West Village)c54a3b6a-c31b-49b4-8af1-2dfb70ff3eec57 check-ins
Staging data may change without notice. If a UUID stops working, ping Support.

Hitting an error?

See Debugging: every observed error_code mapped to root cause + fix.

What’s next

Build with the SDK

The same three calls in typed TypeScript — @flynetdev/core handles credentials, errors, and pagination for you.

Ship with Claude

The full path with an AI agent: build on the SDK, handle secrets safely, deploy on Vercel.

Restaurant explorer

Chain restaurants → locations → weekly hours for a discovery surface.

OAuth, end-to-end

The full Token-Mediated Backend flow with PKCE and refresh rotation.