{
  "openapi": "3.0.3",
  "info": {
    "title": "The Leedz Marketplace API",
    "description": "The Leedz is a marketplace where service vendors buy and sell booking opportunities (called leedz). AI agents can discover available gigs by trade category and location on behalf of their human principals. Two access methods: (1) HTTP API endpoints listed under 'paths' -- public endpoints require no authentication, authenticated endpoints require a JWT session token. (2) Email agent endpoints listed under 'x-email-endpoints' -- send an email to trigger actions and receive HTML email responses. Contact grow@theleedz.com for agent access.",
    "version": "1.0.0",
    "contact": {
      "name": "The Leedz",
      "email": "grow@theleedz.com",
      "url": "https://www.theleedz.com"
    }
  },
  "x-email-endpoints": {
    "findLeedz": {
      "address": "find@theleedz.com",
      "summary": "Find available leedz via email",
      "description": "Send an email to find@theleedz.com to receive available leedz. Authentication is automatic via the sender address. The system looks up the sender's profile and returns leedz matching their trade subscriptions and location preferences. Agents can override search parameters via the subject line. If the sender has no profile or no subscriptions, the response contains the most recently posted leedz across all trades. A new user account is created automatically if one does not exist. Subject line parameters are persisted to the user's profile for future searches.",
      "protocol": "SMTP via AWS SES",
      "authentication": "Sender email address (automatic)",
      "request": {
        "to": "find@theleedz.com",
        "from": "The agent's or user's email address. This address is used to look up the user profile and determines what results are returned.",
        "subject": "Optional. Space-delimited key=value pairs to override search parameters. Format: sb=<trades> zp=<zip> zr=<radius>. Empty subject uses existing profile preferences.",
        "body": "Ignored. Can be anything or empty.",
        "subject_parameters": {
          "sb": {
            "description": "Comma-separated trade names to search. Must be exact matches from the getTrades endpoint (e.g. 'DJ', 'Photographer', 'Caterer'). Invalid trade names return an error email.",
            "required": false,
            "example": "sb=DJ,Photographer"
          },
          "zp": {
            "description": "5-digit US zip code for location filtering.",
            "required": false,
            "validation": "Exactly 5 digits",
            "example": "zp=90210"
          },
          "zr": {
            "description": "Search radius in miles from zip code. Requires zp to be useful.",
            "required": false,
            "validation": "Integer 1-100",
            "example": "zr=25"
          }
        }
      },
      "response": {
        "format": "HTML email sent back to the sender address",
        "cases": {
          "results": "HTML email with leed cards. Each card shows title, trade, date, zip, and price. Each card includes a magic-link button: free leedz link to the dashboard, paid leedz link to a detail/purchase page.",
          "no_results": "HTML email indicating no leedz were found for the search criteria. Includes a link to the dashboard to update preferences.",
          "error": "HTML email with a specific error message. Sent on validation failures (invalid trade name, bad zip format, radius out of range) or unexpected server errors."
        }
      },
      "behavior": {
        "with_subject_params": "If the subject line contains valid sb/zp/zr parameters, these values override the profile and are used for the search. The parameters are also persisted to the user's profile so future empty-subject searches use these preferences.",
        "with_profile": "If no subject params and the sender has trade subscriptions (sb field), the system invokes getLeedz with sb, zp, and zr from their profile.",
        "without_profile": "If no subject params and no subscriptions, the system returns the latest leedz across all trades, sorted by date posted (dp) descending, limited to MAX_RESULTS (configurable, default 10)."
      },
      "example_usage": [
        {
          "description": "Agent searches for DJ gigs near a zip code.",
          "to": "find@theleedz.com",
          "from": "agent-user@gmail.com",
          "subject": "sb=DJ zp=90210 zr=25",
          "body": "",
          "response_description": "HTML email with DJ leedz within 25 miles of 90210. Profile updated with these preferences."
        },
        {
          "description": "Agent uses existing profile preferences (empty subject).",
          "to": "find@theleedz.com",
          "from": "agent-user@gmail.com",
          "subject": "",
          "body": "",
          "response_description": "HTML email with leedz matching the user's saved profile, or latest leedz if no profile."
        },
        {
          "description": "Agent searches multiple trades without location filter.",
          "to": "find@theleedz.com",
          "from": "agent-user@gmail.com",
          "subject": "sb=DJ,MC,Lighting",
          "body": "",
          "response_description": "HTML email with leedz across DJ, MC, and Lighting trades. No location filtering applied."
        }
      ]
    },
    "shareLeedz": {
      "address": "share@theleedz.com",
      "summary": "Create a leed by forwarding a booking inquiry email",
      "description": "Forward a booking inquiry email to share@theleedz.com to automatically create a leed on the marketplace. The system retrieves the raw email from S3, sends it to an LLM to extract booking fields (title, trade, location, date/time, client info), and invokes addLeed to create the leed. The sender receives a success email with full leed details and an edit button, or a failure email with a dashboard link if parsing fails.",
      "protocol": "SMTP via AWS SES",
      "authentication": "Sender email address (automatic). Sender becomes the leed creator.",
      "request": {
        "to": "share@theleedz.com",
        "from": "The vendor/user's email address. This becomes the leed creator (cr field). Must NOT be extracted as the client.",
        "subject": "Optional key=value parameters separated by commas. Fwd:/Re: prefixes are stripped before parsing. Keys are case-insensitive. Parameters: price (aliases: pr) — leed price in dollars, e.g. price=12.99 or price=$5 (converted to cents internally; minimum $1.00 if non-zero, maximum $100.00; defaults to 0/free); share (aliases: sh) — distribution mode: '*' broadcasts to all trade subscribers, 'friends' expands the sender's saved friends list (fr field), a comma-separated list of emails shares to those addresses only, or combinations like '*,alice@x.com'. Defaults to '*'. Non-numeric price values are silently ignored (defaults to 0). Unknown keys are silently ignored. If no parseable params, defaults apply (price=0, share=*).",
        "body": "Forwarded email conversation between the vendor (sender) and a client. The LLM extracts booking details from the full conversation thread including forwarding headers."
      },
      "response": {
        "format": "HTML email sent back to the sender address",
        "cases": {
          "success": "HTML email with full leed details (trade, title, location, date/time, client info, price). Includes a magic-link 'Edit Leed' button that redirects to the editLeed page for the newly created leed.",
          "failure": "HTML email explaining why parsing failed (e.g., missing date, no booking detected, unknown trade, price out of range, Square not authorized). Includes a magic-link 'Your Dashboard' button to create a leed manually."
        }
      },
      "example_usage": [
        {
          "description": "Vendor forwards a booking inquiry. No subject params — leed is created free and broadcast to all trade subscribers.",
          "to": "share@theleedz.com",
          "from": "vendor@gmail.com",
          "subject": "Fwd: DJ for wedding reception?",
          "body": "---------- Forwarded message ----------\nFrom: Jane Smith <jane@example.com>\nDate: Mon, Feb 10, 2026\nSubject: DJ for wedding reception?\nTo: vendor@gmail.com\n\nHi, we need a DJ for our wedding April 5th at The Grand Ballroom, 456 Oak St, Pasadena CA 91101. 6pm-11pm.\n\nThanks,\nJane Smith\n(555) 123-4567",
          "response_description": "Success email: trade=DJ, title='Wedding Reception DJ', location='456 Oak St, Pasadena CA 91101', start=April 5 2026 6:00 PM, end=11:00 PM, client=Jane Smith, price=$0 (free). Broadcast to all DJ subscribers."
        },
        {
          "description": "Vendor sets a price and shares only with their saved friends list.",
          "to": "share@theleedz.com",
          "from": "vendor@gmail.com",
          "subject": "Fwd: Need photographer, price=14.99, share=friends",
          "body": "---------- Forwarded message ----------\nFrom: Tom Lee <tom@example.com>\nDate: Tue, Feb 11, 2026\nSubject: Need photographer\nTo: vendor@gmail.com\n\nLooking for a photographer for a corporate event March 15th, 9am-1pm at 789 Wilshire Blvd, Los Angeles CA 90017.\n\nTom Lee, (310) 555-9988",
          "response_description": "Success email: trade=Photographer, price=$14.99, share=friends list only. If sender has no friends saved, falls back to broadcast with a note."
        },
        {
          "description": "Agent shares a leed to specific vendor emails, seeding a friends list.",
          "to": "share@theleedz.com",
          "from": "agent-vendor@gmail.com",
          "subject": "price=9.99, share=alice@dj.com,bob@photo.com",
          "body": "Client inquiry: Face painting for kids birthday party, March 22 2026 2pm-5pm, 123 Main St, Burbank CA 91505. Contact: Mary Jones, mary@jones.com, (818) 555-1234.",
          "response_description": "Success email: trade=Face Painter, price=$9.99. Leed sent only to alice@dj.com and bob@photo.com. Both emails added to sender's friends list (fr field) permanently."
        }
      ]
    }
  },
  "servers": [
    {
      "url": "https://jjz8op6uy4.execute-api.us-west-2.amazonaws.com/Leedz_Stage_1",
      "description": "Production API"
    }
  ],
  "paths": {
    "/getTrades": {
      "get": {
        "operationId": "getTrades",
        "summary": "Get all trade categories",
        "description": "Returns all 35 service trade categories with current leed counts. Sorted by number of leedz descending. Colors are deterministic hex values assigned after sort.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Array of trade objects sorted by leed count descending",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Trade"
                  }
                },
                "example": [
                  {"sk": "DJ", "nl": 47, "cs": "#ff5700"},
                  {"sk": "Photographer", "nl": 23, "cs": "#a0d700"},
                  {"sk": "Caterer", "nl": 31, "cs": "#00d757"}
                ]
              }
            }
          }
        }
      }
    },
    "/getStats": {
      "get": {
        "operationId": "getStats",
        "summary": "Get platform statistics",
        "description": "Returns platform-wide aggregate statistics from the stats record in DynamoDB.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Stats object with platform-wide counts",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Stats"
                }
              }
            }
          }
        }
      }
    },
    "/getLeedz": {
      "get": {
        "operationId": "getLeedz",
        "summary": "Search marketplace for leedz",
        "description": "Search for available leedz by trade subscriptions and optional location filter. Two mutually exclusive modes: Mode 1 (marketplace discovery) uses 'sb' parameter for trade subscriptions. Mode 2 (my leedz) uses 'cr' parameter for creator email. Provide sb OR cr, not both.",
        "parameters": [
          {
            "name": "sb",
            "in": "query",
            "description": "Comma-separated trade names for marketplace search (e.g. 'DJ,MC,Lighting'). Mutually exclusive with 'cr'.",
            "required": false,
            "schema": {
              "type": "string"
            },
            "example": "DJ,MC"
          },
          {
            "name": "cr",
            "in": "query",
            "description": "Creator email for dashboard view of user's own leedz. Mutually exclusive with 'sb'.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "st",
            "in": "query",
            "description": "Start time filter in epoch milliseconds (13 digits). Leedz with start time >= this value are returned.",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 1737584740000
          },
          {
            "name": "et",
            "in": "query",
            "description": "End time filter in epoch milliseconds (13 digits). Leedz with start time <= this value are returned.",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "example": 1753136740000
          },
          {
            "name": "zp",
            "in": "query",
            "description": "5-digit US zip code for location filtering. Only used with 'sb' mode. Without 'zr', returns exact zip matches only.",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{5}$"
            },
            "example": "90034"
          },
          {
            "name": "zr",
            "in": "query",
            "description": "Search radius in miles from zip code. Requires 'zp'. Uses haversine formula on stored coordinates.",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            },
            "example": 25
          }
        ],
        "responses": {
          "200": {
            "description": "Array of leed objects matching search criteria",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Leed"
                  }
                },
                "example": [
                  {
                    "pk": "leed#DJ",
                    "sk": "1737584740000",
                    "ti": "Wedding DJ needed",
                    "st": 1737584740000,
                    "et": 1737598340000,
                    "zp": "90405",
                    "cr": "vendor@example.com",
                    "pr": 5000,
                    "bn": "",
                    "db": 0
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/showUserPage": {
      "get": {
        "operationId": "showUserPage",
        "summary": "View public user profile",
        "description": "Returns an HTML page showing the user's public profile including username, badges, trades, and stats. Note: returns HTML, not JSON.",
        "parameters": [
          {
            "name": "email",
            "in": "query",
            "description": "User email address",
            "required": true,
            "schema": {
              "type": "string",
              "format": "email"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "HTML page with user profile",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "User not found"
          }
        }
      }
    },
    "/getUser": {
      "get": {
        "operationId": "getUser",
        "summary": "Get authenticated user profile (JSON)",
        "description": "Returns the authenticated user's full profile as JSON, including Square OAuth status. Requires JWT session token. The user email is extracted from the validated JWT -- no email parameter needed.",
        "security": [
          {
            "sessionToken": []
          }
        ],
        "parameters": [
          {
            "name": "session",
            "in": "query",
            "description": "JWT session token obtained via magic link flow",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "User profile JSON object",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          },
          "401": {
            "description": "Authentication failed -- missing or invalid session token"
          },
          "204": {
            "description": "User not found"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "sessionToken": {
        "type": "apiKey",
        "in": "query",
        "name": "session",
        "description": "JWT session token. Obtain by calling the magicLink function URL with ?email=YOUR_EMAIL, then following the magic_url from the response. The session token is valid for 30 days. Contact grow@theleedz.com for agent access."
      }
    },
    "schemas": {
      "Trade": {
        "type": "object",
        "properties": {
          "sk": {
            "type": "string",
            "description": "Trade name (e.g. 'DJ', 'Photographer', 'Caterer')"
          },
          "nl": {
            "type": "integer",
            "description": "Number of active leedz in this trade"
          },
          "cs": {
            "type": "string",
            "description": "Hex color code (deterministic, assigned after sort)",
            "pattern": "^#[0-9a-f]{6}$"
          }
        },
        "required": ["sk", "nl", "cs"]
      },
      "Stats": {
        "type": "object",
        "properties": {
          "pk": {
            "type": "string",
            "description": "Partition key, always 'stats'"
          },
          "sk": {
            "type": "string",
            "description": "Sort key, always 'leedz'"
          }
        },
        "additionalProperties": true,
        "description": "Platform-wide aggregate statistics. Contains counts that vary as the platform evolves."
      },
      "Leed": {
        "type": "object",
        "properties": {
          "pk": {
            "type": "string",
            "description": "Partition key: 'leed#TRADENAME' (e.g. 'leed#DJ')"
          },
          "sk": {
            "type": "string",
            "description": "Leed ID (epoch milliseconds when created)"
          },
          "ti": {
            "type": "string",
            "description": "Title of the leed"
          },
          "st": {
            "type": "integer",
            "format": "int64",
            "description": "Start time of the event (epoch milliseconds)"
          },
          "et": {
            "type": "integer",
            "format": "int64",
            "description": "End time of the event (epoch milliseconds)"
          },
          "zp": {
            "type": "string",
            "description": "5-digit zip code of the event location"
          },
          "cr": {
            "type": "string",
            "description": "Creator email address"
          },
          "pr": {
            "type": "integer",
            "description": "Price in cents (0 = free, 5000 = $50.00, minimum paid = 100, maximum = 10000)"
          },
          "bn": {
            "type": "string",
            "description": "Buyer email address. Empty string if the leed has not been purchased."
          },
          "db": {
            "type": "number",
            "description": "Date bought (epoch ms). 0 if unsold."
          }
        },
        "required": ["pk", "sk", "st", "cr", "pr"]
      },
      "User": {
        "type": "object",
        "properties": {
          "pk": {
            "type": "string",
            "description": "Partition key, always 'user'"
          },
          "sk": {
            "type": "string",
            "description": "User email address"
          },
          "nm": {
            "type": "string",
            "description": "Display name"
          },
          "sb": {
            "type": "string",
            "description": "Comma-separated trade subscriptions (e.g. 'DJ,MC,Lighting')"
          },
          "zp": {
            "type": "string",
            "description": "Home zip code"
          },
          "zr": {
            "type": "string",
            "description": "Search radius in miles"
          },
          "bg": {
            "type": "string",
            "description": "Comma-separated badge IDs (e.g. '0,1,2')"
          },
          "lb": {
            "type": "number",
            "description": "Leedz bought count"
          },
          "lp": {
            "type": "number",
            "description": "Leedz posted/shared count"
          },
          "ls": {
            "type": "number",
            "description": "Leedz sold count"
          },
          "sq_st": {
            "type": "string",
            "description": "Square OAuth status. 'authorized' if Square is connected, otherwise contains OAuth URL."
          }
        },
        "required": ["pk", "sk"]
      }
    }
  }
}