{
  "openapi": "3.1.0",
  "info": {
    "title": "Snaprint public API",
    "version": "1.0.0",
    "summary": "Print-shop directory and demo booking for Snaprint self-service print kiosks.",
    "description": "Read-only directory of listed xerox/print shops by Bengaluru neighbourhood, plus demo booking with the Snaprint founding team. No authentication. Errors are RFC 9457 problem details (application/problem+json) with a machine-readable `code` and a `hint`. /api/book/* is rate limited to 5 requests per 60 s per IP and returns IETF RateLimit / RateLimit-Policy headers (draft-ietf-httpapi-ratelimit-headers) plus Retry-After on 429. Human docs: https://snaprints.com/developers",
    "contact": {
      "name": "Snaprint (Sanskriti Labs)",
      "email": "snaprints@sanskritilabs.in",
      "url": "https://snaprints.com/developers"
    },
    "termsOfService": "https://snaprints.com/terms"
  },
  "externalDocs": {
    "description": "Developer docs",
    "url": "https://snaprints.com/developers"
  },
  "servers": [
    {
      "url": "https://snaprints.com"
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "locations",
      "description": "Directory of listed print shops by neighbourhood."
    },
    {
      "name": "booking",
      "description": "Book a live demo of the Snaprint S1 kiosk."
    },
    {
      "name": "registration",
      "description": "Shop-owner sign-up for Snaprint Desktop."
    },
    {
      "name": "community",
      "description": "r/Snaprint community feed."
    }
  ],
  "paths": {
    "/api/locations/areas": {
      "get": {
        "operationId": "listAreas",
        "tags": [
          "locations"
        ],
        "summary": "List all live neighbourhood areas",
        "description": "Returns every live Bengaluru neighbourhood with its listed print/xerox shops (name, address, coordinates, phone, rating). Cached for 1 hour.",
        "responses": {
          "200": {
            "description": "All live areas.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "count",
                    "areas"
                  ],
                  "properties": {
                    "count": {
                      "type": "integer",
                      "minimum": 0
                    },
                    "areas": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Area"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/locations/{slug}": {
      "get": {
        "operationId": "getArea",
        "tags": [
          "locations"
        ],
        "summary": "Get one neighbourhood area by slug",
        "description": "Returns a single live area and its listed print/xerox shops. Get valid slugs from listAreas.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Area slug, e.g. `rajajinagar`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The area.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Area"
                }
              }
            }
          },
          "404": {
            "description": "No live area with that slug (code `not_found`).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/book/providers": {
      "get": {
        "operationId": "listBookingProviders",
        "tags": [
          "booking"
        ],
        "summary": "List founders available for demo calls",
        "description": "Returns the team members who can be booked for a demo. Use `id` as `providerId` in createBooking.",
        "responses": {
          "200": {
            "description": "Available providers.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "providers"
                  ],
                  "properties": {
                    "providers": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Provider"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (5 requests per 60 s per IP across /api/book/*).",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "502": {
            "description": "Booking backend unavailable (code `upstream_unavailable`).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/book/availability": {
      "get": {
        "operationId": "getBookingAvailability",
        "tags": [
          "booking"
        ],
        "summary": "List open demo slots for a date",
        "description": "Returns open demo time slots on the given date across all providers.",
        "parameters": [
          {
            "name": "date",
            "in": "query",
            "required": true,
            "description": "Date in YYYY-MM-DD.",
            "schema": {
              "type": "string",
              "format": "date",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Open slots, sorted by time.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "slots"
                  ],
                  "properties": {
                    "slots": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Slot"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or malformed `date` (code `invalid_field`).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (5 requests per 60 s per IP across /api/book/*).",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "502": {
            "description": "Booking backend unavailable (code `upstream_unavailable`).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/book/submit": {
      "post": {
        "operationId": "createBooking",
        "tags": [
          "booking"
        ],
        "summary": "Book a demo slot",
        "description": "Books a demo slot previously returned by getBookingAvailability. Same-origin only: the Origin header must match the request host. Body max 8 KB.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "email",
                  "phone",
                  "date",
                  "time",
                  "providerId"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Full name, 1-120 chars, no digits or brackets.",
                    "maxLength": 120
                  },
                  "email": {
                    "type": "string",
                    "description": "Contact email.",
                    "format": "email"
                  },
                  "phone": {
                    "type": "string",
                    "description": "Phone, 6-32 chars of digits, spaces, + ( ) -.",
                    "pattern": "^[+0-9 ()\\-]{6,32}$"
                  },
                  "date": {
                    "type": "string",
                    "description": "Slot date, YYYY-MM-DD, today or later.",
                    "format": "date"
                  },
                  "time": {
                    "type": "string",
                    "description": "Slot time, HH:mm.",
                    "pattern": "^\\d{2}:\\d{2}$"
                  },
                  "providerId": {
                    "type": "integer",
                    "description": "Provider id from the chosen slot."
                  }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Booking created.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id"
                  ],
                  "properties": {
                    "id": {
                      "type": "integer",
                      "description": "Appointment id."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid JSON or field (codes `invalid_json`, `invalid_field`).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "Cross-origin request (code `forbidden_origin`).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Slot no longer available (code `slot_unavailable`).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "Body too large (code `payload_too_large`).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "415": {
            "description": "Content-Type is not application/json (code `unsupported_media_type`).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (5 requests per 60 s per IP across /api/book/*).",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "502": {
            "description": "Booking backend unavailable (code `upstream_unavailable`).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/desktop-registrations": {
      "post": {
        "operationId": "registerDesktopShop",
        "tags": [
          "registration"
        ],
        "summary": "Register a shop for Snaprint Desktop",
        "description": "Registers a print/xerox shop owner for Snaprint Desktop. Credentials are not returned; the team contacts the owner. Same-origin only: the Origin header must match the request host. Body max 4 KB.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "shopName",
                  "ownerName",
                  "phone",
                  "email"
                ],
                "properties": {
                  "shopName": {
                    "type": "string",
                    "description": "Shop name, 1-120 chars.",
                    "maxLength": 120
                  },
                  "ownerName": {
                    "type": "string",
                    "description": "Owner name, 1-120 chars.",
                    "maxLength": 120
                  },
                  "phone": {
                    "type": "string",
                    "description": "Indian mobile number, 10 digits, optional +91 prefix."
                  },
                  "email": {
                    "type": "string",
                    "description": "Owner email.",
                    "format": "email"
                  },
                  "city": {
                    "type": "string",
                    "description": "City name, letters/spaces/.'- only.",
                    "maxLength": 80
                  }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Registered.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid JSON or field (codes `invalid_json`, `invalid_field`).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "Cross-origin request (code `forbidden_origin`).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "Body too large (code `payload_too_large`).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "415": {
            "description": "Content-Type is not application/json (code `unsupported_media_type`).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Upstream registration rate limit hit (code `rate_limited`).",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "502": {
            "description": "Registration backend unavailable (code `upstream_unavailable`).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/reddit/feed": {
      "get": {
        "operationId": "getCommunityFeed",
        "tags": [
          "community"
        ],
        "summary": "Get recent r/Snaprint posts",
        "description": "Returns recent non-pinned posts from r/Snaprint, newest first. Cached for 5 minutes.",
        "responses": {
          "200": {
            "description": "Posts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "posts"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "posts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RedditPost"
                      }
                    }
                  }
                }
              }
            }
          },
          "502": {
            "description": "Reddit unavailable (code `upstream_unavailable`); body also has `ok: false`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "RateLimit": {
        "description": "Remaining quota, e.g. `\"book\";r=4;t=59` (r = requests left, t = seconds until reset).",
        "schema": {
          "type": "string"
        }
      },
      "RateLimit-Policy": {
        "description": "Quota policy, `\"book\";q=5;w=60` (5 requests per 60 s).",
        "schema": {
          "type": "string"
        }
      },
      "Retry-After": {
        "description": "Seconds to wait before retrying.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details with Snaprint extensions.",
        "required": [
          "type",
          "title",
          "status",
          "detail",
          "code",
          "error"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "description": "Link to the error's documentation."
          },
          "title": {
            "type": "string",
            "description": "Short human-readable summary of the error code."
          },
          "status": {
            "type": "integer",
            "description": "HTTP status code."
          },
          "detail": {
            "type": "string",
            "description": "Human-readable explanation of this occurrence."
          },
          "code": {
            "type": "string",
            "description": "Machine-readable error code.",
            "enum": [
              "invalid_json",
              "invalid_field",
              "forbidden_origin",
              "not_found",
              "slot_unavailable",
              "payload_too_large",
              "unsupported_media_type",
              "rate_limited",
              "upstream_unavailable"
            ]
          },
          "hint": {
            "type": "string",
            "description": "How to resolve the error."
          },
          "error": {
            "type": "string",
            "description": "Same as `detail` (kept for older clients)."
          }
        }
      },
      "Area": {
        "type": "object",
        "required": [
          "slug",
          "name",
          "city",
          "pinCodes",
          "intro",
          "keywords",
          "presence",
          "lastReviewed",
          "liveLocations"
        ],
        "properties": {
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "Neighbourhood display name."
          },
          "city": {
            "type": "string",
            "description": "Parent city slug, e.g. `bengaluru`."
          },
          "pinCodes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "intro": {
            "type": "string"
          },
          "keywords": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presence": {
            "type": "string",
            "enum": [
              "live",
              "planned",
              "served"
            ]
          },
          "lastReviewed": {
            "type": "string",
            "format": "date"
          },
          "liveLocations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Shop"
            }
          }
        }
      },
      "Shop": {
        "type": "object",
        "description": "A listed print/xerox shop (curated from public listings, not a Snaprint partner).",
        "required": [
          "name",
          "address",
          "lat",
          "lng",
          "phone",
          "rating",
          "reviews",
          "placeId"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "phone": {
            "type": [
              "string",
              "null"
            ]
          },
          "rating": {
            "type": [
              "number",
              "null"
            ]
          },
          "reviews": {
            "type": "integer"
          },
          "placeId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Google Maps place id."
          }
        }
      },
      "Provider": {
        "type": "object",
        "required": [
          "id",
          "firstName"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "firstName": {
            "type": "string"
          }
        }
      },
      "Slot": {
        "type": "object",
        "required": [
          "time",
          "providerId",
          "providerName"
        ],
        "properties": {
          "time": {
            "type": "string",
            "pattern": "^\\d{2}:\\d{2}$",
            "description": "HH:mm."
          },
          "providerId": {
            "type": "integer"
          },
          "providerName": {
            "type": "string"
          }
        }
      },
      "RedditPost": {
        "type": "object",
        "required": [
          "id",
          "title",
          "preview",
          "author",
          "createdUtc",
          "numComments",
          "score",
          "flair",
          "permalink",
          "isOfficial"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "preview": {
            "type": "string"
          },
          "author": {
            "type": "string"
          },
          "createdUtc": {
            "type": "integer",
            "description": "Unix seconds."
          },
          "numComments": {
            "type": "integer"
          },
          "score": {
            "type": "integer"
          },
          "flair": {
            "type": [
              "string",
              "null"
            ]
          },
          "permalink": {
            "type": "string",
            "format": "uri"
          },
          "isOfficial": {
            "type": "boolean"
          }
        }
      }
    }
  }
}
