Natural Order API v1
Build integrations for proximity-based MTG card trading.
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
| Method | Limit |
|---|---|
| GET | 100 requests / minute |
| POST PATCH DELETE | 30 requests / minute |
| Compute matches | 5 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
/v1/meReturns the authenticated user's profile information.
Response
{
"data": {
"id": "uuid",
"email": "user@example.com",
"display_name": "TraderJoe",
"preferred_language": "es"
}
}Collection
/v1/collectionList your collection. Paginated.
| Parameter | Type | Description |
|---|---|---|
| page | integer | Page number (default: 1) |
| limit | integer | Items 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 }
}/v1/collectionAdd a card to your collection.
| Parameter | Type | Description |
|---|---|---|
| scryfall_id* | string | Scryfall card ID |
| quantity | integer | Default: 1 |
| condition | string | NM, LP, MP, HP, DMG (default: NM) |
| foil | boolean | Default: false |
| price_mode | string | "percentage" or "fixed" |
| price_percentage | number | Percentage of market price (1-200) |
Response
{
"data": {
"id": "uuid",
"scryfall_id": "abc123",
"card_name": "Lightning Bolt",
"quantity": 4,
"condition": "NM"
}
}/v1/collection/:idUpdate a card in your collection.
| Parameter | Type | Description |
|---|---|---|
| quantity | integer | New quantity |
| condition | string | NM, LP, MP, HP, DMG |
| foil | boolean | Foil status |
| price_mode | string | "percentage" or "fixed" |
| price_percentage | number | Percentage of market price |
/v1/collectionRemove a card from your collection.
| Parameter | Type | Description |
|---|---|---|
| id* | string | Collection entry ID to remove |
Response
{
"data": { "deleted": true }
}Wishlist
/v1/wishlistList your wishlist. Paginated.
| Parameter | Type | Description |
|---|---|---|
| page | integer | Page number (default: 1) |
| limit | integer | Items 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 }
}/v1/wishlistAdd a card to your wishlist.
| Parameter | Type | Description |
|---|---|---|
| scryfall_id* | string | Scryfall card ID |
| quantity | integer | Default: 1 |
| max_price | number | Maximum price in USD |
| min_condition | string | NM, LP, MP, HP, DMG (default: LP) |
| foil_preference | string | "any", "foil_only", or "non_foil" |
| priority | integer | 1-10 (default: 5) |
/v1/wishlist/:idUpdate a wishlist item.
| Parameter | Type | Description |
|---|---|---|
| quantity | integer | New quantity |
| max_price | number | Maximum price in USD |
| min_condition | string | Minimum acceptable condition |
| foil_preference | string | "any", "foil_only", or "non_foil" |
| priority | integer | 1-10 |
/v1/wishlistRemove a card from your wishlist.
| Parameter | Type | Description |
|---|---|---|
| id* | string | Wishlist entry ID to remove |
Card Search
/v1/cards/search?q=...Search for Magic cards by name. Powered by Scryfall data.
| Parameter | Type | Description |
|---|---|---|
| q* | string | Search 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
/v1/preferencesGet your trading preferences.
Response
{
"data": {
"trade_mode": "both",
"default_price_percentage": 80,
"minimum_price": 0.50,
"collection_paused": false,
"price_source": "scryfall"
}
}/v1/preferencesUpdate your trading preferences.
| Parameter | Type | Description |
|---|---|---|
| trade_mode | string | "trade", "sell", "buy", or "both" |
| default_price_percentage | number | Global discount (1-200) |
| minimum_price | number | Minimum price in USD |
| collection_paused | boolean | Pause collection visibility |
| price_source | string | Price data source |
Matches
/v1/matchesList your trading matches. Paginated.
| Parameter | Type | Description |
|---|---|---|
| page | integer | Page number (default: 1) |
| limit | integer | Items per page (default: 20, max: 50) |
| status | string | Filter 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 }
}/v1/matches/:idGet 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 }
]
}
}/v1/matches/:idUpdate match status or favorite flag.
| Parameter | Type | Description |
|---|---|---|
| status | string | "contacted" or "dismissed" |
| is_favorite | boolean | Toggle favorite |
/v1/matches/computeRun 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
/v1/matches/:id/requestRequest a trade with this match. The other user will be notified.
Response
{
"data": { "status": "requested", "requested_by": "your-uuid" }
}/v1/matches/:id/requestCancel or reject a pending trade request.
/v1/matches/:id/confirmConfirm 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"
}
}/v1/matches/:id/completeMark the trade as completed (or not).
| Parameter | Type | Description |
|---|---|---|
| completed* | boolean | true to complete, false to cancel |
Match Cards
/v1/matches/:id/cardsToggle exclusion of a specific card in a match.
| Parameter | Type | Description |
|---|---|---|
| card_id* | string | Match card ID |
| is_excluded* | boolean | true to exclude, false to include |
/v1/matches/:id/cardsBulk update card exclusions. All cards not in the array will be included.
| Parameter | Type | Description |
|---|---|---|
| excluded_card_ids* | string[] | Array of match card IDs to exclude |
Response
{
"data": { "updated": 3 }
}Comments
/v1/matches/:id/commentsList comments on a match. Paginated.
| Parameter | Type | Description |
|---|---|---|
| page | integer | Page number (default: 1) |
| limit | integer | Items 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 }
}/v1/matches/:id/commentsPost a comment on a match. Max 300 characters. Limited to 10 comments per month.
| Parameter | Type | Description |
|---|---|---|
| content* | string | Comment text (max 300 chars) |
Error Codes
| Status | Code | Description |
|---|---|---|
| 400 | Bad Request | Invalid parameters or missing required fields |
| 401 | Unauthorized | Missing or invalid API key |
| 404 | Not Found | Resource does not exist or is not accessible |
| 409 | Conflict | Action conflicts with current state (e.g., cards in escrow) |
| 429 | Rate Limit | Too many requests. Check Retry-After header |
| 500 | Server Error | Internal error. Contact support if persistent |
Error response shape
{
"error": "Missing required field: scryfall_id"
}Rate Limiting
Rate limit information is included in response headers.
| Header | Description |
|---|---|
| X-RateLimit-Limit | Maximum requests allowed in the window |
| X-RateLimit-Remaining | Requests remaining in the current window |
| X-RateLimit-Reset | Unix timestamp when the window resets |
| Retry-After | Seconds 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
Search for a card
GET /v1/cards/search?q=Lightning%20Bolt
- 2
Add to your wishlist
POST /v1/wishlist { "scryfall_id": "e2d1f9c2-...", "max_price": 2.00, "priority": 8 } - 3
Add cards to your collection
POST /v1/collection { "scryfall_id": "f5a2b8e1-...", "quantity": 2, "condition": "NM" } - 4
Compute matches
POST /v1/matches/compute
- 5
Review matches
GET /v1/matches?status=active
- 6
Request a trade
POST /v1/matches/abc123/request
- 7
Other user confirms the trade
POST /v1/matches/abc123/confirm
- 8
Mark trade as complete
POST /v1/matches/abc123/complete { "completed": true }