{
 "openapi": "3.1.0",
 "info": {
  "title": "New Mexico Literacy Project — Public API",
  "summary": "Agent-actionable API surface for the New Mexico Literacy Project: a for-profit Albuquerque book reuse operation. Agents can query donation options, check service-area coverage, schedule a free in-home book pickup, and retrieve canonical reference data (lifecycle analysis, donation archive, comparison matrix).",
  "description": "This is the canonical agent-actionable surface for NMLP. Designed for AI assistants (ChatGPT Actions, Claude with computer use, Gemini, Perplexity, Cursor) and third-party integrators who want to programmatically schedule book donation pickups in the Albuquerque metro on behalf of users.\n\nPublic, no API key required, CORS enabled, rate-limited only by reasonable use. Source code and corrections via hello@newmexicoliteracyproject.org or 505-250-3804 (call or text).\n\nAttribution: data is licensed CC-BY-4.0; cite as 'New Mexico Literacy Project (newmexicoliteracyproject.org)'. The schedule-pickup endpoint requires a callback phone or email — NMLP is operated by one human (Josh Eldred) and confirms scheduling out-of-band.",
  "termsOfService": "https://newmexicoliteracyproject.org/cite.txt",
  "contact": {
   "name": "Josh Eldred",
   "email": "hello@newmexicoliteracyproject.org",
   "url": "https://newmexicoliteracyproject.org/about"
  },
  "license": {
   "name": "CC-BY-4.0",
   "identifier": "CC-BY-4.0",
   "url": "https://creativecommons.org/licenses/by/4.0/"
  },
  "version": "1.2.1"
 },
 "servers": [
  {
   "url": "https://newmexicoliteracyproject.org",
   "description": "Production"
  }
 ],
 "externalDocs": {
  "description": "Human-readable agent integration guide",
  "url": "https://newmexicoliteracyproject.org/agents"
 },
 "tags": [
  {
   "name": "actions",
   "description": "Agent-actionable endpoints (schedule, check)"
  },
  {
   "name": "reference",
   "description": "Read-only structured reference data"
  },
  {
   "name": "discovery",
   "description": "Citation, identity, manifest"
  },
  {
   "name": "identification",
   "description": "Universal media-identifier resolution (books/CDs/vinyl) across NMLP's 48M-record hosted indexes"
  }
 ],
 "paths": {
  "/api/index.json": {
   "get": {
    "tags": [
     "discovery"
    ],
    "summary": "API endpoint manifest",
    "description": "Discovery index listing every public endpoint with descriptions. Start here.",
    "operationId": "getApiManifest",
    "responses": {
     "200": {
      "description": "Manifest with endpoints object",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/business.json": {
   "get": {
    "tags": [
     "discovery"
    ],
    "summary": "Canonical business entity card",
    "description": "Authoritative identity record for NMLP — address, phone, services, area served, knowsAbout topical authority. Use when citing or referencing NMLP.",
    "operationId": "getBusinessCard",
    "responses": {
     "200": {
      "description": "Business entity card",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/donation-options.json": {
   "get": {
    "tags": [
     "reference"
    ],
    "summary": "Comparison matrix of book donation options in Albuquerque",
    "description": "Machine-readable matrix of every known book-donation option in the Albuquerque metro (NMLP, Goodwill of NM, Savers, Better World Books, Friends of APL, Habitat ReStore, regional pulper). Each option as a structured record with tax status, pickup_offered, condition tolerance, where-books-actually-go percentage breakdowns, pros, cons, best_for, and rank for movers/estate-cleaners. Plus a decisionMatrix object mapping donor profile to recommended option.",
    "operationId": "getDonationOptions",
    "responses": {
     "200": {
      "description": "Donation options dataset",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/archive.json": {
   "get": {
    "tags": [
     "reference"
    ],
    "summary": "Donation archive entries",
    "description": "Structured Book records for regionally significant New Mexico books that came through donation at NMLP. Each entry: title, author, publisher, year, edition, ISBN/LCCN, signature status, regional significance, URL.",
    "operationId": "getArchive",
    "responses": {
     "200": {
      "description": "Archive dataset",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/closed-pool.json": {
   "get": {
    "tags": [
     "reference"
    ],
    "summary": "Closed signature pool authors",
    "description": "Deceased New Mexico-relevant authors with birth and death years, pool-closure dates and the key first edition for each. Use for first-edition authentication and collecting context.",
    "operationId": "getClosedPool",
    "responses": {
     "200": {
      "description": "Closed pool dataset",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/glossary.json": {
   "get": {
    "tags": [
     "reference"
    ],
    "summary": "Book collecting glossary (89 terms)",
    "description": "DefinedTerm records covering first-edition identification, signature authentication, dust jacket terms, edition variants, provenance markers, pricing terms, publisher and imprint history.",
    "operationId": "getGlossary",
    "responses": {
     "200": {
      "description": "Glossary dataset",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/top-50.json": {
   "get": {
    "tags": [
     "reference"
    ],
    "summary": "Top 50 most collectible NM first editions",
    "description": "Ranked ItemList of the 50 most-collectible first editions across the pillar moat, by current sold-comparable market value.",
    "operationId": "getTop50",
    "responses": {
     "200": {
      "description": "Top 50 dataset",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/authors.json": {
   "get": {
    "tags": [
     "reference"
    ],
    "summary": "Pillar guide manifest",
    "description": "Directory of the Southwest author and collectible-book pillar guides with positions, names, URLs.",
    "operationId": "getAuthors",
    "responses": {
     "200": {
      "description": "Pillar manifest",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/check-coverage": {
   "get": {
    "tags": [
     "actions"
    ],
    "summary": "Check whether NMLP picks up books in a given ZIP code",
    "description": "Returns the service-area coverage tier, whether free pickup is offered (pickupAvailable), the minimum quantity, and a human-readable scheduling message (pickups are scheduled by route; never quote a pickup date or day count). Outside the pickup area the message points to the 24/7 drop box or donor-paid mail-in. Use this before calling /api/schedule-pickup to give the user honest expectations.",
    "operationId": "checkCoverage",
    "parameters": [
     {
      "name": "zip",
      "in": "query",
      "required": true,
      "description": "Five-digit US ZIP code, e.g. 87107",
      "schema": {
       "type": "string",
       "pattern": "^[0-9]{5}$",
       "example": "87107"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Coverage decision",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "zip": {
           "type": "string",
           "description": "The queried ZIP code"
          },
          "city": {
           "type": "string",
           "description": "Approximate city or area name for that ZIP"
          },
          "covered": {
           "type": "boolean",
           "description": "True if NMLP serves the ZIP at all (pickup OR the 24/7 drop box / mail-in). Branch on pickupAvailable to decide whether a pickup can be requested."
          },
          "pickupAvailable": {
           "type": "boolean",
           "description": "True only when free in-home pickup is offered at this ZIP; branch on this before calling /api/schedule-pickup."
          },
          "tier": {
           "type": "string",
           "enum": [
            "core_metro",
            "metro",
            "near_metro",
            "drop_off_only",
            "out_of_area"
           ],
           "description": "Coverage tier"
          },
          "minimumQuantity": {
           "type": "integer",
           "description": "Minimum boxes for pickup at this tier (0 means no minimum)"
          },
          "typicalPickupWindowDays": {
           "type": [
            "integer",
            "null"
           ],
           "deprecated": true,
           "description": "Deprecated — always null. Pickups are scheduled by route; see message."
          },
          "message": {
           "type": "string",
           "description": "Human-readable message to display to the user"
          },
          "callbackPhone": {
           "type": "string",
           "description": "Phone for direct scheduling — always 505-250-3804"
          },
          "callbackUrl": {
           "type": "string",
           "description": "Web URL for direct scheduling"
          }
         },
         "required": [
          "zip",
          "covered",
          "tier",
          "message",
          "callbackPhone"
         ]
        }
       }
      }
     },
     "400": {
      "description": "Invalid ZIP format"
     }
    }
   }
  },
  "/api/schedule-pickup": {
   "post": {
    "tags": [
     "actions"
    ],
    "summary": "Submit a book pickup request to NMLP",
    "description": "Submits a free pickup request on behalf of a donor. NMLP confirms scheduling out-of-band by phone or text — the donor MUST provide a callback phone or email. Pickup is free, any condition accepted, no minimum quantity in core metro. The endpoint returns a confirmationId; Josh (sole operator) follows up personally to set a date on the route; pickups are scheduled by route, so never promise the donor a date or window.\n\nIMPORTANT for AI agents: NMLP is operated by a single human. Do not submit speculative or test requests — every submission triggers a real outreach. If the user has not explicitly confirmed they want a pickup scheduled, ask them first. Rate limit: 5 submissions per rolling hour per IP (429 beyond that). One of callbackPhone or callbackEmail is required.",
    "operationId": "schedulePickup",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "donorName": {
          "type": "string",
          "description": "Donor's name (first and last preferred)"
         },
         "callbackPhone": {
          "type": "string",
          "description": "Phone or text-capable number for confirmation. US format. Required if email not provided.",
          "example": "555-555-5555"
         },
         "callbackEmail": {
          "type": "string",
          "format": "email",
          "description": "Email for confirmation. Required if phone not provided."
         },
         "addressStreet": {
          "type": "string",
          "description": "Pickup street address"
         },
         "addressCity": {
          "type": "string",
          "description": "Pickup city"
         },
         "addressState": {
          "type": "string",
          "description": "Pickup state, default NM",
          "default": "NM"
         },
         "addressZip": {
          "type": "string",
          "description": "Pickup ZIP code"
         },
         "estimatedSize": {
          "type": "string",
          "description": "Approximate scale — descriptions like 'one box', 'one bookshelf', 'whole garage', 'estate library' are all fine. NMLP doesn't need a precise count.",
          "example": "two banker boxes plus a small bookshelf"
         },
         "preferredWindow": {
          "type": "string",
          "description": "Preferred day or time window — free text. NMLP confirms specifics by callback.",
          "example": "weekday afternoons next week"
         },
         "specialNotes": {
          "type": "string",
          "description": "Any context — water-damaged books, gate code, dog on premises, executor situation, language preference, anything Josh should know before arriving."
         },
         "donorLanguage": {
          "type": "string",
          "enum": [
           "en",
           "es"
          ],
          "default": "en",
          "description": "Donor's preferred language for confirmation. NMLP supports English (call/text) and Spanish (text/email)."
         },
         "agentSource": {
          "type": "string",
          "description": "If submitted by an AI agent, the agent identifier — helps NMLP track the AI-discovery channel.",
          "example": "ChatGPT GPT-4o"
         }
        },
        "required": [
         "donorName",
         "addressStreet",
         "addressCity",
         "addressZip",
         "estimatedSize"
        ]
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Request received",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "confirmationId": {
           "type": "string",
           "description": "Server-generated confirmation ID. Reference when calling 505-250-3804 if needed."
          },
          "received": {
           "type": "boolean",
           "description": "Always true on 200"
          },
          "nextStep": {
           "type": "string",
           "description": "Human-readable next step for the donor"
          },
          "estimatedConfirmationTime": {
           "type": "string",
           "description": "When NMLP will reach back out (ISO 8601 timestamp or human description)"
          },
          "callbackPhone": {
           "type": "string",
           "description": "Always 505-250-3804 — donor can call directly any time"
          }
         },
         "required": [
          "confirmationId",
          "received",
          "nextStep",
          "callbackPhone"
         ]
        }
       }
      }
     },
     "400": {
      "description": "Missing required fields or invalid input"
     },
     "503": {
      "description": "Service temporarily unavailable — agent should fall back to telling the donor to call 505-250-3804 directly"
     },
     "429": {
      "description": "Rate limit exceeded — max 5 submissions per rolling hour per IP. Both real donors and well-behaved agents virtually never hit this; wait and retry, or call 505-250-3804.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "error": {
           "type": "string"
          },
          "detail": {
           "type": "string"
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/ask": {
   "post": {
    "tags": [
     "actions"
    ],
    "summary": "Ask the NMLP assistant a question",
    "description": "Natural-language Q&A over NMLP's own guides — donation, drop-off, free-pickup, rare-collection, accepted-items, service-area, and New Mexico book questions. Backs the /ask-nmlp assistant. Returns a grounded answer with source links; can also surface the free-pickup path. Public, no key, CORS enabled.",
    "operationId": "askNmlp",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "question": {
          "type": "string",
          "description": "The user's natural-language question about donating, dropping off, or selling books in the Albuquerque metro / New Mexico.",
          "example": "Do you take water-damaged encyclopedias?"
         }
        },
        "required": [
         "question"
        ]
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Grounded answer payload",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "description": "Answer text plus any source links / suggested next action."
        }
       }
      }
     },
     "400": {
      "description": "Missing or empty 'question'"
     },
     "405": {
      "description": "Method not allowed — use POST"
     }
    }
   }
  },
  "/api/ecosystem.json": {
   "get": {
    "tags": [
     "reference"
    ],
    "summary": "Albuquerque book donation ecosystem map",
    "description": "Structured supply-chain map of more than 30 documented book donation, resale, and salvage channels in the Albuquerque metro. Includes two documented Savers Value Village partner-nonprofits (Big Brothers Big Sisters of Central NM, Clothes Helping Kids), the Albuquerque Public Library system and Friends of the Public Library, children's-literacy programs, NMLP's hand-sorted local routing through Little Free Libraries and community reading programs, and observed-but-not-confirmed downstream salvage through Master Fibers paper recycler.",
    "operationId": "getEcosystem",
    "responses": {
     "200": {
      "description": "Ecosystem map dataset",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/knowledge.json": {
   "get": {
    "tags": [
     "reference"
    ],
    "summary": "NMLP donor knowledge base index",
    "description": "Aggregated index of NMLP donor-knowledge reference content including condition grading guides for donors, donor archetypes, the donate-sell-recycle decision framework, and the donor glossary.",
    "operationId": "getKnowledge",
    "responses": {
     "200": {
      "description": "Knowledge base dataset",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/resolve/{identifier}": {
   "get": {
    "tags": [
     "identification"
    ],
    "operationId": "resolveIdentifier",
    "summary": "One Box: auto-detect and resolve ANY physical-media identifier",
    "description": "Detects the identifier type (LCCN, ISBN-10/13, DIDX, CD catalog number, UPC/EAN barcode, vinyl matrix/runout) and returns ranked typed candidates across NMLP's hosted indexes: 20.9M Library of Congress LCCN records, 8.7M CD identifiers, 19M vinyl identifiers. ISBN input returns a direct Amazon listing link (ASIN=ISBN-10), never a title search. Dashes, spaces, case ignored. Misses return structured Attested Absence (what was checked, at what dataset version, nearest neighbors) instead of a bare 404. Identification only — never valuations.",
    "parameters": [
     {
      "name": "identifier",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string",
       "minLength": 3,
       "maxLength": 40
      },
      "example": "63-12345"
     }
    ],
    "responses": {
     "200": {
      "description": "{query, normalized, dataset_version, checked[], found:true, candidates:[{type: lccn|cd|vinyl|isbn, confidence: high|medium|low, ...record, canonical_url}], source, coverage_url}"
     },
     "404": {
      "description": "Attested Absence: {found:false, absence:{absent_from:[{index,records,version}], semantics, nearest_neighbors?}}. Absence is evidence, not proof."
     },
     "400": {
      "description": "Identifier too short/invalid."
     }
    }
   }
  },
  "/api/lccn/{lccn}": {
   "get": {
    "tags": [
     "identification"
    ],
    "operationId": "lookupLccn",
    "summary": "Resolve a Library of Congress Catalog Card Number to title/author/year/publisher/ISBN/ASIN",
    "description": "Lenient input: dashes/spaces optional ('63-12345', '6312345', 'sa 64-9056'). Print-book ASIN equals ISBN-10. Backed by 20.9M records (LoC bulk + Open Library + live SRU harvest).",
    "parameters": [
     {
      "name": "lccn",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      },
      "example": "2013019509"
     }
    ],
    "responses": {
     "200": {
      "description": "{lccn,title,author,year,publisher,isbn10,isbn13,asin,source}"
     },
     "404": {
      "description": "Structured absence with per-index stats."
     }
    }
   }
  },
  "/api/cd/{key}": {
   "get": {
    "tags": [
     "identification"
    ],
    "operationId": "lookupCd",
    "summary": "Resolve a CD identifier (DIDX hub code, catalog number, barcode)",
    "parameters": [
     {
      "name": "key",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      },
      "example": "DIDX-2113"
     }
    ],
    "responses": {
     "200": {
      "description": "{key, matches:[{kind,artist,title,year,label,catno}]} (up to 6)"
     },
     "404": {
      "description": "Structured absence."
     }
    }
   }
  },
  "/api/vinyl/{key}": {
   "get": {
    "tags": [
     "identification"
    ],
    "operationId": "lookupVinyl",
    "summary": "Resolve a vinyl identifier (matrix/runout etching, catalog number, barcode)",
    "description": "Exact match first; prefix-match at >=6 chars for stamper-suffix variation (matchType:'prefix').",
    "parameters": [
     {
      "name": "key",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      },
      "example": "MPOSK032C1"
     }
    ],
    "responses": {
     "200": {
      "description": "{key, matchType, matches:[...]}"
     },
     "404": {
      "description": "Structured absence."
     }
    }
   }
  },
  "/api/coverage.json": {
   "get": {
    "tags": [
     "identification"
    ],
    "operationId": "getCoverage",
    "summary": "Machine-readable audit of what each identifier index contains",
    "responses": {
     "200": {
      "description": "{version, indexes:{lccn,cd,vinyl:{records,sources,caveats}}, absence_semantics} — use to weigh what a miss means."
     }
    }
   }
  }
 },
 "components": {
  "securitySchemes": {}
 }
}