Natural OrderNatural Order API
Back to app →

Natural Order API v1

Build integrations for proximity-based MTG card trading.

18
Endpoints
Bearer
Authentication
JSON
Response Format

Authentication

Generate an API key from your profile page. Include it in every request as a Bearer token.

Authorization: Bearer sk_live_abc123...

Base URL:

https://natural-order.vercel.app/api/v1

Rate Limits

MethodLimit
GET100 requests / minute
POST PATCH DELETE30 requests / minute
Compute matches5 requests / minute

Response Format

All responses return JSON.

Success (single)

{
  "data": { ... }
}

Success (list)

{
  "data": [ ... ],
  "meta": {
    "page": 1,
    "limit": 50,
    "total_count": 128,
    "has_more": true
  }
}

Error

{
  "error": "Descriptive error message"
}

Profile

GET/v1/me

Returns the authenticated user's profile information.

Response

{
  "data": {
    "id": "uuid",
    "email": "user@example.com",
    "display_name": "TraderJoe",
    "preferred_language": "es"
  }
}

Collection

GET/v1/collection

List your collection. Paginated.

ParameterTypeDescription
pageintegerPage number (default: 1)
limitintegerItems per page (default: 50, max: 100)

Response

{
  "data": [
    {
      "id": "uuid",
      "scryfall_id": "abc123",
      "card_name": "Lightning Bolt",
      "quantity": 4,
      "condition": "NM",
      "foil": false,
      "asking_price": 1.20
    }
  ],
  "meta": { "page": 1, "limit": 50, "total_count": 215, "has_more": true }
}
POST/v1/collection

Add a card to your collection.

ParameterTypeDescription
scryfall_id*stringScryfall card ID
quantityintegerDefault: 1
conditionstringNM, LP, MP, HP, DMG (default: NM)
foilbooleanDefault: false
price_modestring"percentage" or "fixed"
price_percentagenumberPercentage of market price (1-200)

Response

{
  "data": {
    "id": "uuid",
    "scryfall_id": "abc123",
    "card_name": "Lightning Bolt",
    "quantity": 4,
    "condition": "NM"
  }
}
PATCH/v1/collection/:id

Update a card in your collection.

ParameterTypeDescription
quantityintegerNew quantity
conditionstringNM, LP, MP, HP, DMG
foilbooleanFoil status
price_modestring"percentage" or "fixed"
price_percentagenumberPercentage of market price
DELETE/v1/collection

Remove a card from your collection.

ParameterTypeDescription
id*stringCollection entry ID to remove

Response

{
  "data": { "deleted": true }
}

Wishlist

GET/v1/wishlist

List your wishlist. Paginated.

ParameterTypeDescription
pageintegerPage number (default: 1)
limitintegerItems per page (default: 50, max: 100)

Response

{
  "data": [
    {
      "id": "uuid",
      "scryfall_id": "abc123",
      "card_name": "Ragavan, Nimble Pilferer",
      "quantity": 1,
      "max_price": 50.00,
      "min_condition": "LP",
      "priority": 8
    }
  ],
  "meta": { "page": 1, "limit": 50, "total_count": 42, "has_more": false }
}
POST/v1/wishlist

Add a card to your wishlist.

ParameterTypeDescription
scryfall_id*stringScryfall card ID
quantityintegerDefault: 1
max_pricenumberMaximum price in USD
min_conditionstringNM, LP, MP, HP, DMG (default: LP)
foil_preferencestring"any", "foil_only", or "non_foil"
priorityinteger1-10 (default: 5)
PATCH/v1/wishlist/:id

Update a wishlist item.

ParameterTypeDescription
quantityintegerNew quantity
max_pricenumberMaximum price in USD
min_conditionstringMinimum acceptable condition
foil_preferencestring"any", "foil_only", or "non_foil"
priorityinteger1-10
DELETE/v1/wishlist

Remove a card from your wishlist.

ParameterTypeDescription
id*stringWishlist entry ID to remove

Card Search

GET/v1/cards/search?q=...

Search for Magic cards by name. Powered by Scryfall data.

ParameterTypeDescription
q*stringSearch query (min 2 characters)

Response

{
  "data": [
    {
      "scryfall_id": "abc123",
      "name": "Lightning Bolt",
      "set_code": "lea",
      "set_name": "Limited Edition Alpha",
      "prices_usd": "385.00",
      "image_uri": "https://cards.scryfall.io/..."
    }
  ]
}

Preferences

GET/v1/preferences

Get your trading preferences.

Response

{
  "data": {
    "trade_mode": "both",
    "default_price_percentage": 80,
    "minimum_price": 0.50,
    "collection_paused": false,
    "price_source": "scryfall"
  }
}
PATCH/v1/preferences

Update your trading preferences.

ParameterTypeDescription
trade_modestring"trade", "sell", "buy", or "both"
default_price_percentagenumberGlobal discount (1-200)
minimum_pricenumberMinimum price in USD
collection_pausedbooleanPause collection visibility
price_sourcestringPrice data source

Matches

GET/v1/matches

List your trading matches. Paginated.

ParameterTypeDescription
pageintegerPage number (default: 1)
limitintegerItems per page (default: 20, max: 50)
statusstringFilter by status (active, requested, confirmed, etc.)

