openapi: 3.0.4 info: title: Spothole API description: |- Spothole is a utility to aggregate "spots" from amateur radio DX clusters and xOTA spotting sites, and provide an open JSON API as well as a website to browse the data. While there are other web-based interfaces to DX clusters, and sites that aggregate spots from various outdoor activity programmes for amateur radio, Spothole differentiates itself by supporting a large number of data sources, and by being "API first" rather than just providing a web front-end. This allows other software to be built on top of it. Spothole itself is also open source, Public Domain licenced code that anyone can take and modify. contact: email: ian@ianrenton.com license: name: The Unlicense url: https://unlicense.org/#the-unlicense version: v1 servers: - url: https://spothole.app/api/v1 paths: /spots: get: tags: - spots summary: Get spots description: The main API call that retrieves spots from the system. Supply this with no query parameters to retrieve all spots known to the system. Supply query parameters to filter what is retrieved. operationId: spots parameters: - name: limit in: query description: Limit the number of spots in the response required: false schema: type: integer - name: since in: query description: Limit the spots to only ones at this time or later. Time in UTC seconds since UNIX epoch. Equivalent to "max_age" but saves the client having to work out how many seconds ago "midnight" was. required: false schema: type: number - name: max_age in: query description: Limit the spots to only ones received in the last 'n' seconds. Equivalent to "since" but saves the client having to work out what time was 'n' seconds ago on every call. Refer to the "max_spot_age" in the /options call to figure out what the maximum useful value you can provide is. Larger values will still be accepted, there just won't be any spots in the system older than max_spot_age. required: false schema: type: number - name: received_since in: query description: Limit the spots to only ones that the system found out about at this time or later. Time in UTC seconds since UNIX epoch. If you are using a front-end that tracks the last time it queried the API and requests spots since then, you want *this* version of the query parameter, not "since", because otherwise it may miss things. The logic is "greater than" rather than "greater than or equal to", so you can submit the time of the last received item back to this call and you will get all the more recent spots back, without duplicating the previous latest spot. required: false schema: type: number - name: source in: query description: "Limit the spots to only ones from one or more sources. To select more than one source, supply a comma-separated list." required: false schema: type: string enum: - POTA - SOTA - WWFF - WWBOTA - GMA - HEMA - ParksNPeaks - Cluster - RBN - APRS-IS - name: sig in: query description: "Limit the spots to only ones from one or more Special Interest Groups provided as an argument. To select more than one SIG, supply a comma-separated list." required: false schema: type: string enum: - POTA - SOTA - WWFF - WWBOTA - GMA - HEMA - WCA - MOTA - SiOTA - ARLHS - ILLW - ZLOTA - IOTA - name: needs_sig in: query description: "Limit the spots to only ones with a Special Interest Group such as POTA. Because supplying all known SIGs as a `sigs` parameter is unwieldy, and leaving `sigs` blank will also return spots with *no* SIG, this parameter can be set true to return only spots with a SIG, regardless of what it is, so long as it's not blank. This is what Field Spotter uses to exclude generic cluster spots and only retrieve xOTA things." required: false schema: type: boolean default: false - name: needs_sig_ref in: query description: "Limit the spots to only ones which have at least one reference (e.g. a park reference) for Special Interest Groups such as POTA." required: false schema: type: boolean default: false - name: band in: query description: "Limit the spots to only ones from one or more bands. To select more than one band, supply a comma-separated list." required: false schema: type: string enum: - 160m - 80m - 60m - 40m - 30m - 20m - 17m - 15m - 12m - 10m - 6m - 4m - 2m - 70cm - 23cm - 13cm - name: mode in: query description: "Limit the spots to only ones from one or more modes. To select more than one mode, supply a comma-separated list." required: false schema: type: string enum: - CW - PHONE - SSB - USB - LSB - AM - FM - DV - DMR - DSTAR - C4FM - M17 - DIGI - DATA - FT8 - FT4 - RTTY - SSTV - JS8 - HELL - BPSK - PSK - BPSK31 - OLIVIA - name: mode_type in: query description: "Limit the spots to only ones from one or more mode families. To select more than one mode family, supply a comma-separated list." required: false schema: type: string enum: - CW - PHONE - DATA - name: dx_continent in: query description: "Limit the spots to only ones where the DX (the operator being spotted) is on the given continent(s). To select more than one continent, supply a comma-separated list." required: false schema: type: string enum: - EU - NA - SA - AS - AF - OC - AN - name: de_continent in: query description: "Limit the spots to only ones where the spotteris on the given continent(s). To select more than one continent, supply a comma-separated list." required: false schema: type: string enum: - EU - NA - SA - AS - AF - OC - AN - name: dedupe in: query description: "\"De-duplicate\" the spots, returning only the latest spot for any given callsign." required: false schema: type: boolean default: false - name: comment_includes in: query description: "Return only spots where the comment includes the provided string (case-insensitive)." required: false schema: type: string - name: needs_good_location in: query description: "Return only spots with a 'good' location. (See the spot `dx_location_good` parameter for details. Useful for map-based clients, to avoid spots with 'bad' locations e.g. loads of cluster spots ending up in the centre of the DXCC entitity.)" required: false schema: type: boolean default: false - name: allow_qrt in: query description: Allow spots that are known to be QRT to be returned. required: false schema: type: boolean default: true responses: '200': description: Success content: application/json: schema: type: array items: $ref: '#/components/schemas/Spot' /alerts: get: tags: - alerts summary: Get alerts description: Retrieves alerts (indications of upcoming activations) from the system. Supply this with no query parameters to retrieve all alerts known to the system. Supply query parameters to filter what is retrieved. operationId: spots parameters: - name: limit in: query description: Limit the number of alerts in the response required: false schema: type: integer - name: received_since in: query description: Limit the alerts to only ones that the system found out about at this time or later. Time in UTC seconds since UNIX epoch. If you are using a front-end that tracks the last time it queried the API and requests alerts since then, you want *this* version of the query parameter, not "since", because otherwise it may miss things. The logic is "greater than" rather than "greater than or equal to", so you can submit the time of the last received item back to this call and you will get all the more recent alerts back, without duplicating the previous latest spot. required: false schema: type: integer - name: max_duration in: query description: Limit the alerts to only ones with a duration of this many seconds or less. Duration is end time minus start time, if end time is set, otherwise the activation is assumed to be short and therefore to always pass this check. This is useful to filter out people who alert POTA activations lasting months or even years, but note it will also include multi-day or multi-week DXpeditions that you might otherwise be interested in. See the dxpeditions_skip_max_duration_check parameter for the workaround. required: false schema: type: integer - name: dxpeditions_skip_max_duration_check in: query description: Return DXpedition alerts even if they last longer than max_duration. This allows the user to filter out multi-day/multi-week POTA alerts where the operator likely won't be on the air most of the time, but keep multi-day/multi-week DXpeditions where the operator(s) likely *will* be on the air most of the time. required: false schema: type: boolean - name: source in: query description: "Limit the alerts to only ones from one or more sources. To select more than one source, supply a comma-separated list." required: false schema: type: string enum: - POTA - SOTA - WWFF - WWBOTA - GMA - HEMA - ParksNPeaks - Cluster - RBN - APRS-IS - name: sig in: query description: "Limit the alerts to only ones from one or more Special Interest Groups. To select more than one SIG, supply a comma-separated list." required: false schema: type: string enum: - POTA - SOTA - WWFF - WWBOTA - GMA - HEMA - WCA - MOTA - SiOTA - ARLHS - ILLW - ZLOTA - IOTA - name: dx_continent in: query description: "Limit the alerts to only ones where the DX (the operator being spotted) is on the given continent(s). To select more than one continent, supply a comma-separated list." required: false schema: type: string enum: - EU - NA - SA - AS - AF - OC - AN responses: '200': description: Success content: application/json: schema: type: array items: $ref: '#/components/schemas/Alert' /status: get: tags: - general summary: Get server status description: Query information about the server for use in a diagnostics display. operationId: status responses: '200': description: Success content: application/json: schema: type: object properties: "software-version": type: string description: The version number of the software. example: "1.0.1" "server-owner-callsign": type: string description: The callsign of this server's operator. example: "M0TRT" "uptime_sec": type: integer description: The amount of time the software has been running for, in seconds. example: 12345 "mem_use_mb": type: number description: The amount of memory the software is using, in megabytes. example: 123.456 "num_spots": type: integer description: Number of spots currently in the system. example: 123 "num_alerts": type: integer description: Number of alerts currently in the system. example: 123 "cleanup": type: object properties: status: type: string description: The status of the cleanup thread example: OK last_ran: type: number description: The last time the cleanup operation ran, UTC seconds since UNIX epoch. example: 1759579508 "webserver": type: object properties: status: type: string description: The status of the web server example: OK last_page_access: type: number description: The last time a page was accessed on the web server, UTC seconds since UNIX epoch. example: 1759579508 last_api_access: type: number description: The last time an API endpoint was accessed on the web server, UTC seconds since UNIX epoch. example: 1759579508 spot_providers: type: array description: An array of all the spot providers. items: $ref: '#/components/schemas/SpotProviderStatus' alert_providers: type: array description: An array of all the alert providers. items: $ref: '#/components/schemas/AlertProviderStatus' /options: get: tags: - general summary: Get enumeration options description: Retrieves the list of options for various enumerated types, which can be found in the spots and also provided back to the API as query parameters. While these enumerated options are defined in this spec anyway, providing them in an API call allows us to define extra parameters, like the colours associated with bands, and also allows clients to set up their filters and features without having to have internal knowledge about, for example, what bands the server knows about. operationId: options responses: '200': description: Success content: application/json: schema: type: object properties: bands: type: array description: An array of all the supported bands. items: $ref: '#/components/schemas/Band' modes: type: array description: An array of all the supported modes. items: type: string example: "LSB" mode_types: type: array description: An array of all the supported mode types. items: type: string example: "PHONE" sigs: type: array description: An array of all the supported Special Interest Groups. items: $ref: '#/components/schemas/SIG' sources: type: array description: An array of all the supported data sources. items: type: string example: "Cluster" continents: type: array description: An array of all the supported continents. items: type: string example: "EU" max_spot_age: type: integer description: The maximum age, in seconds, of any spot before it will be deleted by the system. When querying the /api/v1/spots endpoint and providing a "max_age" or "since" parameter, there is no point providing a number larger than this, because the system drops all spots older than this. example: 3600 spot_allowed: type: boolean description: Whether the POST /spot call, to add spots to the server directly via its API, is permitted on this server. example: true /spot: post: tags: - spots summary: Add a spot description: "Supply a new spot object, which will be added to the system. Currently, this will not be reported up the chain to a cluster, POTA, SOTA etc. This will be introduced in a future version. cURL example: `curl --request POST --header \"Content-Type: application/json\" --data '{\"dx_call\":\"M0TRT\",\"time\":1760019539, \"freq\":14200000, \"comment\":\"Test spot please ignore\", \"de_call\":\"M0TRT\"}' https://spothole.app/api/v1/spot`" operationId: spot requestBody: description: The JSON spot object required: true content: application/json: schema: $ref: '#/components/schemas/Spot' responses: '200': description: Success content: application/json: schema: type: string example: "OK" '415': description: Incorrect Content-Type content: application/json: schema: type: string example: "Failed" '422': description: Validation error content: application/json: schema: type: string example: "Failed" '500': description: Internal server error content: application/json: schema: type: string example: "Failed" components: schemas: Spot: type: object properties: id: type: string description: Unique identifier based on a hash of the spot to distinguish this one from any others. example: 442c5d56ac467341f1943e8596685073b38f5a5d4c3802ca1e16ecf98967956c dx_call: type: string description: Callsign of the operator that has been spotted example: M0TRT dx_name: type: string description: Name of the operator that has been spotted example: Ian dx_country: type: string description: Country of the DX operator example: United Kingdom dx_flag: type: string description: Country flag of the DX operator example: "" dx_continent: type: string description: Continent of the DX operator enum: - EU - NA - SA - AS - AF - OC - AN example: EU dx_dxcc_id: type: integer description: DXCC ID of the DX operator example: 235 dx_cq_zone: type: integer description: CQ zone of the DX operator example: 27 dx_itu_zone: type: integer description: ITU zone of the DX operator example: 14 dx_aprs_ssid: type: string description: If this is an APRS spot, what SSID was the DX operator using? example: "" dx_grid: type: string description: Maidenhead grid locator for the DX spot. This could be from a geographical reference e.g. POTA, or just from the country example: IO91aa dx_latitude: type: number description: Latitude of the DX spot, in degrees. This could be from a geographical reference e.g. POTA, or from a QRZ lookup example: 51.2345 dx_longitude: type: number description: Longitude of the DX spot, in degrees. This could be from a geographical reference e.g. POTA, or from a QRZ lookup example: -1.2345 dx_location_source: type: string description: Where we got the DX location (grid/latitude/longitude) from. If this was from the spot itself, it's likely quite accurate, but if we had to fall back to QRZ lookup, or even a location based on the DXCC itself, it will be a lot less accurate. enum: - SPOT - "WAB/WAI GRID" - QRZ - DXCC - NONE example: SPOT dx_location_good: type: boolean description: Does the software think the location is good enough to put a marker on a map? This is true if the source is "SPOT" or "WAB/WAI GRID", or alternatively if the source is "QRZ" and the callsign doesn't have a slash in it (i.e. operator likely at home). example: true de_call: type: string description: Callsign of the operator that has spotted them example: M0TEST de_country: type: string description: Country of the spotter example: United Kingdom de_flag: type: string description: Country flag of the spotter example: "" de_continent: type: string enum: - EU - NA - SA - AS - AF - OC - AN description: Continent of the spotter example: EU de_dxcc_id: type: integer description: DXCC ID of the spotter example: 235 de_grid: type: string description: Maidenhead grid locator for the spotter. This is not going to be from a xOTA reference so it will likely just be a QRZ or DXCC lookup. If the spotter is also portable, this is probably wrong, but it's good enough for some simple mapping. example: IO91aa de_latitude: type: number description: Latitude of the spotter, in degrees. This is not going to be from a xOTA reference so it will likely just be a QRZ or DXCC lookup. If the spotter is also portable, this is probably wrong, but it's good enough for some simple mapping. example: 51.2345 de_longitude: type: number description: Longitude of the DX spotspotter, in degrees. This is not going to be from a xOTA reference so it will likely just be a QRZ or DXCC lookup. If the spotter is also portable, this is probably wrong, but it's good enough for some simple mapping. example: -1.2345 mode: type: string description: Reported mode. enum: - CW - PHONE - SSB - USB - LSB - AM - FM - DV - DMR - DSTAR - C4FM - M17 - DIGI - DATA - FT8 - FT4 - RTTY - SSTV - JS8 - HELL - BPSK - PSK - BPSK31 - OLIVIA example: SSB mode_type: type: string description: Inferred mode "family". enum: - CW - PHONE - DATA example: PHONE mode_source: type: string description: Where we got the mode from. If this was from the spot itself, it's likely quite accurate, but if we had to fall back to the bandplan, it might not be correct. enum: - SPOT - COMMENT - BANDPLAN - NONE freq: type: number description: Frequency, in Hz example: 7150500 band: type: string description: Band, defined by the frequency. enum: - 160m - 80m - 60m - 40m - 30m - 20m - 17m - 15m - 12m - 10m - 6m - 4m - 2m - 70cm - 23cm - 13cm - Unknown example: 40m time: type: number description: Time of the spot, UTC seconds since UNIX epoch example: 1759579508 time_iso: type: string description: Time of the spot, ISO 8601 example: "2025-10-05T12:34:56.789Z" received_time: type: number description: Time that this software received the spot, UTC seconds since UNIX epoch. This is used with the "since_received" call to our API to receive all data that is new to us, even if by a quirk of the API it might be older than the list time the client polled the API. example: 1759579508 received_time_iso: type: string description: Time that this software received the spot, ISO 8601 example: "2025-10-05T12:34:56.789Z" comment: type: string description: Comment left by the spotter, if any example: "59 in NY 73" sig: type: string description: Special Interest Group (SIG), e.g. outdoor activity programme such as POTA enum: - POTA - SOTA - WWFF - WWBOTA - GMA - HEMA - WCA - MOTA - SiOTA - ARLHS - ILLW - ZLOTA - IOTA example: POTA sig_refs: type: array items: type: string description: SIG references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO example: GB-0001 sig_refs_names: type: array items: type: string description: SIG reference names example: Null Country Park sig_refs_urls: type: array items: type: string description: SIG reference URLs, which the user can look up for more information example: "https://pota.app/#/park/GB-0001" activation_score: type: integer description: Activation score. SOTA only example: 0 icon: type: string descripton: Icon, from the Font Awesome set. This is fairly opinionated but is here to help the Spothole web UI and Field Spotter. Does not include the "fa-" prefix. example: tree band_color: type: string descripton: Colour to represent this spot, if a client chooses to colour spots based on their frequency band, using PSK Reporter's default colours. HTML colour e.g. hex. example: #ff0000" band_contrast_color: type: string descripton: Black or white, whichever best contrasts with "band_color". example: "white" qrt: type: boolean description: QRT state. Some APIs return spots marked as QRT. Otherwise we can check the comments. example: false source: type: string description: Where we got the spot from. enum: - POTA - SOTA - WWFF - WWBOTA - GMA - HEMA - ParksNPeaks - Cluster - RBN - APRS-IS example: POTA source_id: type: string description: The ID the source gave it, if any. example: "GUID-123456" Alert: type: object properties: id: type: string description: Unique identifier based on a hash of the alert to distinguish this one from any others. example: 442c5d56ac467341f1943e8596685073b38f5a5d4c3802ca1e16ecf98967956c dx_calls: type: array description: Callsigns of the operator(s) that are going to be activating items: type: string example: M0TRT dx_names: type: array description: Names of the operator(s) that are going to be activating items: type: string example: Ian dx_country: type: string description: Country of the DX operator. This, and the subsequent fields, assume that all activators will be in the same country! example: United Kingdom dx_flag: type: string description: Country flag of the DX operator example: "" dx_continent: type: string description: Continent of the DX operator enum: - EU - NA - SA - AS - AF - OC - AN example: EU dx_dxcc_id: type: integer description: DXCC ID of the DX operator example: 235 dx_cq_zone: type: integer description: CQ zone of the DX operator example: 27 dx_itu_zone: type: integer description: ITU zone of the DX operator example: 14 freqs_modes: type: string description: An indication of the frequencies and modes that the activation will use, if provided. example: "7-ssb 14-ssb maybe 145-fm" start_time: type: number description: Time that the activation is expected to start, UTC seconds since UNIX epoch example: 1759579508 start_time_iso: type: string description: Time that the activation is expected to start, ISO 8601 example: "2025-10-05T12:34:56.789Z" end_time: type: number description: Time that the activation is expected to star, UTC seconds since UNIX epoch example: 1759579508 end_time_iso: type: string description: Time that the activation is expected to star, ISO 8601 example: "2025-10-05T12:34:56.789Z" received_time: type: number description: Time that this software received the alert, UTC seconds since UNIX epoch. This is used with the "since_received" call to our API to receive all data that is new to us, even if by a quirk of the API it might be older than the list time the client polled the API. example: 1759579508 received_time_iso: type: string description: Time that this software received the alert, ISO 8601 example: "2025-10-05T12:34:56.789Z" comment: type: string description: Comment made by the activator, if any example: "2025 DXpedition to null island" sig: type: string description: Special Interest Group (SIG), e.g. outdoor activity programme such as POTA enum: - POTA - SOTA - WWFF - WWBOTA - GMA - HEMA - WCA - MOTA - SiOTA - ARLHS - ILLW - ZLOTA - IOTA example: POTA sig_refs: type: array items: type: string description: SIG references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO example: GB-0001 sig_refs_names: type: array items: type: string description: SIG reference names example: Null Country Park activation_score: type: integer description: Activation score. SOTA only example: 0 icon: type: string descripton: Icon, from the Font Awesome set. This is fairly opinionated but is here to help the Spothole web UI and Field Spotter. Does not include the "fa-" prefix. example: tree source: type: string description: Where we got the alert from. enum: - POTA - SOTA - WWFF - WWBOTA - GMA - HEMA - ParksNPeaks - Cluster - RBN - APRS-IS example: POTA source_id: type: string description: The ID the source gave it, if any. example: "GUID-123456" SpotProviderStatus: type: object properties: name: type: string description: The name of the provider. example: POTA enabled: type: boolean description: Whether the provider is enabled or not. example: true status: type: string description: The status of the provider. example: OK last_updated: type: number description: The last time at which this provider received data, UTC seconds since UNIX epoch. example: 1759579508 last_spot: type: number description: The time of the latest spot received by this provider, UTC seconds since UNIX epoch. example: 1759579508 AlertProviderStatus: type: object properties: name: type: string description: The name of the provider. example: POTA enabled: type: boolean description: Whether the provider is enabled or not. example: true status: type: string description: The status of the provider. example: OK last_updated: type: number description: The last time at which this provider received data, UTC seconds since UNIX epoch. example: 1759579508 Band: type: object properties: name: type: string description: The name of the band example: 40m start_freq: type: int description: The start frequency of this band, in Hz. example: 7000000 end_freq: type: int description: The end frequency of this band, in Hz. example: 7200000 color: type: string description: The color associated with this mode, as used on PSK Reporter. example: "#5959ff" contrast_color: type: string description: Black or white, whichever provides the best contrast against the band colour. example: white SIG: type: object properties: name: type: string description: The abbreviated name of the SIG example: POTA description: type: string description: The full name of the SIG example: Parks on the Air icon: type: string description: Icon, from the Font Awesome set. This is fairly opinionated but is here to help the Spothole web UI and Field Spotter. Does not include the "fa-" prefix. example: tree ref_regex: type: string description: Regex that matches this SIG's reference IDs. Generally for Spothole's own internal use, clients probably won't need this. example: "[A-Z]{2}\\-\\d+"