{"openapi":"3.1.0","info":{"title":"Sitey","version":"1.0.0","summary":"Free subdomains with the DNS behind them: claim a name, point it at an address with an A or CNAME record, and add the TXT record a host asks for. Over REST or MCP, with or without an account.","description":"Free subdomains with the DNS behind them: claim a name, point it at an address with an A or CNAME record, and add the TXT record a host asks for. Over REST or MCP, with or without an account.\n\nLimits:\n- An account may hold 5 subdomains and is refused the next one.\n- A caller with no API key is identified by its IP address and owns what that address created; there the 5 is enforced.\n- A name is lent, not given: 3 months for a record owned by an account, 1 month for one created without one. Every response carries expires_at, and renewal opens 14 days before it.\n- Nothing is removed on that date yet — deletion is switched off — but the date is real and renewing already works.\n- Nothing can be bought while the payment route is off: a caller who is refused is refused, not offered a price.\n\nThe same operations are available to an agent runtime over MCP at https://sitey.my/mcp (Streamable HTTP, one POST per message); https://sitey.my/.well-known/mcp.json describes it."},"servers":[{"url":"https://sitey.my"}],"externalDocs":{"description":"Docs","url":"https://sitey.my/docs"},"security":[{},{"apiKey":[]}],"paths":{"/api/v1/domains":{"get":{"summary":"The roots you may create a subdomain under","description":"Call this first. Which roots are on offer is a row in a table, not a constant, and the first one in the list is the one the site itself uses.","responses":{"200":{"description":"The active roots, canonical first.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"domains":{"type":"array","items":{"type":"string"},"example":["sitey.my"]}},"required":["domains"]}},"required":["success","data"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/check/{subdomain}/{domain}":{"get":{"summary":"Is this name free","description":"Checks the zone and the database. A name can be taken in either and available in neither, so both are asked before the answer is yes.","parameters":[{"name":"subdomain","in":"path","required":true,"schema":{"type":"string"},"description":"The subdomain label on its own, without the root."},{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"description":"One of the roots from GET /api/v1/domains."}],"responses":{"200":{"description":"Whether the name can be claimed.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"available":{"type":"boolean"},"subdomain":{"type":"string","example":"demo"},"domain":{"type":"string","example":"sitey.my"},"fqdn":{"type":"string","example":"demo.sitey.my"}},"required":["available","subdomain","domain","fqdn"]}},"required":["success","data"]}}}},"400":{"description":"INVALID_SUBDOMAIN — the label is not one DNS accepts. INVALID_DOMAIN — that root is not managed here; GET /api/v1/domains lists the ones that are.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/subdomains":{"post":{"summary":"Claim a name and point it somewhere","description":"Creates the DNS record and the row behind it together. Claim the name first and deploy to it second if that is the order you need: the target is asked for an HTTP response, but the answer is reported in `reachable` rather than refused, because most hosts want the DNS record in place before they will serve anything at the name. Nothing is removed for being unreachable. A record whose target stays dark is counted every night and otherwise left alone.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"subdomain":{"type":"string","example":"demo","description":"The label on its own."},"domain":{"type":"string","example":"sitey.my","description":"One of the roots from GET /api/v1/domains."},"type":{"type":"string","enum":["A","CNAME"],"description":"A for an IP address, CNAME for a hostname. Nothing else is written.","default":"A"},"value":{"type":"string","example":"203.0.113.10","description":"An IPv4 address for A, a hostname for CNAME. A CNAME may not point at itself."}},"required":["subdomain","domain","value"]}}}},"responses":{"201":{"description":"The record, the date it falls due, and whether anything is answering at it yet.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"fqdn":{"type":"string","example":"demo.sitey.my"},"type":{"type":"string","enum":["A","CNAME"],"description":"A for an IP address, CNAME for a hostname. Nothing else is written."},"value":{"type":"string","example":"203.0.113.10"},"expires_at":{"type":"string","format":"date-time","description":"When the lease ends. It travels in the response because a caller with no account has no mailbox to be reminded at."},"reachable":{"type":"boolean","description":"Whether the target answered an HTTP request at the moment the record was written. False is not a failure and nothing was rolled back; it means the name resolves and there is not yet anything at the other end."},"note":{"type":"string","description":"Present only when `reachable` is false: which check ran, and what it means."}},"required":["fqdn","type","value","expires_at","reachable"]}},"required":["success","data"]}}}},"400":{"description":"INVALID_SUBDOMAIN — the label is not one DNS accepts. INVALID_INPUT — the value is not an address of the type given. INVALID_DOMAIN — that root is not managed here. BLACKLISTED — the name is reserved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"Only when the payment route is switched on, and only for a caller over the limit. The body is an x402 payment requirement — what to pay, in what, and to whom — and the same request repeated with an `x-payment` header goes through. With the route off this is a 403 instead.","content":{"application/json":{"schema":{"type":"object","properties":{"x402Version":{"type":"integer"},"error":{"type":"string"},"accepts":{"type":"array","items":{"type":"object"}}}}}}},"403":{"description":"LIMIT_REACHED — the caller is holding as many as they may. The message says both numbers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"SUBDOMAIN_TAKEN — somebody else has it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}},"get":{"summary":"What this caller holds","description":"An API key lists the account's records. Without one, the records created from this IP address — which is the only identity an anonymous caller has.","responses":{"200":{"description":"Newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"subdomains":{"type":"array","items":{"$ref":"#/components/schemas/Subdomain"}}},"required":["subdomains"]}},"required":["success","data"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/subdomains/{subdomain}/{domain}":{"patch":{"summary":"Point an existing name somewhere else","description":"The record type cannot be changed, only the value it points at. The new target is asked for an HTTP response and the answer comes back in `reachable`, exactly as on create: the record is moved either way, so a name can be pointed at somewhere that is not serving yet.","parameters":[{"name":"subdomain","in":"path","required":true,"schema":{"type":"string"},"description":"The subdomain label on its own, without the root."},{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"description":"One of the roots from GET /api/v1/domains."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"string","example":"203.0.113.11"}},"required":["value"]}}}},"responses":{"200":{"description":"The record as it now stands.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"fqdn":{"type":"string","example":"demo.sitey.my"},"type":{"type":"string","enum":["A","CNAME"],"description":"A for an IP address, CNAME for a hostname. Nothing else is written."},"value":{"type":"string","example":"203.0.113.11"},"reachable":{"type":"boolean","description":"Whether the new target answered. False did not stop the move."},"note":{"type":"string","description":"Present only when `reachable` is false."}},"required":["fqdn","type","value","reachable"]}},"required":["success","data"]}}}},"400":{"description":"INVALID_INPUT — the value is not an address of this record's type. INVALID_DOMAIN — that root is not managed here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"FORBIDDEN — no such record under this caller. Deliberately the same answer as one that exists and belongs to somebody else.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"summary":"Give a name back","description":"Removes the zone record and the row together. The name is free immediately.","parameters":[{"name":"subdomain","in":"path","required":true,"schema":{"type":"string"},"description":"The subdomain label on its own, without the root."},{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"description":"One of the roots from GET /api/v1/domains."}],"responses":{"200":{"description":"Gone.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"fqdn":{"type":"string","example":"demo.sitey.my"},"deleted":{"type":"boolean","const":true}},"required":["fqdn","deleted"]}},"required":["success","data"]}}}},"400":{"description":"INVALID_DOMAIN — that root is not managed here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"FORBIDDEN — no such record under this caller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/subdomains/{subdomain}/{domain}/renew":{"post":{"summary":"Keep a name for another period","description":"The clock is reset from today rather than added to the old date, so calling this in a loop cannot stack up years. It opens 14 days before expiry — asking earlier is refused with the date it becomes possible.","parameters":[{"name":"subdomain","in":"path","required":true,"schema":{"type":"string"},"description":"The subdomain label on its own, without the root."},{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"description":"One of the roots from GET /api/v1/domains."}],"responses":{"200":{"description":"The new date.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"fqdn":{"type":"string","example":"demo.sitey.my"},"expires_at":{"type":"string","format":"date-time"}},"required":["fqdn","expires_at"]}},"required":["success","data"]}}}},"400":{"description":"INVALID_DOMAIN — that root is not managed here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"FORBIDDEN — no such record under this caller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"SUBDOMAIN_NOT_FOUND — it was there a moment ago and is not now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"RENEWAL_NOT_DUE — too early, or the record has no expiry at all. The message names the date to come back on, so the call does not have to be retried blindly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/subdomains/{subdomain}/{domain}/txt":{"post":{"summary":"Add the TXT record a host asks for","description":"Only `_vercel` may be used as a prefix, and the record is written at the root domain, which is where verification services look for it. Calling this twice with different values replaces this subdomain's own line and leaves everybody else's alone.","parameters":[{"name":"subdomain","in":"path","required":true,"schema":{"type":"string"},"description":"The subdomain label on its own, without the root."},{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"description":"One of the roots from GET /api/v1/domains."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"host_prefix":{"type":"string","enum":["_vercel"],"example":"_vercel"},"value":{"type":"string","maxLength":512,"description":"No line breaks, control characters, quotes or backslashes."}},"required":["host_prefix","value"]}}}},"responses":{"200":{"description":"The TXT record as written.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"fqdn":{"type":"string","example":"_vercel.sitey.my"},"type":{"type":"string","const":"TXT"},"value":{"type":"string"}},"required":["fqdn","type","value"]}},"required":["success","data"]}}}},"400":{"description":"INVALID_INPUT — the value is empty, too long, or carries a character a zone file cannot hold. INVALID_HOST_PREFIX — not one of the prefixes above. INVALID_DOMAIN — that root is not managed here. ROOT_LEVEL_FORBIDDEN — the `root_level` flag is gone; every TXT record is written at the root now, so the flag selects nothing. Omit it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"FORBIDDEN — no such subdomain under this caller to hang a TXT record on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/subdomains/{subdomain}/{domain}/txt/{hostPrefix}":{"delete":{"summary":"Take that TXT record away","description":"Removes this subdomain's own line. The name is shared with every other subdomain of the root, so the stored value is what identifies which line is yours.","parameters":[{"name":"subdomain","in":"path","required":true,"schema":{"type":"string"},"description":"The subdomain label on its own, without the root."},{"name":"domain","in":"path","required":true,"schema":{"type":"string"},"description":"One of the roots from GET /api/v1/domains."},{"name":"hostPrefix","in":"path","required":true,"schema":{"type":"string"},"description":"The prefix the TXT record was created under."}],"responses":{"200":{"description":"Gone. `zone_record_removed` is false when the row was there and the zone line was not — the row is removed either way, and saying so beats claiming a change that did not happen.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"fqdn":{"type":"string","example":"_vercel.sitey.my"},"type":{"type":"string","const":"TXT"},"deleted":{"type":"boolean","const":true},"zone_record_removed":{"type":"boolean"}},"required":["fqdn","type","deleted","zone_record_removed"]}},"required":["success","data"]}}}},"400":{"description":"INVALID_HOST_PREFIX — not a prefix this service writes. INVALID_DOMAIN — that root is not managed here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"FORBIDDEN — no such subdomain under this caller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"TXT_NOT_FOUND — that subdomain has no TXT record under this prefix.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"An API key from the dashboard, sent as `Authorization: Bearer styo_…`. It makes the records belong to an account rather than to an address. Anything else after `Bearer ` is refused rather than treated as anonymous."}},"responses":{"Unauthorized":{"description":"UNAUTHORIZED — the Authorization header carried something that is not a key of ours.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"RATE_LIMITED — more than 100 requests in a minute from one address. Wait and repeat.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"ServerError":{"description":"INTERNAL_ERROR — ours, not yours. Nothing partial is left behind: a write that cannot finish is rolled back in full.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Error":{"type":"object","description":"Every failure has this shape. Branch on `code`, log `message`.","properties":{"error":{"type":"boolean","const":true},"message":{"type":"string"},"code":{"type":"string"}},"required":["error","message","code"]},"Subdomain":{"type":"object","properties":{"subdomain":{"type":"string","example":"demo"},"domain":{"type":"string","example":"sitey.my"},"type":{"type":"string","enum":["A","CNAME"],"description":"A for an IP address, CNAME for a hostname. Nothing else is written."},"value":{"type":"string","example":"203.0.113.10"},"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"}}}}}}