Response

{
  "data": [
    {
      "id": "uuid",
      "counterpart": { "display_name": "CardShark", "avatar_url": "..." },
      "match_type": "two_way",
      "match_score": 72,
      "distance_km": 3.5,
      "cards_you_want": 5,
      "cards_they_want": 3,
      "status": "active"
    }
  ],
  "meta": { "page": 1, "limit": 20, "total_count": 8, "has_more": false }
}
GET/v1/matches/:id

Get full match details including matched cards.

Response

{
  "data": {
    "id": "uuid",
    "counterpart": { "display_name": "CardShark" },
    "match_type": "two_way",
    "status": "active",
    "distance_km": 3.5,
    "cards_you_want": [
      { "card_id": "uuid", "card_name": "Lightning Bolt", "asking_price": 1.20 }
    ],
    "cards_they_want": [
      { "card_id": "uuid", "card_name": "Counterspell", "asking_price": 0.80 }
    ]
  }
}
PATCH/v1/matches/:id

Update match status or favorite flag.

ParameterTypeDescription
statusstring"contacted" or "dismissed"
is_favoritebooleanToggle favorite
POST/v1/matches/compute

Run the matching algorithm. Finds nearby users with compatible collections and wishlists. Rate limited to 5 requests per minute.

Response

{
  "data": {
    "matches_found": 5,
    "new_matches": 3,
    "updated_matches": 2
  }
}

Trade Actions

Trade lifecycle: active requested confirmed completed

POST/v1/matches/:id/request

Request a trade with this match. The other user will be notified.

Response

{
  "data": { "status": "requested", "requested_by": "your-uuid" }
}
DELETE/v1/matches/:id/request

Cancel or reject a pending trade request.

POST/v1/matches/:id/confirm

Confirm the trade. Starts a 15-day escrow period. Cards are reserved from other matches.

Response

{
  "data": {
    "status": "confirmed",
    "confirmed_at": "2026-01-15T10:30:00Z",
    "escrow_expires_at": "2026-01-30T10:30:00Z"
  }
}
POST/v1/matches/:id/complete

Mark the trade as completed (or not).

ParameterTypeDescription
completed*booleantrue to complete, false to cancel

Match Cards

PATCH/v1/matches/:id/cards

Toggle exclusion of a specific card in a match.

ParameterTypeDescription
card_id*stringMatch card ID
is_excluded*booleantrue to exclude, false to include
PUT/v1/matches/:id/cards

Bulk update card exclusions. All cards not in the array will be included.

ParameterTypeDescription
excluded_card_ids*string[]Array of match card IDs to exclude

Response

{
  "data": { "updated": 3 }
}

Comments

GET/v1/matches/:id/comments

List comments on a match. Paginated.

ParameterTypeDescription
pageintegerPage number (default: 1)
limitintegerItems per page (default: 50)

Response

{
  "data": [
    {
      "id": "uuid",
      "content": "Hey, I can meet downtown tomorrow!",
      "author": { "display_name": "TraderJoe" },
      "created_at": "2026-01-15T14:30:00Z"
    }
  ],
  "meta": { "page": 1, "limit": 50, "total_count": 3, "has_more": false }
}
POST/v1/matches/:id/comments

Post a comment on a match. Max 300 characters. Limited to 10 comments per month.

ParameterTypeDescription
content*stringComment text (max 300 chars)

Error Codes

StatusCodeDescription
400Bad RequestInvalid parameters or missing required fields
401UnauthorizedMissing or invalid API key
404Not FoundResource does not exist or is not accessible
409ConflictAction conflicts with current state (e.g., cards in escrow)
429Rate LimitToo many requests. Check Retry-After header
500Server ErrorInternal error. Contact support if persistent

Error response shape

{
  "error": "Missing required field: scryfall_id"
}

Rate Limiting

Rate limit information is included in response headers.

HeaderDescription
X-RateLimit-LimitMaximum requests allowed in the window
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix timestamp when the window resets
Retry-AfterSeconds to wait before retrying (only on 429)

Example 429 response

HTTP/1.1 429 Too Many Requests
Retry-After: 23
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1706112000

{
  "error": "Rate limit exceeded. Try again in 23 seconds."
}

Full Workflow Example

End-to-end example of finding and completing a trade via the API.

  1. 1

    Search for a card

    GET /v1/cards/search?q=Lightning%20Bolt
  2. 2

    Add to your wishlist

    POST /v1/wishlist
    {
      "scryfall_id": "e2d1f9c2-...",
      "max_price": 2.00,
      "priority": 8
    }
  3. 3

    Add cards to your collection

    POST /v1/collection
    {
      "scryfall_id": "f5a2b8e1-...",
      "quantity": 2,
      "condition": "NM"
    }
  4. 4

    Compute matches

    POST /v1/matches/compute
  5. 5

    Review matches

    GET /v1/matches?status=active
  6. 6

    Request a trade

    POST /v1/matches/abc123/request
  7. 7

    Other user confirms the trade

    POST /v1/matches/abc123/confirm
  8. 8

    Mark trade as complete

    POST /v1/matches/abc123/complete
    {
      "completed": true
    }