{"openapi":"3.1.0","info":{"title":"Spidev public API","version":"1.0.0","summary":"Read-only content API for spidev.ai, plus newsletter and consultation booking.","description":"Spidev is an AI agents & automation agency (Monterrey, Mexico — Spanish and English).\n\nThis API serves the same published content as the website: blog posts, case studies, CMS pages, categories and search.\n\nReads are public and need no credentials. Optionally, agents can register an OAuth client (`POST /oauth/register`, open self-serve registration) and request a scoped bearer token (`POST /oauth/token`, client_credentials) to declare least-privilege intent.\n\nEvery page on the site also negotiates markdown: request any page URL with `Accept: text/markdown` for a clean markdown rendition. The site index for LLMs is at /llms.txt.","contact":{"name":"Spidev","url":"https://spidev.ai/en/start","email":"info@spidev.ai"},"termsOfService":"https://spidev.ai/en/terms-of-service"},"externalDocs":{"description":"Developer & agent documentation","url":"https://spidev.ai/developers"},"servers":[{"url":"https://spidev.ai"}],"security":[{},{"oauth2ClientCredentials":[]}],"tags":[{"name":"content","description":"Published site content (read-only, public)"},{"name":"search","description":"Site search index"},{"name":"graphql","description":"GraphQL endpoint over the same content"},{"name":"forms","description":"Newsletter sign-up and consultation booking"},{"name":"oauth","description":"OAuth 2.0 client registration and token issuance"}],"paths":{"/api/posts":{"get":{"operationId":"listPosts","tags":["content"],"summary":"List published blog posts","description":"Published blog posts, localized. Sort by `-publishedAt` for newest first. The `content` field is Lexical rich text; for readable article text, fetch the post page with `Accept: text/markdown` instead.","parameters":[{"$ref":"#/components/parameters/locale"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/page"},{"$ref":"#/components/parameters/sort"},{"$ref":"#/components/parameters/depth"},{"$ref":"#/components/parameters/where"}],"responses":{"200":{"description":"Paginated posts","headers":{"RateLimit-Limit":{"description":"Requests allowed per window (fixed 60s window, per client IP).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until the current window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedPosts"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/posts/{id}":{"get":{"operationId":"getPost","tags":["content"],"summary":"Get one blog post by id","parameters":[{"$ref":"#/components/parameters/id"},{"$ref":"#/components/parameters/locale"},{"$ref":"#/components/parameters/depth"}],"responses":{"200":{"description":"The post","headers":{"RateLimit-Limit":{"description":"Requests allowed per window (fixed 60s window, per client IP).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until the current window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Post"}}}},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/projects":{"get":{"operationId":"listProjects","tags":["content"],"summary":"List published case studies","description":"Client projects / case studies with sector, services, stack and results.","parameters":[{"$ref":"#/components/parameters/locale"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/page"},{"$ref":"#/components/parameters/sort"},{"$ref":"#/components/parameters/depth"},{"$ref":"#/components/parameters/where"}],"responses":{"200":{"description":"Paginated projects","headers":{"RateLimit-Limit":{"description":"Requests allowed per window (fixed 60s window, per client IP).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until the current window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedProjects"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/pages":{"get":{"operationId":"listPages","tags":["content"],"summary":"List published CMS pages","parameters":[{"$ref":"#/components/parameters/locale"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/page"},{"$ref":"#/components/parameters/sort"},{"$ref":"#/components/parameters/depth"},{"$ref":"#/components/parameters/where"}],"responses":{"200":{"description":"Paginated pages","headers":{"RateLimit-Limit":{"description":"Requests allowed per window (fixed 60s window, per client IP).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until the current window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedPages"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/categories":{"get":{"operationId":"listCategories","tags":["content"],"summary":"List blog categories","parameters":[{"$ref":"#/components/parameters/locale"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/page"},{"$ref":"#/components/parameters/sort"},{"$ref":"#/components/parameters/depth"},{"$ref":"#/components/parameters/where"}],"responses":{"200":{"description":"Paginated categories","headers":{"RateLimit-Limit":{"description":"Requests allowed per window (fixed 60s window, per client IP).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until the current window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedCategories"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/search":{"get":{"operationId":"searchContent","tags":["search"],"summary":"Query the search index","description":"Search over indexed posts. Filter with the `where` parameter, e.g. `?where[title][like]=agents`.","parameters":[{"$ref":"#/components/parameters/locale"},{"$ref":"#/components/parameters/limit"},{"$ref":"#/components/parameters/page"},{"$ref":"#/components/parameters/sort"},{"$ref":"#/components/parameters/depth"},{"$ref":"#/components/parameters/where"}],"responses":{"200":{"description":"Paginated results","headers":{"RateLimit-Limit":{"description":"Requests allowed per window (fixed 60s window, per client IP).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until the current window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedSearchResults"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/graphql":{"post":{"operationId":"graphqlQuery","tags":["graphql"],"summary":"Run a GraphQL query","description":"GraphQL over the same published content (Posts, Projects, Pages, Categories, Search). Introspection is enabled — fetch the schema to discover every type. Access control still applies: anonymous callers only see published documents.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GraphQLRequest"}}}},"responses":{"200":{"description":"GraphQL result","headers":{"RateLimit-Limit":{"description":"Requests allowed per window (fixed 60s window, per client IP).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until the current window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GraphQLResponse"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/newsletter/subscribe":{"post":{"operationId":"subscribeNewsletter","tags":["forms"],"summary":"Subscribe an email to the Spidev newsletter","description":"Single opt-in newsletter sign-up. `consent` must be an explicit true from the address owner. Always answers 200 with the same body for accepted requests, whether or not the address was already subscribed.","security":[{},{"oauth2ClientCredentials":["forms:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewsletterSubscription"}}}},"responses":{"200":{"description":"Accepted","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/start/submit":{"post":{"operationId":"submitConsultationRequest","tags":["forms"],"summary":"Request a free consultation with the Spidev team","description":"The programmatic equivalent of the /en/start and /es/start booking form. Send `stage: \"lead\"` with the contact and company details; the Spidev team follows up by email/WhatsApp to schedule.","security":[{},{"oauth2ClientCredentials":["forms:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsultationRequest"}}}},"responses":{"200":{"description":"Received — the team follows up over email","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/oauth/register":{"post":{"operationId":"registerOAuthClient","tags":["oauth"],"summary":"Register an OAuth client (RFC 7591, open self-serve registration)","requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientRegistrationRequest"}}}},"responses":{"201":{"description":"Client credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientRegistrationResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/oauth/token":{"post":{"operationId":"requestOAuthToken","tags":["oauth"],"summary":"Exchange client credentials for a scoped bearer token","requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/TokenRequest"}}}},"responses":{"200":{"description":"Access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"description":"invalid_client","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}}},"components":{"securitySchemes":{"oauth2ClientCredentials":{"type":"oauth2","description":"Optional for reads (the content API is public); declares least-privilege intent. Register a client at /oauth/register first. Authorization server metadata: /.well-known/oauth-authorization-server","flows":{"clientCredentials":{"tokenUrl":"https://spidev.ai/oauth/token","scopes":{"content:read":"Read published content (posts, projects, pages, categories) over REST or GraphQL","search:read":"Query the site search index","forms:write":"Submit the newsletter sign-up and consultation booking forms"}}}}},"parameters":{"id":{"name":"id","in":"path","required":true,"schema":{"type":"integer"},"description":"Document id"},"locale":{"name":"locale","in":"query","schema":{"type":"string","enum":["es","en"]},"description":"Content locale. Defaults to es."},"limit":{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":10},"description":"Documents per page."},"page":{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1},"description":"Page number."},"sort":{"name":"sort","in":"query","schema":{"type":"string"},"description":"Sort field; prefix with - for descending, e.g. -publishedAt."},"depth":{"name":"depth","in":"query","schema":{"type":"integer","minimum":0,"maximum":2,"default":1},"description":"Relationship population depth."},"where":{"name":"where","in":"query","schema":{"type":"object"},"style":"deepObject","explode":true,"description":"Payload query filter, e.g. where[slug][equals]=my-post. See https://payloadcms.com/docs/queries/overview"}},"responses":{"BadRequest":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Document not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Rate limit exceeded — retry after the indicated delay","headers":{"RateLimit-Limit":{"description":"Requests allowed per window (fixed 60s window, per client IP).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until the current window resets.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string"},"errors":{"type":"array","items":{"type":"object"}}}},"OAuthError":{"type":"object","properties":{"error":{"type":"string"},"error_description":{"type":"string"}},"required":["error"]},"Post":{"type":"object","description":"A published blog post.","properties":{"id":{"type":"integer"},"title":{"type":"string"},"slug":{"type":"string"},"excerpt":{"type":["string","null"]},"content":{"type":"object","description":"Lexical rich-text document (tree of nodes under root)."},"faqs":{"type":["array","null"],"items":{"type":"object","properties":{"question":{"type":"string"},"answer":{"type":"string"}}}},"categories":{"type":["array","null"],"items":{}},"publishedAt":{"type":["string","null"],"format":"date-time"},"readingTime":{"type":["integer","null"]},"updatedAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"}},"additionalProperties":true},"Project":{"type":"object","description":"A client case study.","properties":{"id":{"type":"integer"},"title":{"type":"string"},"client":{"type":["string","null"]},"year":{"type":["integer","string","null"]},"sector":{"type":["string","null"]},"summary":{"type":["string","null"]},"services":{"type":["array","null"],"items":{"type":"object"}},"stack":{"type":["array","null"],"items":{"type":"object"}},"results":{"type":["array","null"],"items":{"type":"object"}},"updatedAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"}},"additionalProperties":true},"Page":{"type":"object","description":"A CMS page.","properties":{"id":{"type":"integer"},"title":{"type":"string"},"slug":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"}},"additionalProperties":true},"Category":{"type":"object","properties":{"id":{"type":"integer"},"title":{"type":"string"},"slug":{"type":["string","null"]}},"additionalProperties":true},"SearchResult":{"type":"object","properties":{"id":{"type":"integer"},"title":{"type":["string","null"]},"slug":{"type":["string","null"]},"meta":{"type":["object","null"]}},"additionalProperties":true},"PaginatedPosts":{"type":"object","description":"Paginated blog posts","properties":{"docs":{"type":"array","items":{"$ref":"#/components/schemas/Post"}},"totalDocs":{"type":"integer"},"limit":{"type":"integer"},"totalPages":{"type":"integer"},"page":{"type":"integer"},"pagingCounter":{"type":"integer"},"hasPrevPage":{"type":"boolean"},"hasNextPage":{"type":"boolean"},"prevPage":{"type":["integer","null"]},"nextPage":{"type":["integer","null"]}},"required":["docs"]},"PaginatedProjects":{"type":"object","description":"Paginated case studies","properties":{"docs":{"type":"array","items":{"$ref":"#/components/schemas/Project"}},"totalDocs":{"type":"integer"},"limit":{"type":"integer"},"totalPages":{"type":"integer"},"page":{"type":"integer"},"pagingCounter":{"type":"integer"},"hasPrevPage":{"type":"boolean"},"hasNextPage":{"type":"boolean"},"prevPage":{"type":["integer","null"]},"nextPage":{"type":["integer","null"]}},"required":["docs"]},"PaginatedPages":{"type":"object","description":"Paginated CMS pages","properties":{"docs":{"type":"array","items":{"$ref":"#/components/schemas/Page"}},"totalDocs":{"type":"integer"},"limit":{"type":"integer"},"totalPages":{"type":"integer"},"page":{"type":"integer"},"pagingCounter":{"type":"integer"},"hasPrevPage":{"type":"boolean"},"hasNextPage":{"type":"boolean"},"prevPage":{"type":["integer","null"]},"nextPage":{"type":["integer","null"]}},"required":["docs"]},"PaginatedCategories":{"type":"object","description":"Paginated categories","properties":{"docs":{"type":"array","items":{"$ref":"#/components/schemas/Category"}},"totalDocs":{"type":"integer"},"limit":{"type":"integer"},"totalPages":{"type":"integer"},"page":{"type":"integer"},"pagingCounter":{"type":"integer"},"hasPrevPage":{"type":"boolean"},"hasNextPage":{"type":"boolean"},"prevPage":{"type":["integer","null"]},"nextPage":{"type":["integer","null"]}},"required":["docs"]},"PaginatedSearchResults":{"type":"object","description":"Paginated search results","properties":{"docs":{"type":"array","items":{"$ref":"#/components/schemas/SearchResult"}},"totalDocs":{"type":"integer"},"limit":{"type":"integer"},"totalPages":{"type":"integer"},"page":{"type":"integer"},"pagingCounter":{"type":"integer"},"hasPrevPage":{"type":"boolean"},"hasNextPage":{"type":"boolean"},"prevPage":{"type":["integer","null"]},"nextPage":{"type":["integer","null"]}},"required":["docs"]},"GraphQLRequest":{"type":"object","properties":{"query":{"type":"string","description":"GraphQL query string"},"variables":{"type":"object"},"operationName":{"type":"string"}},"required":["query"]},"GraphQLResponse":{"type":"object","properties":{"data":{"type":["object","null"]},"errors":{"type":"array","items":{"type":"object"}}}},"NewsletterSubscription":{"type":"object","properties":{"email":{"type":"string","format":"email","maxLength":254},"consent":{"type":"boolean","description":"Must be true — explicit consent from the address owner."},"locale":{"type":"string","enum":["es","en"],"description":"Language the newsletter will arrive in."},"source":{"type":"string","maxLength":120}},"required":["email","consent"]},"ConsultationRequest":{"type":"object","properties":{"stage":{"type":"string","enum":["lead"],"description":"Use \"lead\"."},"name":{"type":"string"},"email":{"type":"string","format":"email"},"company":{"type":"string"},"whatsapp":{"type":"string","description":"Phone number with country code."},"companyInfo":{"type":"string","description":"What the company does."},"goal":{"type":"string","description":"What they want to automate or achieve."},"projectType":{"type":"string"},"locale":{"type":"string","enum":["es","en"]}},"required":["stage","name","email","company","whatsapp","companyInfo","goal"]},"ClientRegistrationRequest":{"type":"object","properties":{"client_name":{"type":"string","maxLength":120},"scope":{"type":"string","description":"Space-separated scopes. Supported: content:read search:read forms:write"}}},"ClientRegistrationResponse":{"type":"object","properties":{"client_id":{"type":"string"},"client_secret":{"type":"string"},"client_id_issued_at":{"type":"integer"},"client_secret_expires_at":{"type":"integer"},"grant_types":{"type":"array","items":{"type":"string"}},"token_endpoint_auth_method":{"type":"string"},"scope":{"type":"string"}},"required":["client_id","client_secret"]},"TokenRequest":{"type":"object","properties":{"grant_type":{"type":"string","enum":["client_credentials"]},"client_id":{"type":"string"},"client_secret":{"type":"string"},"scope":{"type":"string"}},"required":["grant_type"]},"TokenResponse":{"type":"object","properties":{"access_token":{"type":"string"},"token_type":{"type":"string","enum":["Bearer"]},"expires_in":{"type":"integer"},"scope":{"type":"string"}},"required":["access_token","token_type","expires_in"]}}}}