{"openapi":"3.1.0","info":{"title":"rakibulism.space API","version":"1.0.0","summary":"The read-only JSON API behind Rakibul Islam's portfolio.","description":"This site is a portfolio, not a service: there is no authentication, nothing to write to, and one JSON endpoint — the geo lookup the site uses to greet a visitor by city. It is described here exactly rather than dressed up as something larger.\n\n**Versioning.** The version is in the server URL: this document describes v1 at /api/v1. A breaking change to a response shape ships as /api/v2; the previous version keeps working and is marked with a `Deprecation` header (RFC 8594) plus a `Link rel=\"successor-version\"` (RFC 8288) naming its replacement. A `Sunset` header appears only if a path will actually be removed, with at least six months' notice. The unversioned /api/geo is already in that state — deprecated, no sunset, still working — and new integrations should use /api/v1/geo.\n\n**Rate limits.** 60 requests per minute per caller, reported on every /api response including errors as `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` and `RateLimit-Policy` (the IETF draft names). Exceeding it returns 429 with `Retry-After`. The counter is held per serverless instance rather than in shared storage, so a caller spread across instances gets a higher effective limit than the advertised number — stated here rather than hidden, because an agent that trusts `RateLimit-Remaining` should know how it is counted.\n\n**The rest of the site.** Every content page also serves a markdown rendition (append `.md`; the homepage is /index.md), alongside a sitemap, an RSS feed and an llms.txt index. Those are documents rather than API operations, so they are listed at https://rakibulism.space/llms.txt and https://rakibulism.space/developers rather than here.","contact":{"name":"Rakibul Islam","url":"https://rakibulism.space/contact","email":"40rakib70@gmail.com"},"license":{"name":"Content © Rakibul Islam. Code samples MIT.","url":"https://rakibulism.space/privacy"}},"servers":[{"url":"https://rakibulism.space/api/v1","description":"Version 1. The version is part of the path, so every operation below is relative to it."}],"tags":[{"name":"Geo","description":"Approximate caller location. Read-only, unauthenticated, never cached."}],"externalDocs":{"description":"Developer resources","url":"https://rakibulism.space/developers"},"paths":{"/geo":{"get":{"operationId":"getVisitorLocation","summary":"Resolve the caller's approximate location","description":"Returns the city and country the request appears to originate from, resolved at the edge from the caller's IP address. Used by the site itself to greet a visitor by city. Nothing is stored, the IP is never returned, and the response is never cached. Returns nulls rather than an error when the location cannot be determined — an unplaceable caller is an expected outcome, not a failure.","tags":["Geo"],"parameters":[],"responses":{"200":{"description":"Location resolved, or nulls if the edge could not place the caller.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer","example":60}},"RateLimit-Remaining":{"description":"Requests left in the current window.","schema":{"type":"integer","example":59}},"RateLimit-Reset":{"description":"Seconds until the window resets.","schema":{"type":"integer","example":47}},"RateLimit-Policy":{"description":"The policy in force, as limit;w=window-seconds.","schema":{"type":"string","example":"60;w=60"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeoLocation"}}}},"405":{"description":"A method other than GET was used. Carries an Allow header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded. Carries Retry-After alongside the RateLimit headers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer","example":60}},"RateLimit-Remaining":{"description":"Requests left in the current window.","schema":{"type":"integer","example":59}},"RateLimit-Reset":{"description":"Seconds until the window resets.","schema":{"type":"integer","example":47}},"RateLimit-Policy":{"description":"The policy in force, as limit;w=window-seconds.","schema":{"type":"string","example":"60;w=60"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","example":47}}}}}}},"/{path}":{"get":{"operationId":"getUnknownApiPath","summary":"Any other path under the API","description":"Every path under /api that is not an endpoint answers with the same structured JSON error rather than an HTML page, carrying the rate-limit headers, so a wrong guess is machine-readable and still tells the caller the policy.","tags":["Geo"],"parameters":[{"name":"path","in":"path","required":true,"description":"Any path segment that is not a real endpoint.","schema":{"type":"string","examples":["v1/posts"]}}],"responses":{"404":{"description":"No endpoint at that path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"405":{"description":"A method other than GET was used. Carries an Allow header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded. Carries Retry-After alongside the RateLimit headers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer","example":60}},"RateLimit-Remaining":{"description":"Requests left in the current window.","schema":{"type":"integer","example":59}},"RateLimit-Reset":{"description":"Seconds until the window resets.","schema":{"type":"integer","example":47}},"RateLimit-Policy":{"description":"The policy in force, as limit;w=window-seconds.","schema":{"type":"string","example":"60;w=60"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","example":47}}}}}}}},"components":{"schemas":{"GeoLocation":{"type":"object","required":["city","country","source"],"properties":{"city":{"type":["string","null"],"description":"City the request appears to originate from, decoded from the edge header. Null when unavailable.","example":"Dhaka"},"country":{"type":["string","null"],"description":"ISO 3166-1 alpha-2 country code, uppercased. Null when unavailable.","example":"BD"},"source":{"type":"string","enum":["vercel","none"],"description":"\"vercel\" when the edge resolved a location, \"none\" when it could not (local development, or an unplaceable address)."}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message","hint","docs"],"properties":{"code":{"type":"string","enum":["not_found","method_not_allowed"],"description":"Stable, machine-comparable error identifier."},"message":{"type":"string","description":"What went wrong, in one sentence."},"hint":{"type":"string","description":"What to do instead."},"docs":{"type":"string","format":"uri","description":"URL of this specification."}}}}}},"headers":{"RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer","example":60}},"RateLimit-Remaining":{"description":"Requests left in the current window.","schema":{"type":"integer","example":59}},"RateLimit-Reset":{"description":"Seconds until the window resets.","schema":{"type":"integer","example":47}},"RateLimit-Policy":{"description":"The policy in force, as limit;w=window-seconds.","schema":{"type":"string","example":"60;w=60"}}}}}