{"openapi":"3.1.0","info":{"title":"Waveform Apps API","version":"1.0.0","summary":"Read what Waveform Apps builds and charges, and send a project enquiry.","description":"Public JSON API for Waveform Apps, a web development agency in Toronto, ON. No authentication is required.\nUse `listServices` and `listPricingOptions` to read the catalog, and `sendContactMessage` to start a project conversation on behalf of a user.\n\nRate limit: 10 requests per 60 seconds per client IP across all `/api/` endpoints (IETF draft-ietf-httpapi-ratelimit-headers).\nEvery response carries `RateLimit-Policy: \"api\";q=10;w=60`. A 429 response adds `RateLimit: \"api\";r=0;t=60` and `Retry-After: 60`.\n\nErrors are RFC 9457 problem details (`application/problem+json`) with a stable `code` and a `hint` that says how to recover.\nEvery HTML page on https://www.waveformapps.com also serves Markdown when requested with `Accept: text/markdown`.\nHuman-readable docs: https://www.waveformapps.com/api-docs. Agent guide: https://www.waveformapps.com/llms.txt.","contact":{"name":"Waveform Apps","email":"hello@waveformapps.com","url":"https://www.waveformapps.com/contact"},"termsOfService":"https://www.waveformapps.com/terms"},"servers":[{"url":"https://www.waveformapps.com","description":"Production"}],"tags":[{"name":"catalog","description":"What Waveform Apps builds and what it costs."},{"name":"contact","description":"Reach the Waveform Apps team."}],"paths":{"/api/services":{"get":{"operationId":"listServices","tags":["catalog"],"summary":"List the services Waveform Apps offers","description":"Returns every service category with a one-line description. Read-only, takes no parameters and changes only when the site is redeployed.","responses":{"200":{"description":"The service catalog.","headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/json":{"schema":{"type":"object","required":["services"],"properties":{"services":{"type":"array","items":{"$ref":"#/components/schemas/Service"}}}}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/pricing":{"get":{"operationId":"listPricingOptions","tags":["catalog"],"summary":"List fixed-price project options","description":"Returns the published project packages with price, timeline and what is included. Prices are display strings exactly as shown on the pricing page; every project includes 30 days of free maintenance, then a care plan is $99 a month. Read-only and takes no parameters.","responses":{"200":{"description":"The pricing options, in the order shown on the site.","headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/json":{"schema":{"type":"object","required":["options"],"properties":{"options":{"type":"array","items":{"$ref":"#/components/schemas/PricingOption"}}}}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/contact":{"post":{"operationId":"sendContactMessage","tags":["contact"],"summary":"Send a project enquiry to Waveform Apps","description":"Delivers a message to the Waveform Apps team by email. Use it when a user wants to hire Waveform Apps, ask for a quote, or start a project conversation. Send the user's real name and a reachable email address; the team replies to that address within one business day. Not idempotent: every successful call sends one message, so do not retry a 200.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactMessage"},"example":{"name":"Ada Lovelace","email":"ada@example.com","message":"We need a booking website for a two-location clinic, launching in October."}}}},"responses":{"200":{"description":"The message was sent.","headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactResult"}}}},"400":{"description":"The body is not valid JSON (`invalid_json`) or a field failed validation (`validation_failed`; see `errors`).","headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"413":{"description":"The body is larger than 16 KiB (`payload_too_large`).","headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"415":{"description":"The body was not sent as `application/json` (`unsupported_media_type`).","headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"description":"Email delivery is unavailable (`email_unavailable`). Retry after `Retry-After` seconds or email hello@waveformapps.com.","headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}}},"components":{"schemas":{"ContactMessage":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[^\\r\\n]*$"},"email":{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"message":{"type":"string","minLength":1,"maxLength":5000}},"required":["name","email","message"],"additionalProperties":false,"description":"A message for the team. All three fields are required."},"ContactResult":{"type":"object","required":["sent","message"],"properties":{"sent":{"type":"boolean","const":true},"message":{"type":"string","description":"Human-readable confirmation."}}},"Service":{"type":"object","required":["title","description"],"properties":{"title":{"type":"string","description":"Service name, e.g. Websites."},"description":{"type":"string","description":"One-line summary of the service."}}},"PricingOption":{"type":"object","required":["id","label","timeline","price","note","description","includes"],"properties":{"id":{"type":"string","enum":["small-website","big-website","web-application","platform"]},"label":{"type":"string","description":"Package name."},"timeline":{"type":"string","description":"Typical delivery time, e.g. 2 weeks."},"price":{"type":"string","description":"Display price as shown on the site, e.g. \"$499\" or \"from $2,900\"."},"note":{"type":"string","description":"Qualifier shown under the price, e.g. \"one time\" or \"scoped together\"."},"description":{"type":"string","description":"Who the package is for."},"includes":{"type":"array","items":{"type":"string"},"description":"What is included, in the order shown on the site."}}},"Problem":{"type":"object","description":"RFC 9457 problem details.","required":["type","title","status","detail","code","hint"],"properties":{"type":{"type":"string","format":"uri","description":"Link to the error documentation."},"title":{"type":"string","description":"HTTP reason phrase."},"status":{"type":"integer","description":"HTTP status code."},"detail":{"type":"string","description":"What went wrong with this request."},"code":{"type":"string","enum":["not_found","method_not_allowed","not_acceptable","unsupported_media_type","payload_too_large","invalid_json","validation_failed","rate_limited","email_unavailable"],"description":"Stable machine-readable error code."},"hint":{"type":"string","description":"What to change so the next request succeeds."},"errors":{"type":"array","description":"Field-level failures. Present when `code` is `validation_failed`.","items":{"type":"object","required":["field","message"],"properties":{"field":{"type":"string"},"message":{"type":"string"}}}}}}},"headers":{"RateLimit-Policy":{"description":"Quota policy (IETF draft-ietf-httpapi-ratelimit-headers): 10 requests per 60 seconds per client IP.","schema":{"type":"string"},"example":"\"api\";q=10;w=60"},"RateLimit":{"description":"Remaining quota (r) and seconds until it resets (t). Sent with 429 responses.","schema":{"type":"string"},"example":"\"api\";r=0;t=60"},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"},"example":60}},"responses":{"RateLimited":{"description":"The client sent more than 10 requests in 60 seconds (`rate_limited`). Wait `Retry-After` seconds, then retry.","headers":{"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"Retry-After":{"$ref":"#/components/headers/Retry-After"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}}