{"openapi":"3.1.0","info":{"title":"Road Sign Math API","version":"1.7.0","summary":"The signs, the map and the players of Road Sign Math.","description":"Every public sign, the ways people found to make it true, the map and who’s playing, read by anyone; sign-in by email, your own account, and posting signs and ways from the app. Answers are JSON in UTF-8 with snake_case keys and carry an `API-Version` header. A request with a token is never cached. An error is `{\"error\": {\"code\", \"message\"}}`.","license":{"name":"MIT","identifier":"MIT"}},"servers":[{"url":"https://roadsignmath.xyz"}],"paths":{"/api/v1":{"get":{"operationId":"getIndex","summary":"The API version and how many signs, players and states there are","description":"Read `api_version` to know what this server can do.","responses":{"200":{"description":"The API version and how many signs, players and states there are","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Index"}}}}}}},"/api/v1/openapi.json":{"get":{"operationId":"getOpenApi","summary":"This description","description":"OpenAPI 3.1, the same document as contracts/api-surface.json in the repo.","responses":{"200":{"description":"This description","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenApi"}}}}}}},"/api/v1/signs":{"get":{"operationId":"listSigns","summary":"The latest signs, newest first","description":"A page of at most `limit` signs (default 20, at most 50). Pass `next_cursor` back as `cursor` for the next page; it is null on the last.","parameters":[{"name":"cursor","in":"query","required":false,"description":"The `next_cursor` from the page before.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Signs on the page.","schema":{"type":"integer","minimum":1,"maximum":50,"default":20}}],"responses":{"200":{"description":"The latest signs, newest first","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignPage"}}}},"400":{"description":"A parameter or the body is missing or out of range; the message says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"postSign","summary":"Post a sign","description":"Posts the uploaded photos with the numbers you checked, where and when you found it, who found it (you, unless you say), and optionally the first way to make it true. Without a way it is posted unsolved. `unlocked` lists the badges the post earned you. Our moderator looks first: a post that is clearly objectionable is held, and only you see it, with `moderation_note` saying why. The moderator also writes each photo’s alt text. You need a name first. At most 20 a day.","security":[{"token":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Any 8 to 128 letters, digits, - or _. Sending the same post again with the same key gives back the sign it made instead of a second one, for a day.","schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{8,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignPost"}}}},"responses":{"201":{"description":"Post a sign","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignAndBadges"}}}},"400":{"description":"A parameter or the body is missing or out of range; the message says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No token, a token that has ended, or a sign-in code that is wrong or too old.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"It already exists (that way was found), or the same post or account change is still being made.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many tries for now; the message says when to try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The moderator or the hearts counter couldn’t answer just now; try again in a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/signs/near":{"get":{"operationId":"listSignsNear","summary":"Signs nearest a point","description":"The 50 nearest signs with a pin, nearest first, each with its straight-line `miles`. A sign nobody placed is left out.","parameters":[{"name":"lat","in":"query","required":true,"description":"Latitude in degrees.","schema":{"type":"number","minimum":-90,"maximum":90}},{"name":"lng","in":"query","required":true,"description":"Longitude in degrees.","schema":{"type":"number","minimum":-180,"maximum":180}}],"responses":{"200":{"description":"Signs nearest a point","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NearSigns"}}}},"400":{"description":"A parameter or the body is missing or out of range; the message says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/signs/similar":{"get":{"operationId":"listSimilarSigns","summary":"Signs that may be this one","description":"Signs within 2 miles that share at least half of the given numbers, for \"Nobody has posted this sign yet\" before posting. An empty list means none.","parameters":[{"name":"lat","in":"query","required":true,"description":"Latitude in degrees.","schema":{"type":"number","minimum":-90,"maximum":90}},{"name":"lng","in":"query","required":true,"description":"Longitude in degrees.","schema":{"type":"number","minimum":-180,"maximum":180}},{"name":"numbers","in":"query","required":true,"description":"The numbers read off the sign, comma separated, at most 30.","schema":{"type":"string","examples":["90,82,8,7"]}}],"responses":{"200":{"description":"Signs that may be this one","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimilarSigns"}}}},"400":{"description":"A parameter or the body is missing or out of range; the message says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/signs/{id}":{"get":{"operationId":"getSign","summary":"One sign and its ways","description":"By id or by slug. The ways are oldest first; the first is the poster’s, unless it was posted unsolved.","parameters":[{"name":"id","in":"path","required":true,"description":"The sign’s id or slug.","schema":{"type":"string"}}],"responses":{"200":{"description":"One sign and its ways","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignDetail"}}}},"400":{"description":"A parameter or the body is missing or out of range; the message says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such route, sign or player.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"deleteSign","summary":"Delete a sign you posted","description":"Removes the sign, its photos, every way on it and its hearts. A 503 means its hearts couldn’t be removed just now and the sign is still there; try again in a minute.","security":[{"token":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The sign’s id or slug.","schema":{"type":"string"}}],"responses":{"204":{"description":"Delete a sign you posted","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}}},"400":{"description":"A parameter or the body is missing or out of range; the message says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No token, a token that has ended, or a sign-in code that is wrong or too old.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such route, sign or player.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The moderator or the hearts counter couldn’t answer just now; try again in a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/map":{"get":{"operationId":"getMap","summary":"Every sign with a pin, for the map","description":"Every sign, with or without a pin (`lat` and `lng` are null without), and the years they span. Each pin carries its place, numbers and finders, so a map can search every sign by town, state, number or person from this one answer.","responses":{"200":{"description":"Every sign with a pin, for the map","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MapPins"}}}}}}},"/api/v1/players":{"get":{"operationId":"listPlayers","summary":"Who’s playing","description":"Every player with at least one sign, the one with the latest sign first.","responses":{"200":{"description":"Who’s playing","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Players"}}}}}}},"/api/v1/players/{handle}":{"get":{"operationId":"getPlayer","summary":"One player, their signs and their badges","description":"A player’s signs are the ones they found, newest first. Their badges are the ones they earned, in catalog order, worked out from the signs and ways you can see.","parameters":[{"name":"handle","in":"path","required":true,"description":"The player’s handle.","schema":{"type":"string"}}],"responses":{"200":{"description":"One player, their signs and their badges","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlayerDetail"}}}},"400":{"description":"A parameter or the body is missing or out of range; the message says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such route, sign or player.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/badges":{"get":{"operationId":"getBadges","summary":"Every badge there is","description":"Badges are little road signs a player collects for what happened on the road: a new state, a square root, a sign found a year after the first. Nothing is added up or compared. A secret badge gives only its id and shelf. A player’s earned badges are in `GET /players/{handle}` and `GET /me`.","responses":{"200":{"description":"Every badge there is","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadgeCatalog"}}}}}}},"/api/v1/auth/start":{"post":{"operationId":"startSignIn","summary":"Email a sign-in link and code","description":"Sends a link that opens the app and a 6-digit code, both good for 15 minutes. The answer is the same whether or not the address has an account; the first sign-in makes one. At most 5 emails an hour to an address.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInStart"}}}},"responses":{"202":{"description":"Email a sign-in link and code","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInStarted"}}}},"400":{"description":"A parameter or the body is missing or out of range; the message says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many tries for now; the message says when to try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/auth/verify":{"post":{"operationId":"finishSignIn","summary":"Trade the link’s token or the code for an app token","description":"Send `login_id` with either the `token` from the link or the `code` from the email. Five wrong codes end the sign-in. The answer’s `token` goes in `Authorization: Bearer` from then on; it lasts until sign-out, or 180 days without use.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInFinish"}}}},"responses":{"200":{"description":"Trade the link’s token or the code for an app token","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignedIn"}}}},"400":{"description":"A parameter or the body is missing or out of range; the message says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No token, a token that has ended, or a sign-in code that is wrong or too old.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many tries for now; the message says when to try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/auth/signout":{"post":{"operationId":"signOut","summary":"End this token","description":"Other devices stay signed in.","security":[{"token":[]}],"responses":{"204":{"description":"End this token","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}}},"401":{"description":"No token, a token that has ended, or a sign-in code that is wrong or too old.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/me":{"get":{"operationId":"getMe","summary":"You","description":"Your public profile, plus your email and whether you still need to give a name.","security":[{"token":[]}],"responses":{"200":{"description":"You","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"401":{"description":"No token, a token that has ended, or a sign-in code that is wrong or too old.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"updateMe","summary":"Change your name or bio","description":"The first name you give also sets your handle, the address of your page; it stays after that. A null or empty bio removes it.","security":[{"token":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeUpdate"}}}},"responses":{"200":{"description":"Change your name or bio","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"400":{"description":"A parameter or the body is missing or out of range; the message says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No token, a token that has ended, or a sign-in code that is wrong or too old.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"deleteMe","summary":"Delete your account","description":"Deletes your account and everything it made: the signs you posted (with every way and heart on them), the ways you found and the hearts you gave on other signs, your name among a sign’s finders, and your email. It can’t be undone. A 503 means a heart couldn’t be taken back just now and nothing more was deleted; try again in a minute. A 409 means one of your hearts was changing; try again.","security":[{"token":[]}],"responses":{"204":{"description":"Delete your account","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}}},"401":{"description":"No token, a token that has ended, or a sign-in code that is wrong or too old.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"It already exists (that way was found), or the same post or account change is still being made.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The moderator or the hearts counter couldn’t answer just now; try again in a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/me/avatar":{"put":{"operationId":"setAvatar","summary":"Set your profile photo","description":"The body is the photo itself, JPEG, PNG or WebP, at most 4 MB. It is cropped square from its middle (send it already cropped), kept at two sizes with its metadata (and so its location) removed, and shown to everyone at once in place of your first letter. The moderator looks first: a 422 means it wouldn’t show the photo, and its message says why. The photo it replaces is removed. At most 20 a day. A 409 means your account was changing (being deleted, or another photo going up); try again.","security":[{"token":[]}],"requestBody":{"required":true,"content":{"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/png":{"schema":{"type":"string","format":"binary"}},"image/webp":{"schema":{"type":"string","format":"binary"}}}},"responses":{"200":{"description":"Set your profile photo","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"400":{"description":"A parameter or the body is missing or out of range; the message says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No token, a token that has ended, or a sign-in code that is wrong or too old.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"It already exists (that way was found), or the same post or account change is still being made.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The photo is over 4 MB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The moderator wouldn’t show that profile photo; the message says why, for the player.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many tries for now; the message says when to try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The moderator or the hearts counter couldn’t answer just now; try again in a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"removeAvatar","summary":"Remove your profile photo","description":"Removes your profile photo; your first letter shows again. Removing none is fine. A 409 means your account was changing; try again.","security":[{"token":[]}],"responses":{"200":{"description":"Remove your profile photo","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"401":{"description":"No token, a token that has ended, or a sign-in code that is wrong or too old.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"It already exists (that way was found), or the same post or account change is still being made.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/me/blocks":{"get":{"operationId":"listBlocks","summary":"Players you blocked","description":"Their signs and ways are left out of everything you read.","security":[{"token":[]}],"responses":{"200":{"description":"Players you blocked","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Blocks"}}}},"401":{"description":"No token, a token that has ended, or a sign-in code that is wrong or too old.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/me/photos/{photoId}/{size}":{"get":{"operationId":"getMyPhoto","summary":"A photo of one of your signs","description":"One size of a photo on a sign you posted. A held or hidden sign’s photos are served only here, to you; the summary’s `url` and `thumb_url` point here for them. Never cached.","security":[{"token":[]}],"parameters":[{"name":"photoId","in":"path","required":true,"description":"The photo’s id.","schema":{"type":"string"}},{"name":"size","in":"path","required":true,"description":"large (1600 px) or thumb (480 px).","schema":{"type":"string","enum":["large","thumb"]}}],"responses":{"200":{"description":"A photo of one of your signs","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"image/jpeg":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"A parameter or the body is missing or out of range; the message says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No token, a token that has ended, or a sign-in code that is wrong or too old.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such route, sign or player.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/photos":{"post":{"operationId":"uploadPhoto","summary":"Upload a photo for a post","description":"The body is the photo itself, JPEG, PNG or WebP, at most 4 MB. It is kept at two sizes with its metadata (and so its location) removed. Send the answer’s `id` in `photo_ids` when posting, within a day. At most 60 a day.","security":[{"token":[]}],"requestBody":{"required":true,"content":{"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/png":{"schema":{"type":"string","format":"binary"}},"image/webp":{"schema":{"type":"string","format":"binary"}}}},"responses":{"201":{"description":"Upload a photo for a post","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Upload"}}}},"400":{"description":"A parameter or the body is missing or out of range; the message says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No token, a token that has ended, or a sign-in code that is wrong or too old.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The photo is over 4 MB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many tries for now; the message says when to try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/signs/{id}/ways":{"post":{"operationId":"addWay","summary":"Add another way to make a sign true","description":"The math must be true, and every number in it must be one of the sign’s numbers, each used no more often than the sign shows it. A way someone already found is a conflict. Answers with the sign, all its ways, and in `unlocked` the badges the way earned you. You need a name first. At most 100 a day.","security":[{"token":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The sign’s id or slug.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WayPost"}}}},"responses":{"201":{"description":"Add another way to make a sign true","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignAndBadges"}}}},"400":{"description":"A parameter or the body is missing or out of range; the message says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No token, a token that has ended, or a sign-in code that is wrong or too old.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such route, sign or player.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"It already exists (that way was found), or the same post or account change is still being made.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many tries for now; the message says when to try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/signs/{id}/heart":{"put":{"operationId":"heartSign","summary":"Heart a sign","description":"One heart per player per sign; hearting it again changes nothing. Only a shown sign can be hearted. Answers with `hearted` and the sign’s `hearts`, which counts the website’s hearts too (null when the count can’t be read just now). A 503 means the heart couldn’t be counted and nothing changed. At most 200 hearts given or taken back a day.","security":[{"token":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The sign’s id or slug.","schema":{"type":"string"}}],"responses":{"200":{"description":"Heart a sign","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HeartState"}}}},"400":{"description":"A parameter or the body is missing or out of range; the message says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No token, a token that has ended, or a sign-in code that is wrong or too old.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such route, sign or player.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"It already exists (that way was found), or the same post or account change is still being made.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many tries for now; the message says when to try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The moderator or the hearts counter couldn’t answer just now; try again in a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"unheartSign","summary":"Take back your heart","description":"Taking back a heart you didn’t give changes nothing. A 503 means it couldn’t be taken back just now and nothing changed.","security":[{"token":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The sign’s id or slug.","schema":{"type":"string"}}],"responses":{"200":{"description":"Take back your heart","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HeartState"}}}},"400":{"description":"A parameter or the body is missing or out of range; the message says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No token, a token that has ended, or a sign-in code that is wrong or too old.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such route, sign or player.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"It already exists (that way was found), or the same post or account change is still being made.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many tries for now; the message says when to try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The moderator or the hearts counter couldn’t answer just now; try again in a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/signs/{id}/report":{"post":{"operationId":"reportSign","summary":"Report a sign","description":"Our moderator looks at the sign again with your reason, and hides it if it agrees it is objectionable. One report per sign; sending another gives back the first answer. At most 20 a day.","security":[{"token":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"The sign’s id or slug.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportPost"}}}},"responses":{"200":{"description":"Report a sign","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportOutcome"}}}},"400":{"description":"A parameter or the body is missing or out of range; the message says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No token, a token that has ended, or a sign-in code that is wrong or too old.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such route, sign or player.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"It already exists (that way was found), or the same post or account change is still being made.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many tries for now; the message says when to try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The moderator or the hearts counter couldn’t answer just now; try again in a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/app/views":{"post":{"operationId":"countAppViews","summary":"Count the app’s screen views","description":"Up to 20 screen views at once, each the path of the website page it matches (`/`, `/map`, `/signs/<slug>`, `/players/<handle>`) or a short one of its own (`/capture`): lowercase letters, digits and hyphens, at most two parts. They are counted in the site’s analytics as the app’s, with the request’s User-Agent and country and nothing about who sent them; signed in or not makes no difference. The answer comes once they are passed on, or a moment later if the counter is slow; a view that can’t be counted is dropped. At most 120 batches an hour from one network.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppViews"}}}},"responses":{"202":{"description":"Count the app’s screen views","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppViewsAccepted"}}}},"400":{"description":"A parameter or the body is missing or out of range; the message says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many tries for now; the message says when to try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/players/{handle}/block":{"post":{"operationId":"blockPlayer","summary":"Block a player","description":"Their signs and ways are left out of everything you read. They aren’t told.","security":[{"token":[]}],"parameters":[{"name":"handle","in":"path","required":true,"description":"The player’s handle.","schema":{"type":"string"}}],"responses":{"204":{"description":"Block a player","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}}},"400":{"description":"A parameter or the body is missing or out of range; the message says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No token, a token that has ended, or a sign-in code that is wrong or too old.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such route, sign or player.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"unblockPlayer","summary":"Unblock a player","description":"Their signs and ways come back.","security":[{"token":[]}],"parameters":[{"name":"handle","in":"path","required":true,"description":"The player’s handle.","schema":{"type":"string"}}],"responses":{"204":{"description":"Unblock a player","headers":{"API-Version":{"schema":{"type":"string"},"description":"This server’s API version."}}},"400":{"description":"A parameter or the body is missing or out of range; the message says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No token, a token that has ended, or a sign-in code that is wrong or too old.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such route, sign or player.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["bad_request","unauthorized","not_found","conflict","too_large","refused","rate_limited","internal","unavailable"]},"message":{"type":"string"}}}}},"Index":{"type":"object","required":["api_version","signs","players","states","openapi_url"],"properties":{"api_version":{"type":"string","examples":["1.7.0"]},"signs":{"type":"integer","minimum":0},"players":{"type":"integer","minimum":0},"states":{"type":"integer","minimum":0},"openapi_url":{"type":"string","format":"uri"}}},"OpenApi":{"type":"object","description":"This document."},"Place":{"type":"object","required":["label","state","lat","lng","approximate"],"properties":{"label":{"type":"string","examples":["I-35W, Bloomington, Minnesota"]},"state":{"anyOf":[{"type":"string","description":"Two-letter US state."},{"type":"null"}]},"lat":{"anyOf":[{"type":"number"},{"type":"null"}]},"lng":{"anyOf":[{"type":"number"},{"type":"null"}]},"approximate":{"type":"boolean","description":"True when the pin is only as good as a town."}}},"Photo":{"type":"object","required":["id","width","height","alt","url","thumb_url"],"properties":{"id":{"type":"string"},"width":{"type":"integer","minimum":0},"height":{"type":"integer","minimum":0},"alt":{"type":"string"},"url":{"type":"string","format":"uri"},"thumb_url":{"type":"string","format":"uri"}}},"SignNumber":{"type":"object","description":"A number on the sign, as its poster checked it.","required":["value","text","why","box"],"properties":{"value":{"type":"string","description":"The number the math may use, as decimal text.","examples":["7"]},"text":{"anyOf":[{"type":"string","description":"What it was read from.","examples":["EXIT 7B"]},{"type":"null"}]},"why":{"anyOf":[{"type":"string","description":"Why the value differs from the text.","examples":["Read as 7"]},{"type":"null"}]},"box":{"anyOf":[{"type":"array","description":"Where on the first photo: x, y, width, height as fractions, origin top left.","items":{"type":"number","minimum":0,"maximum":1},"minItems":4,"maxItems":4},{"type":"null"}]}}},"Finder":{"type":"object","required":["name","handle","role"],"properties":{"name":{"type":"string"},"handle":{"anyOf":[{"type":"string"},{"type":"null"}]},"role":{"type":"string","enum":["found","took_photo","spotted"]}}},"FirstWay":{"type":"object","required":["text","tex","value"],"properties":{"text":{"type":"string","examples":["3 × 33 = 99"]},"tex":{"anyOf":[{"type":"string","description":"LaTeX for drawing the math (÷ as a stacked fraction); null only if the text no longer parses.","examples":["3 \\times 33 = 99"]},{"type":"null"}]},"value":{"type":"string","examples":["99"]}}},"Sign":{"type":"object","required":["id","slug","title","url","caption","found_on","posted_at","posted_by","place","photos","numbers","finders","first_way","way_count","hearts","hearted","visibility","moderation_note"],"properties":{"id":{"type":"string"},"slug":{"type":"string"},"title":{"type":"string"},"url":{"type":"string","format":"uri"},"caption":{"anyOf":[{"type":"string"},{"type":"null"}]},"found_on":{"type":"string","format":"date"},"posted_at":{"type":"string","format":"date-time"},"posted_by":{"anyOf":[{"type":"string"},{"type":"null"}]},"place":{"$ref":"#/components/schemas/Place"},"photos":{"type":"array","items":{"$ref":"#/components/schemas/Photo"}},"numbers":{"type":"array","items":{"$ref":"#/components/schemas/SignNumber"}},"finders":{"type":"array","items":{"$ref":"#/components/schemas/Finder"}},"first_way":{"anyOf":[{"$ref":"#/components/schemas/FirstWay"},{"type":"null"}]},"way_count":{"type":"integer","minimum":0},"hearts":{"anyOf":[{"type":"integer","minimum":0,"description":"Hearts on the sign, from the website and the app together; null when they can’t be counted just now."},{"type":"null"}]},"hearted":{"anyOf":[{"type":"boolean","description":"Whether you hearted it; null when not signed in."},{"type":"null"}]},"visibility":{"type":"string","enum":["shown","held","hidden"],"description":"Always shown, except on your own signs: held when posted, hidden after a report."},"moderation_note":{"anyOf":[{"type":"string","description":"Why your sign is held or hidden; null when shown."},{"type":"null"}]}}},"Way":{"type":"object","required":["id","text","tex","value","numbers","by","created_at"],"properties":{"id":{"type":"string"},"text":{"type":"string","description":"Canonical text: Unicode operators, √ for square roots."},"tex":{"anyOf":[{"type":"string","description":"LaTeX for drawing the math (÷ as a stacked fraction); null only if the text no longer parses.","examples":["3 \\times 33 = 99"]},{"type":"null"}]},"value":{"type":"string","description":"What both sides equal."},"numbers":{"type":"array","items":{"type":"string"}},"by":{"anyOf":[{"type":"object","required":["handle","name"],"properties":{"handle":{"type":"string"},"name":{"type":"string"}}},{"type":"null"}]},"created_at":{"type":"string","format":"date-time"}}},"SignDetail":{"allOf":[{"$ref":"#/components/schemas/Sign"},{"type":"object","required":["ways"],"properties":{"ways":{"type":"array","items":{"$ref":"#/components/schemas/Way"}}}}]},"SignPage":{"type":"object","required":["signs","next_cursor"],"properties":{"signs":{"type":"array","items":{"$ref":"#/components/schemas/Sign"}},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}]}}},"NearSigns":{"type":"object","required":["signs"],"properties":{"signs":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Sign"},{"type":"object","required":["miles"],"properties":{"miles":{"type":"number","minimum":0}}}]}}}},"SimilarSigns":{"type":"object","required":["signs"],"properties":{"signs":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Sign"},{"type":"object","required":["miles"],"properties":{"miles":{"type":"number","minimum":0}}}]}}}},"Pin":{"type":"object","required":["id","slug","title","url","lat","lng","approximate","state","place_label","found_on","numbers","finders","thumb_url","hearts","hearted"],"properties":{"id":{"type":"string"},"slug":{"type":"string"},"title":{"type":"string"},"url":{"type":"string","format":"uri"},"lat":{"anyOf":[{"type":"number"},{"type":"null"}]},"lng":{"anyOf":[{"type":"number"},{"type":"null"}]},"approximate":{"type":"boolean"},"state":{"anyOf":[{"type":"string"},{"type":"null"}]},"place_label":{"type":"string","description":"Where it was, as the sign’s `place.label`."},"found_on":{"type":"string","format":"date"},"numbers":{"type":"array","items":{"type":"string","description":"The values of the sign’s checked numbers, in the sign’s order.","examples":["7"]}},"finders":{"type":"array","items":{"type":"object","required":["name","handle"],"properties":{"name":{"type":"string"},"handle":{"anyOf":[{"type":"string"},{"type":"null"}]}}}},"thumb_url":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}]},"hearts":{"anyOf":[{"type":"integer","minimum":0,"description":"Hearts on the sign, from the website and the app together; null when they can’t be counted just now."},{"type":"null"}]},"hearted":{"anyOf":[{"type":"boolean","description":"Whether you hearted it; null when not signed in."},{"type":"null"}]}}},"MapPins":{"type":"object","required":["pins","years"],"properties":{"pins":{"type":"array","items":{"$ref":"#/components/schemas/Pin"}},"years":{"type":"array","items":{"type":"integer"}}}},"Player":{"type":"object","required":["handle","name","avatar_color","avatar_url","avatar_thumb_url","bio","url","sign_count","states","first_year"],"properties":{"handle":{"type":"string"},"name":{"type":"string"},"avatar_color":{"type":"string","pattern":"^#[0-9A-F]{6}$","description":"Behind the first letter of the name, when there is no profile photo."},"avatar_url":{"anyOf":[{"type":"string","format":"uri","description":"The profile photo, 512 pixels square, JPEG; null when there is none."},{"type":"null"}]},"avatar_thumb_url":{"anyOf":[{"type":"string","format":"uri","description":"The profile photo at 160 pixels square, for small faces."},{"type":"null"}]},"bio":{"anyOf":[{"type":"string"},{"type":"null"}]},"url":{"type":"string","format":"uri"},"sign_count":{"type":"integer","minimum":0},"states":{"type":"array","items":{"type":"string"}},"first_year":{"anyOf":[{"type":"integer"},{"type":"null"}]}}},"Players":{"type":"object","required":["players"],"properties":{"players":{"type":"array","items":{"$ref":"#/components/schemas/Player"}}}},"PlayerDetail":{"allOf":[{"$ref":"#/components/schemas/Player"},{"type":"object","required":["signs","badges"],"properties":{"signs":{"type":"array","items":{"$ref":"#/components/schemas/Sign"}},"badges":{"type":"array","items":{"$ref":"#/components/schemas/EarnedBadge"}}}}]},"CatalogBadge":{"type":"object","description":"A badge in the catalog. Each shelf is drawn as a family of road signs.","required":["id","shelf","secret","name","how","mark","steps"],"properties":{"id":{"type":"string"},"shelf":{"type":"string","enum":["firsts","places","math","seasons","secret"]},"secret":{"type":"boolean","description":"A secret badge gives only its id and shelf until someone earns it."},"name":{"anyOf":[{"type":"string"},{"type":"null"}]},"how":{"anyOf":[{"type":"string","description":"How it is earned, in a line."},{"type":"null"}]},"mark":{"anyOf":[{"type":"string","description":"What its sign shows: a word, a symbol or a number."},{"type":"null"}]},"steps":{"anyOf":[{"type":"array","items":{"type":"integer","minimum":0},"description":"For a badge earned in steps, each step’s count."},{"type":"null"}]}}},"BadgeCatalog":{"type":"object","required":["badges"],"properties":{"badges":{"type":"array","items":{"$ref":"#/components/schemas/CatalogBadge"}}}},"EarnedBadge":{"type":"object","description":"A badge a player earned, with the sign that earned it first. Badges follow the signs on the map, so one goes when its sign does.","required":["id","shelf","name","how","mark","step","earned_on","sign"],"properties":{"id":{"type":"string"},"shelf":{"type":"string","enum":["firsts","places","math","seasons","secret"]},"name":{"type":"string"},"how":{"type":"string"},"mark":{"type":"string","description":"What its sign shows; a stepped badge shows its step."},"step":{"anyOf":[{"type":"integer","minimum":0,"description":"The step reached, for a badge earned in steps."},{"type":"null"}]},"earned_on":{"type":"string","format":"date","description":"The day the sign was found, in the finder’s own time. For the math, a way posted with its sign takes that day too; a way added later takes the day it was written in US Central."},"sign":{"type":"object","required":["id","slug","title","url"],"properties":{"id":{"type":"string"},"slug":{"type":"string"},"title":{"type":"string"},"url":{"type":"string","format":"uri"}}}}},"SignAndBadges":{"allOf":[{"$ref":"#/components/schemas/SignDetail"},{"type":"object","required":["unlocked"],"properties":{"unlocked":{"type":"array","items":{"$ref":"#/components/schemas/EarnedBadge"},"description":"Badges this post or way earned you just now, or a higher step of one."}}}]},"Me":{"allOf":[{"$ref":"#/components/schemas/Player"},{"type":"object","required":["email","needs_name","badges"],"properties":{"email":{"type":"string","format":"email","description":"Only ever sent to you."},"needs_name":{"type":"boolean","description":"True until you give a name; ask for one before posting."},"badges":{"type":"array","items":{"$ref":"#/components/schemas/EarnedBadge"}}}}]},"MeUpdate":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string","minLength":1,"maxLength":40},"bio":{"anyOf":[{"type":"string","maxLength":160},{"type":"null"}]}}},"Blocks":{"type":"object","required":["players"],"properties":{"players":{"type":"array","items":{"type":"object","required":["handle","name"],"properties":{"handle":{"type":"string"},"name":{"type":"string"}}}}}},"Upload":{"type":"object","required":["id","width","height"],"properties":{"id":{"type":"string","description":"Send it in `photo_ids` when posting."},"width":{"type":"integer","minimum":0},"height":{"type":"integer","minimum":0}}},"SignPost":{"type":"object","additionalProperties":false,"required":["photo_ids","title","numbers","place","found_on"],"properties":{"photo_ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":4},"title":{"type":"string","minLength":1,"maxLength":60},"caption":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}]},"numbers":{"type":"array","minItems":1,"maxItems":30,"items":{"type":"object","additionalProperties":false,"required":["value"],"properties":{"value":{"type":"string","pattern":"^[0-9]*\\.?[0-9]+$","examples":["7"]},"text":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}]},"why":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}]},"box":{"anyOf":[{"type":"array","items":{"type":"number","minimum":0,"maximum":1},"minItems":4,"maxItems":4},{"type":"null"}]}}}},"place":{"type":"object","additionalProperties":false,"required":["label"],"properties":{"label":{"type":"string","minLength":1,"maxLength":120},"state":{"anyOf":[{"type":"string","pattern":"^[A-Z]{2}$"},{"type":"null"}]},"lat":{"anyOf":[{"type":"number","minimum":-90,"maximum":90},{"type":"null"}]},"lng":{"anyOf":[{"type":"number","minimum":-180,"maximum":180},{"type":"null"}]},"approximate":{"type":"boolean","default":false}}},"found_on":{"type":"string","format":"date","description":"The day it was found, in your own time."},"finders":{"type":"array","minItems":1,"maxItems":6,"description":"Who found it: a player by handle, or a name for someone who doesn’t play. Default: you.","items":{"type":"object","additionalProperties":false,"properties":{"handle":{"type":"string"},"name":{"type":"string","minLength":1,"maxLength":40},"role":{"type":"string","enum":["found","took_photo","spotted"],"default":"found"}}}},"way":{"type":"string","maxLength":400,"description":"The first way to make it true, as math text. Leave it out to post unsolved.","examples":["90 − 82 = 8"]}}},"WayPost":{"type":"object","required":["text"],"properties":{"text":{"type":"string","maxLength":400,"examples":["7 + 12 = 19"]}}},"ReportPost":{"type":"object","required":["reason"],"properties":{"reason":{"type":"string","minLength":1,"maxLength":300}}},"ReportOutcome":{"type":"object","required":["outcome","note"],"properties":{"outcome":{"type":"string","enum":["hidden","kept"]},"note":{"type":"string","description":"The moderator’s reason."}}},"HeartState":{"type":"object","required":["hearted","hearts"],"properties":{"hearted":{"type":"boolean"},"hearts":{"anyOf":[{"type":"integer","minimum":0,"description":"The sign’s hearts now; null when they can’t be counted just now."},{"type":"null"}]}}},"AppViews":{"type":"object","additionalProperties":false,"required":["views"],"properties":{"views":{"type":"array","minItems":1,"maxItems":20,"items":{"type":"object","additionalProperties":false,"required":["path"],"properties":{"path":{"type":"string","maxLength":100,"pattern":"^\\/(?:[a-z0-9-]{1,80}(?:\\/[a-z0-9-]{1,80})?)?$","examples":["/signs/speed-limit-55","/map"]}}}}}},"AppViewsAccepted":{"type":"object","required":["accepted"],"properties":{"accepted":{"type":"integer","minimum":0,"description":"How many views were taken."}}},"SignInStart":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}},"SignInStarted":{"type":"object","required":["login_id","expires_in"],"properties":{"login_id":{"type":"string","description":"Send it back with the code or the link’s token."},"expires_in":{"type":"integer","description":"Seconds the link and code work for.","examples":[900]}}},"SignInFinish":{"type":"object","required":["login_id"],"description":"The login_id and exactly one of code or token.","properties":{"login_id":{"type":"string"},"code":{"type":"string","pattern":"^[0-9]{6}$"},"token":{"type":"string","description":"The `t` parameter of the link in the email."}}},"SignedIn":{"type":"object","required":["token","new_player","me"],"properties":{"token":{"type":"string","description":"Send as `Authorization: Bearer <token>`.","pattern":"^rsm_"},"new_player":{"type":"boolean","description":"True when this sign-in made the account."},"me":{"$ref":"#/components/schemas/Me"}}}},"securitySchemes":{"token":{"type":"http","scheme":"bearer","description":"The `token` from `POST /api/v1/auth/verify`."}}}}