{
	"openapi": "3.0.3",
	"info": {
		"title": "PhoneBlock API",
		"description": "Access to the PhoneBlock database for third-party tools. Please use the test installation at https://phoneblock.net/pb-test/api/ for testing purpose!",
		"version": "v1.6.0"
	},
	"servers": [
		{
			"url": "https://phoneblock.net/pb-test/api"
		},
		{
			"url": "https://phoneblock.net/phoneblock/api"
		}
	],
	"paths": {
		"/num/{num}": {
			"get": {
				"summary": "Checks whether the given phone number in on the blocklist.",
				"description": "The calling phone number is sent in plaintext — the server learns which number the authenticated user is asking about. For a privacy-preserving alternative that hides the queried number from the server, use the k-anonymity prefix lookup at /check-prefix.\n\nAuthentication is optional on this endpoint. When the request carries a valid bearer token, the response additionally honors the user's personal blocklist/whitelist for that number and the `userComment` field is populated from the user's own previously-submitted comment (if any).",
				"responses": {
					"200": {
						"description": "Checks whether the given phone number is in the PhoneBlock database.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/PhoneInfo"
								},
								"example": {
									"phone": "+49123456789",
									"votes": 42,
									"votesWildcard": 120,
									"rating": "C_POLL",
									"whiteListed": false,
									"blackListed": false,
									"archived": false,
									"dateAdded": 1700000000000,
									"lastUpdate": 1710000000000,
									"label": "(DE) 0123 456789",
									"location": "Berlin"
								}
							}
						}
					}
				},
				"security": [
					{
						"PhoneBlockUser": [],
						"APIKey": []
					}
				]
			},
			"parameters": [
				{
					"name": "num",
					"in": "path",
					"description": "Phone number to inspect",
					"required": true,
					"schema": {
						"type": "string"
					}
				},
				{
					"name": "format",
					"in": "query",
					"description": "The format to return.",
					"required": false,
					"default": "json",
					"schema": {
						"type": "string",
						"enum": [
							"json",
							"xml"
						]
					}
				}
			]
		},
		"/check": {
			"get": {
				"summary": "Checks a phone number hash against the SPAM database. This API provides better privacy than the direct lookup with the phone number.",
				"description": "Clients hash the calling number locally with SHA1 and send only the hash, so the server never sees the plaintext number. For maximum privacy — where the server cannot link the authenticated user to a specific number at all — use /check-prefix (k-anonymity prefix lookup) instead: the full SHA1 still uniquely identifies the queried number against a rainbow table.",
				"responses": {
					"200": {
						"description": "Checks whether the phone number with the given SHA1 hash is in the PhoneBlock database. If the exact number is not found but prefix hashes are provided, range-based detection is attempted.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/PhoneInfo"
								},
								"example": {
									"phone": "+49123456789",
									"votes": 42,
									"votesWildcard": 120,
									"rating": "C_POLL",
									"whiteListed": false,
									"blackListed": false,
									"archived": false,
									"dateAdded": 1700000000000,
									"lastUpdate": 1710000000000,
									"label": "(DE) 0123 456789",
									"location": "Berlin"
								}
							}
						}
					}
				},
				"security": [
					{
						"PhoneBlockUser": [],
						"APIKey": []
					}
				]
			},
			"parameters": [
				{
					"name": "sha1",
					"in": "query",
					"description": "The hex-encoded SHA1 hash (40 hex digits) of the calling phone number to check for SPAM. The number to hash must be in international format (e.g. '3D1D76F0C3664E1E818C6ECCFD8843AD1F4091CC' for the German phone number '+4917650642602'). You can use the /hash API for testing and debugging your hash computation.",
					"required": true,
					"schema": {
						"type": "string",
						"example": "3D1D76F0C3664E1E818C6ECCFD8843AD1F4091CC"
					}
				},
				{
					"name": "prefix10",
					"in": "query",
					"description": "Optional hex-encoded SHA1 hash (40 hex digits) of the phone number with the last digit removed. Used for range-based spam detection when the exact number is not in the database.",
					"required": false,
					"schema": {
						"type": "string"
					}
				},
				{
					"name": "prefix100",
					"in": "query",
					"description": "Optional hex-encoded SHA1 hash (40 hex digits) of the phone number with the last two digits removed. Used for range-based spam detection when the exact number is not in the database.",
					"required": false,
					"schema": {
						"type": "string"
					}
				},
				{
					"name": "format",
					"in": "query",
					"description": "The format to return.",
					"required": false,
					"default": "json",
					"schema": {
						"type": "string",
						"enum": [
							"json",
							"xml"
						]
					}
				}
			]
		},
		"/check-prefix": {
			"get": {
				"summary": "Privacy-preserving k-anonymity lookup against the SPAM database.",
				"description": "Instead of a full SHA1 hash (as used by /check) the client sends only the first 4 or more hex characters of the hash. The server responds with every matching entry so the client can match locally — the server never learns which specific number the client was asking about. Optional prefix10/prefix100 parameters enable range-based spam detection. When a match is on the user's personal block- or whitelist, the server merges that information into the returned entry (flags 'blackListed' / 'whiteListed').\n\nFair-use contribution: because this endpoint hides which number the client asked about, the server no longer sees activity data that it needs to keep community blocklists tailored (especially space-constrained variants for devices like Fritz!Box routers). Well-behaved clients that get a positive local match are expected to report the plaintext calling number back via POST /report-call/{phone}, which lets the server increment the call counter without having to learn the full set of queried numbers.",
				"responses": {
					"200": {
						"description": "All entries whose SHA1 hash starts with the supplied prefix(es).",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/PrefixCheckResult"
								},
								"example": {
									"numbers": [
										{
											"phone": "+49123456789",
											"votes": 42,
											"rating": "C_POLL",
											"whiteListed": false,
											"blackListed": false,
											"archived": false,
											"dateAdded": 1700000000000,
											"lastUpdate": 1710000000000,
											"label": "(DE) 0123 456789",
											"location": "Berlin"
										}
									],
									"range10":  [ { "prefix": "+4912345678", "votes": 24, "cnt": 5 } ],
									"range100": [ { "prefix": "+491234567",  "votes": 78, "cnt": 12 } ]
								}
							}
						}
					},
					"400": {
						"description": "Missing 'sha1' parameter, or a prefix value is not between 4 and 40 hex characters (even length)."
					},
					"401": {
						"description": "Authentication required — unlike the cache-friendly HIBP-style endpoints this API requires a bearer token. The server must know the user to apply personal block/whitelist entries, and authenticating individual queries does not weaken privacy because the hash prefix alone covers thousands of candidate numbers. Only API keys are accepted: session cookies and Basic auth (username/password) are rejected for this endpoint so that only machine clients holding a dedicated token can reach it."
					}
				},
				"security": [
					{
						"APIKey": []
					}
				]
			},
			"parameters": [
				{
					"name": "sha1",
					"in": "query",
					"description": "Hex prefix of the SHA1 hash of the phone number (4–40 hex characters, even length). 4 hex characters (16 bit) keep on average ~3000 plausible German numbers per bucket and are the recommended minimum. Longer prefixes are accepted — 40 characters is equivalent to an exact lookup like /check.",
					"required": true,
					"schema": {
						"type": "string",
						"example": "3d1d"
					}
				},
				{
					"name": "prefix10",
					"in": "query",
					"description": "Optional hex prefix (4–40 hex characters, even length) of the SHA1 hash of the phone number with the last digit removed. Needed for 10-digit range-based spam detection.",
					"required": false,
					"schema": {
						"type": "string"
					}
				},
				{
					"name": "prefix100",
					"in": "query",
					"description": "Optional hex prefix (4–40 hex characters, even length) of the SHA1 hash of the phone number with the last two digits removed. Needed for 100-digit range-based spam detection.",
					"required": false,
					"schema": {
						"type": "string"
					}
				},
				{
					"name": "format",
					"in": "query",
					"description": "The format to return.",
					"required": false,
					"default": "json",
					"schema": {
						"type": "string",
						"enum": [
							"json",
							"xml"
						]
					}
				}
			]
		},
		"/report-call/{phone}": {
			"post": {
				"summary": "Report an incoming SPAM call for blocklist-tailoring statistics.",
				"description": "Intended to be invoked after a client (e.g. a light-weight device that uses /check-prefix) has identified an incoming call as SPAM. Increments NUMBERS.CALLS and refreshes NUMBERS.LASTPING for known blocklist numbers so that the server knows which numbers are currently active and can select them for space-constrained tailored blocklists.\n\nThis is not a rating — /rate accepts at most one rating per (user, number); /report-call accepts many events for the same number and exists to measure activity, not opinion.\n\nAbuse control: every user may contribute at most 20 reports to the global counter per UTC day. Further reports on the same day still appear in the user's own call history but do not move the global counter.\n\nReports for numbers that are not in the blocklist database are silently accepted but have no effect — the server never creates new rows from report-call input.",
				"responses": {
					"204": {
						"description": "Report accepted. The body is empty. The response does not tell whether the number was in the database or whether the global counter was actually updated — that is by design, so clients cannot probe the blocklist contents via this endpoint."
					},
					"400": {
						"description": "Missing or invalid phone number in the URL."
					},
					"401": {
						"description": "Authentication required. Only API keys (bearer tokens with the 'rate' privilege) are accepted; session cookies and Basic auth are rejected for this endpoint."
					}
				},
				"security": [
					{
						"APIKey": []
					}
				]
			},
			"parameters": [
				{
					"name": "phone",
					"in": "path",
					"description": "The calling phone number in international format. To avoid URL-encoding the leading '+', clients may equivalently send the legacy '00'-prefix form (e.g. '0049301234567' instead of '+49301234567').",
					"required": true,
					"schema": {
						"type": "string",
						"example": "0049301234567"
					}
				}
			]
		},
		"/search/{num}": {
			"get": {
				"summary": "Retrieve extensive information for the given phone number.",
				"responses": {
					"200": {
						"description": "Retrieves information for the given phone number from the PhoneBlock database.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/SearchResult"
								},
								"example": {
									"phoneId": "⚡0123456789",
									"info": {
										"phone": "+49123456789",
										"votes": 42,
										"votesWildcard": 120,
										"rating": "C_POLL",
										"whiteListed": false,
										"blackListed": false,
										"archived": false,
										"dateAdded": 1700000000000,
										"lastUpdate": 1710000000000,
										"label": "(DE) 0123 456789",
										"location": "Berlin"
									},
									"topRating": "G_FRAUD",
									"ratings": {
										"G_FRAUD": 30,
										"E_ADVERTISING": 12
									},
									"searches": [0, 1, 3, 5, 2, 0, 0, 0, 0, 0, 0, 0],
									"aiSummary": "Aggressive spam caller attempting fraud."
								}
							}
						}
					}
				},
				"security": [
					{
						"PhoneBlockUser": [],
						"APIKey": []
					}
				]
			},
			"parameters": [
				{
					"name": "num",
					"in": "path",
					"description": "Phone number to inspect",
					"required": true,
					"schema": {
						"type": "string"
					}
				},
				{
					"name": "format",
					"in": "query",
					"description": "The format to return.",
					"required": false,
					"default": "json",
					"schema": {
						"type": "string",
						"enum": [
							"json",
							"xml"
						]
					}
				}
			]
		},
		"/blocklist": {
			"get": {
				"summary": "Retrieves the blocklist with optional incremental synchronization",
				"description": "Returns all active phone numbers from the blocklist with their actual vote counts. Numbers below the server's minimum vote threshold are excluded from the full blocklist and returned with votes=0 in incremental updates (indicating removal). When 'since' is omitted, returns the complete blocklist. When 'since' is provided, returns only changes since that version (incremental sync). Entries with votes=0 indicate the number should be removed from the client's local blocklist.\n\n**Important:** This endpoint returns only the community-maintained blocklist data. It does NOT include user-specific personalizations (personal blacklist/whitelist). For call filtering, clients must check BOTH the community blocklist AND the user's personal black/whitelist entries (retrieved via /api/blacklist and /api/whitelist). Personal whitelist entries should override community blocklist entries, and personal blacklist entries should block calls regardless of community ratings.\n\n**Rate Limits:** To avoid excessive server load, clients should limit full synchronization (without 'since' parameter) to a maximum of once per month. Incremental synchronization (with 'since' parameter) should be performed at most once per day. Clients exceeding these limits may be subject to rate limiting.\n\n**Implementation:** Perform an initial full sync to retrieve the complete blocklist and its version number. Store this data locally and save the version number. For subsequent updates, use the saved version number in the 'since' parameter to retrieve only changes. Apply the incremental updates to your local copy: add/update entries with votes > 0, remove entries with votes = 0. After each sync, update your saved version number to the one returned in the response.",
				"responses": {
					"200": {
						"description": "Returns all active phone numbers from the PhoneBlock database with their actual vote counts. Numbers with votes=0 indicate removal.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"properties": {
										"numbers": {
											"type": "array",
											"items": {
												"type": "object",
												"properties": {
													"phone": {
														"type": "string"
													},
													"rating": {
														"type": "string",
														"enum": [
															"A_LEGITIMATE",
															"B_MISSED",
															"C_PING",
															"D_POLL",
															"E_ADVERTISING",
															"F_GAMBLE",
															"G_FRAUD"
														]
													},
													"votes": {
														"type": "integer",
														"description": "Actual vote count. A value of 0 indicates the number should be removed from the client's local blocklist."
													},
													"lastActivity": {
														"type": "integer",
														"format": "int64",
														"description": "Timestamp (milliseconds since epoch) of the last activity for this number"
													}
												}
											}
										},
										"version": {
											"type": "integer",
											"description": "Current version of the blocklist for incremental synchronization"
										}
									}
								},
								"example": {
									"numbers": [
										{
											"phone": "+49123456789",
											"rating": "G_FRAUD",
											"votes": 4
										},
										{
											"phone": "+390456789123",
											"rating": "F_GAMBLE",
											"votes": 10
										}
									],
									"version": 42
								}
							}
						}
					}
				},
				"security": [
					{
						"PhoneBlockUser": [],
						"APIKey": []
					}
				]
			},
			"parameters": [
				{
					"name": "since",
					"in": "query",
					"description": "Optional version number for incremental synchronization. When provided, returns only changes since the specified version. Entries with votes=0 indicate the number dropped below the minimum threshold and should be removed from the client's local blocklist.",
					"required": false,
					"schema": {
						"type": "integer"
					}
				},
				{
					"name": "format",
					"in": "query",
					"description": "The format to return.",
					"required": false,
					"default": "json",
					"schema": {
						"type": "string",
						"enum": [
							"json",
							"xml"
						]
					}
				}
			]
		},
		"/register": {
			"get": {
				"summary": "Retrieve information required to start a user registration",
				"responses": {
					"200": {
						"description": "Starts a new registration session. The session ID must be added to all following requests during the user registration. The captcha data is a Base64 encoded image that hides a text that must be added to the registration information. ",
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"properties": {
										"session": {
											"type": "string"
										},
										"captcha": {
											"type": "string"
										}
									}
								},
								"example": {
									"session": "ehWfZJpRTJ9dQLemiVgY",
									"captcha": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPoAAAA8CAIAAADAPzDDAAAizElEQVR4Xu3deVyN+f/w8fdpX1Ra0EYpSjEhMogosrRIoVCKbNkZ2xCyFIqsmXJJvoTQlkSy..."
								}
							}
						}
					}
				}
			},
			"post": {
				"summary": "Register a new user",
				"description": "During registration, a user provides his e-mail address and an answer to a captcha challenge. The captcha challenge is a Base64 encoded image that must be retrieved with a GET request to the same URI prior to registration.",
				"requestBody": {
					"description": "User information to start the registration with.",
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"type": "object",
								"properties": {
									"session": {
										"type": "string"
									},
									"answer": {
										"type": "string"
									},
									"email": {
										"type": "string"
									}
								}
							},
							"example": {
								"session": "ehWfZJpRTJ9dQLemiVgY",
								"answer": "ABCDEFG123",
								"email": "whoami@company.de"
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "An e-mail was successfully sent to the provided e-mail address. The e-mail contains a registration code, that the user must provide to the final call to the verify request to receive the login credentials.",
						"content": {
							"text/plain; charset=utf-8": {
								"schema": {
									"type": "string"
								}
							}
						}
					}
				}
			}
		},
		"/verify": {
			"post": {
				"summary": "Verifies a user registration.",
				"description": "A user registration must be verified by providing the code sent to the user's e-mail address. The login credentials required to access protected PhoneBlock resources is created as response to the verification call.",
				"requestBody": {
					"description": "Information that verifies that the user owns the provided e-mail address.",
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"type": "object",
								"properties": {
									"session": {
										"type": "string"
									},
									"code": {
										"type": "string"
									}
								}
							},
							"example": {
								"session": "xxxx",
								"code": "123456"
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "The user registration has completed successfully. The user may now use the provided credentials to access protected parts of the PhoneBlock API or log in to the web page to customize settings.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"properties": {
										"session": {
											"type": "string"
										},
										"login": {
											"type": "string"
										},
										"password": {
											"type": "string"
										}
									}
								},
								"example": {
									"session": "ehWfZJpRTJ9dQLemiVgY",
									"login": "6d8f7262-faa6-41bc-adb4-50a6013c2b07",
									"password": "5s4df6s5df"
								}
							}
						}
					}
				}
			}
		},
		"/ratings": {
			"get": {
				"summary": "Retrieve all available rating codes",
				"responses": {
					"200": {
						"description": null,
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"properties": {
										"values": {
											"type": "array",
											"item": {
												"type": "string"
											}
										}
									}
								},
								"example": {
									"values": [
										"A_LEGITIMATE",
										"B_MISSED",
										"C_PING",
										"D_POLL",
										"E_ADVERTISING",
										"F_GAMBLE",
										"G_FRAUD"
									]
								}
							}
						}
					}
				}
			},
			"parameters": [
				{
					"name": "format",
					"in": "query",
					"description": "The format to return.",
					"required": false,
					"default": "json",
					"schema": {
						"type": "string",
						"enum": [
							"json",
							"xml"
						]
					}
				}
			]
		},
		"/rate": {
			"post": {
				"summary": "Stores a user rating for a phone number",
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"type": "object",
								"properties": {
									"phone": {
										"type": "string"
									},
									"rating": {
										"type": "string"
									},
									"comment": {
										"type": "string"
									}
								}
							},
							"example": {
								"phone": "+49123456789",
								"rating": "C_PING",
								"comment": "Anrufer hat sofort aufgelegt."
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "The rating has been stored successfully.",
						"content": {
							"text/plain; charset=utf-8": {
								"schema": {
									"type": "string"
								}
							}
						}
					}
				},
				"security": [
					{
						"PhoneBlockUser": [],
						"APIKey": []
					}
				]
			}
		},
		"/hash": {
			"get": {
				"summary": "Demonstrates hash computation for the /check API (for testing and debugging only, otherwise there is no privacy benefit).",
				"responses": {
					"200": {
						"description": "Computes the encoded SHA1 hash of the given phone number. The phone number is normalized to international format before hash computation. E.g. for '004917650642602', or '017650642602' the hash of '+4917650642602' is computed.",
						"content": {
							"text/plain": {
								"schema": {
									"type": "string"
								},
								"example": "3D1D76F0C3664E1E818C6ECCFD8843AD1F4091CC"
							}
						}
					}
				}
			},
			"parameters": [
				{
					"name": "phone",
					"in": "query",
					"description": "The phone number to hash.",
					"required": true,
					"schema": {
						"type": "string"
					}
				}
			]
		},
		"/test": {
			"get": {
				"summary": "Checks connection and authentication. Call to this API can be used when setting up a PhoneBlock connection in a third-party app to verify the user has provided valid credentials.",
				"responses": {
					"200": {
						"description": "Checks provided authentication token and returns 'ok' on success.",
						"content": {
							"text/plain": {
								"schema": {
									"type": "string"
								},
								"example": "ok"
							}
						}
					},
					"401": {
						"description": "If no or invalid credentials have been provided.",
						"content": {
							"text/plain": {
								"schema": {
									"type": "string"
								},
								"example": "Please provide login credentials."
							}
						}
					}
				},
				"security": [
					{
						"APIKey": []
					}
				]
			},
			"parameters": []
		},
		"/account": {
			"get": {
				"summary": "Retrieves the current user's account settings.",
				"description": "Returns language preference, country dial prefix, display name, and email address for the authenticated user.",
				"responses": {
					"200": {
						"description": "Account settings retrieved successfully.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/AccountSettings"
								},
								"example": {
									"lang": "de-DE",
									"dialPrefix": "+49",
									"displayName": "John Doe",
									"email": "john.doe@example.com"
								}
							}
						}
					},
					"401": {
						"description": "If no or invalid credentials have been provided.",
						"content": {
							"text/plain": {
								"schema": {
									"type": "string"
								},
								"example": "Please provide login credentials."
							}
						}
					},
					"404": {
						"description": "User settings not found.",
						"content": {
							"text/plain": {
								"schema": {
									"type": "string"
								},
								"example": "User settings not found"
							}
						}
					}
				},
				"security": [
					{
						"APIKey": []
					}
				]
			},
			"put": {
				"summary": "Updates the current user's account settings.",
				"description": "Updates language preference, country dial prefix, or display name. All fields are optional. If countryCode is provided, the server converts it to the corresponding dial prefix.",
				"requestBody": {
					"description": "Account settings to update. All fields are optional.",
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/UpdateAccountRequest"
							},
							"examples": {
								"updateLanguage": {
									"summary": "Update language preference",
									"value": {
										"lang": "en-US"
									}
								},
								"updateCountryCode": {
									"summary": "Update country via country code",
									"value": {
										"countryCode": "DE"
									}
								},
								"updateDialPrefix": {
									"summary": "Update dial prefix directly",
									"value": {
										"dialPrefix": "+49"
									}
								},
								"updateAll": {
									"summary": "Update all fields",
									"value": {
										"lang": "de-DE",
										"countryCode": "DE",
										"displayName": "John Doe"
									}
								}
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "Account settings updated successfully. Returns the updated settings.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/AccountSettings"
								},
								"example": {
									"lang": "de-DE",
									"dialPrefix": "+49",
									"displayName": "John Doe",
									"email": "john.doe@example.com"
								}
							}
						}
					},
					"400": {
						"description": "Invalid request. Returns error message describing the validation failure.",
						"content": {
							"text/plain": {
								"schema": {
									"type": "string"
								},
								"examples": {
									"invalidLang": {
										"value": "Invalid language tag format. Expected format: 'de' or 'en-US'"
									},
									"invalidCountryCode": {
										"value": "Invalid country code. Expected ISO 3166-1 alpha-2 format (e.g., 'DE', 'US', 'BR')"
									},
									"invalidDialPrefix": {
										"value": "Invalid dial prefix format. Expected format: '+XX' (e.g., '+49', '+1', '+351')"
									},
									"emptyDisplayName": {
										"value": "Display name must not be empty"
									}
								}
							}
						}
					},
					"401": {
						"description": "If no or invalid credentials have been provided.",
						"content": {
							"text/plain": {
								"schema": {
									"type": "string"
								},
								"example": "Please provide login credentials."
							}
						}
					}
				},
				"security": [
					{
						"APIKey": []
					}
				]
			},
			"parameters": []
		},
		"/blacklist": {
			"get": {
				"summary": "Retrieves the current user's personalized blacklist.",
				"description": "Returns a list of phone numbers that the user has explicitly marked as blocked (blacklist). Numbers are added to the blacklist by rating them via the POST /api/rate endpoint with spam ratings such as E_ADVERTISING, F_GAMBLE, or G_FRAUD. This endpoint only retrieves the list; use the rating API to add numbers.",
				"responses": {
					"200": {
						"description": "Blocklist retrieved successfully.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/NumberList"
								},
								"example": {
									"numbers": [
										{
											"phone": "+491234567890",
											"label": "(DE) 01234 567890",
											"comment": "Spam caller",
											"rating": "G_FRAUD",
											"created": 1709913600000
										},
										{
											"phone": "+491234567891",
											"label": "(DE) 01234 567891",
											"comment": null,
											"rating": null,
											"created": 1709913600000
										}
									]
								}
							}
						}
					},
					"401": {
						"description": "If no or invalid credentials have been provided.",
						"content": {
							"text/plain": {
								"schema": {
									"type": "string"
								},
								"example": "Please provide login credentials."
							}
						}
					},
					"404": {
						"description": "User not found.",
						"content": {
							"text/plain": {
								"schema": {
									"type": "string"
								},
								"example": "User not found"
							}
						}
					}
				},
				"security": [
					{
						"APIKey": []
					}
				]
			}
		},
		"/blacklist/{phone}": {
			"delete": {
				"summary": "Removes a phone number from the user's blacklist.",
				"description": "Deletes the specified phone number from the user's personalized blacklist. To add numbers to the blacklist, use POST /api/rate.",
				"parameters": [
					{
						"name": "phone",
						"in": "path",
						"required": true,
						"description": "The phone number to remove from the blacklist (e.g., +491234567890)",
						"schema": {
							"type": "string"
						},
						"example": "+491234567890"
					}
				],
				"responses": {
					"204": {
						"description": "Phone number removed from blacklist successfully."
					},
					"401": {
						"description": "If no or invalid credentials have been provided.",
						"content": {
							"text/plain": {
								"schema": {
									"type": "string"
								},
								"example": "Please provide login credentials."
							}
						}
					},
					"404": {
						"description": "Phone number not found in personalization list.",
						"content": {
							"text/plain": {
								"schema": {
									"type": "string"
								},
								"example": "Phone number not found in personalization list"
							}
						}
					}
				},
				"security": [
					{
						"APIKey": []
					}
				]
			},
			"put": {
				"summary": "Updates the comment for a phone number in the user's blacklist.",
				"description": "Updates the user's comment for the specified phone number in their personalized blacklist.",
				"parameters": [
					{
						"name": "phone",
						"in": "path",
						"required": true,
						"description": "The phone number to update the comment for (e.g., +491234567890)",
						"schema": {
							"type": "string"
						},
						"example": "+491234567890"
					}
				],
				"requestBody": {
					"description": "Updated comment for the phone number",
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/PersonalizedNumber"
							},
							"example": {
								"phone": "+491234567890",
								"comment": "Updated spam caller description"
							}
						}
					}
				},
				"responses": {
					"204": {
						"description": "Comment updated successfully."
					},
					"401": {
						"description": "If no or invalid credentials have been provided.",
						"content": {
							"text/plain": {
								"schema": {
									"type": "string"
								},
								"example": "Please provide login credentials."
							}
						}
					},
					"404": {
						"description": "Phone number not found in personalization list.",
						"content": {
							"text/plain": {
								"schema": {
									"type": "string"
								},
								"example": "Phone number not found in personalization list"
							}
						}
					}
				},
				"security": [
					{
						"APIKey": []
					}
				]
			}
		},
		"/whitelist": {
			"get": {
				"summary": "Retrieves the current user's personalized whitelist.",
				"description": "Returns a list of phone numbers that the user has explicitly marked as legitimate (whitelist). Numbers are added to the whitelist by rating them via the POST /api/rate endpoint with the A_LEGITIMATE rating. This endpoint only retrieves the list; use the rating API to add numbers.",
				"responses": {
					"200": {
						"description": "Whitelist retrieved successfully.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/NumberList"
								},
								"example": {
									"numbers": [
										{
											"phone": "+491234567893",
											"label": "(DE) 01234 567893",
											"comment": "My doctor",
											"rating": "A_LEGITIMATE",
											"created": 1709913600000
										},
										{
											"phone": "+491234567894",
											"label": "(DE) 01234 567894",
											"comment": null,
											"rating": null,
											"created": 1709913600000
										}
									]
								}
							}
						}
					},
					"401": {
						"description": "If no or invalid credentials have been provided.",
						"content": {
							"text/plain": {
								"schema": {
									"type": "string"
								},
								"example": "Please provide login credentials."
							}
						}
					},
					"404": {
						"description": "User not found.",
						"content": {
							"text/plain": {
								"schema": {
									"type": "string"
								},
								"example": "User not found"
							}
						}
					}
				},
				"security": [
					{
						"APIKey": []
					}
				]
			}
		},
		"/whitelist/{phone}": {
			"delete": {
				"summary": "Removes a phone number from the user's whitelist.",
				"description": "Deletes the specified phone number from the user's personalized whitelist. To add numbers to the whitelist, use POST /api/rate.",
				"parameters": [
					{
						"name": "phone",
						"in": "path",
						"required": true,
						"description": "The phone number to remove from the whitelist (e.g., +491234567893)",
						"schema": {
							"type": "string"
						},
						"example": "+491234567893"
					}
				],
				"responses": {
					"204": {
						"description": "Phone number removed from whitelist successfully."
					},
					"401": {
						"description": "If no or invalid credentials have been provided.",
						"content": {
							"text/plain": {
								"schema": {
									"type": "string"
								},
								"example": "Please provide login credentials."
							}
						}
					},
					"404": {
						"description": "Phone number not found in personalization list.",
						"content": {
							"text/plain": {
								"schema": {
									"type": "string"
								},
								"example": "Phone number not found in personalization list"
							}
						}
					}
				},
				"security": [
					{
						"APIKey": []
					}
				]
			},
			"put": {
				"summary": "Updates the comment for a phone number in the user's whitelist.",
				"description": "Updates the user's comment for the specified phone number in their personalized whitelist.",
				"parameters": [
					{
						"name": "phone",
						"in": "path",
						"required": true,
						"description": "The phone number to update the comment for (e.g., +491234567893)",
						"schema": {
							"type": "string"
						},
						"example": "+491234567893"
					}
				],
				"requestBody": {
					"description": "Updated comment for the phone number",
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/PersonalizedNumber"
							},
							"example": {
								"phone": "+491234567893",
								"comment": "Updated doctor's office description"
							}
						}
					}
				},
				"responses": {
					"204": {
						"description": "Comment updated successfully."
					},
					"401": {
						"description": "If no or invalid credentials have been provided.",
						"content": {
							"text/plain": {
								"schema": {
									"type": "string"
								},
								"example": "Please provide login credentials."
							}
						}
					},
					"404": {
						"description": "Phone number not found in personalization list.",
						"content": {
							"text/plain": {
								"schema": {
									"type": "string"
								},
								"example": "Phone number not found in personalization list"
							}
						}
					}
				},
				"security": [
					{
						"APIKey": []
					}
				]
			}
		}
	},
	"components": {
		"schemas": {
			"RangeMatch": {
				"type": "object",
				"description": "A single aggregated SPAM range matching a hash prefix in a PrefixCheckResult.",
				"properties": {
					"prefix": {
						"type": "string",
						"description": "The plaintext prefix of the aggregated range (e.g. '+4930123')."
					},
					"votes": {
						"type": "integer",
						"description": "Aggregated number of SPAM votes for all numbers in this range."
					},
					"cnt": {
						"type": "integer",
						"description": "Number of distinct SPAM numbers known for this range."
					}
				}
			},
			"PrefixCheckResult": {
				"type": "object",
				"description": "Response of the privacy-preserving k-anonymity prefix lookup. Each array contains every entry whose SHA1 hash starts with the corresponding prefix from the request; the client matches them against its own (locally-hashed) number to decide whether the caller is on the blocklist.",
				"properties": {
					"numbers": {
						"type": "array",
						"description": "Phone numbers whose SHA1 hash starts with the 'sha1' prefix from the request. The per-number 'votes' is the raw direct-vote count — the client computes the wildcard-combined value locally from 'range10' / 'range100'.",
						"items": {
							"$ref": "#/components/schemas/PhoneInfo"
						}
					},
					"range10": {
						"type": "array",
						"description": "10-digit range aggregations whose SHA1 hash starts with the 'prefix10' prefix from the request. Only blocks that have crossed the server-side wildcard-vote threshold are returned, matching the behavior of /check; sparsely-populated ranges are filtered out. Empty if no 'prefix10' was supplied or no qualifying match was found.",
						"items": {
							"$ref": "#/components/schemas/RangeMatch"
						}
					},
					"range100": {
						"type": "array",
						"description": "100-digit range aggregations whose SHA1 hash starts with the 'prefix100' prefix from the request. Only blocks that have crossed the server-side wildcard-vote threshold are returned, matching the behavior of /check; sparsely-populated ranges are filtered out. Empty if no 'prefix100' was supplied or no qualifying match was found.",
						"items": {
							"$ref": "#/components/schemas/RangeMatch"
						}
					}
				}
			},
			"PhoneInfo": {
				"type": "object",
				"description": "Information about a phone number published by the PhoneBlock API.",
				"properties": {
					"phone": {
						"type": "string",
						"description": "The number being requested."
					},
					"votes": {
						"type": "integer",
						"description": "The number of votes that support blocking the requested number."
					},
					"votesWildcard": {
						"type": "integer",
						"description": "Combined votes from the phone number's spam range, whereas 'votes' counts only the exact number. 'votesWildcard' exceeds 'votes' when neighboring numbers have been reported too (a spammer rotating through a range) and can be non-zero even when 'votes' is 0.\n\nHow a range is recognized: PhoneBlock groups reported numbers into a 10-block (all numbers sharing every digit except the last) and a 100-block (all numbers sharing every digit except the last two). A number counts toward its block only once it has a positive net vote balance (at least one more spam than legitimate vote). A block becomes a recognized spam range when a 10-block holds at least 4 such numbers, or a 100-block holds at least 3 qualifying 10-blocks. 'votesWildcard' then sums the votes of the whole matching block.\n\nExample - one report each on 20 consecutive numbers: '...000' to '...019' fills two 10-blocks with 10 numbers each; both cross the 4-number threshold, so every number gets a 10-block wildcard rating, but the 100-block has only 2 qualifying sub-blocks (< 3) and is not rated. '...005' to '...024' instead spreads across three 10-blocks (5/10/5 numbers); all three qualify, so the 100-block becomes a spam range too. Consecutive numbers alone therefore do not trigger a 100-block rating - the reports must reach 4 or more numbers in each of 3 different 10-blocks (minimum 12 numbers)."
					},
					"rating": {
						"type": "string",
						"description": "The rating for the requested phone number.",
						"enum": [
							"A_LEGITIMATE",
							"B_MISSED",
							"C_PING",
							"D_POLL",
							"E_ADVERTISING",
							"F_GAMBLE",
							"G_FRAUD"
						]
					},
					"whiteListed": {
						"type": "boolean",
						"description": "Whether this number is on the global white list and therefore cannot receive votes."
					},
					"blackListed": {
						"type": "boolean",
						"description": "Whether this number is on the requesting user's personal block list. Only set in the /check endpoint response."
					},
					"archived": {
						"type": "boolean",
						"description": "Whether this number no longer is on the blocklist, because no votes have been received for a long time."
					},
					"dateAdded": {
						"type": "integer",
						"format": "int64",
						"description": "Date when this number was added to the SPAM database (in milliseconds since epoch)."
					},
					"lastUpdate": {
						"type": "integer",
						"format": "int64",
						"description": "Date when the last report for this number was received (in milliseconds since epoch)."
					},
					"label": {
						"type": "string",
						"description": "The phone number formatted for local display (e.g., '(DE) 030 12345678').",
						"nullable": true
					},
					"location": {
						"type": "string",
						"description": "The city or region from where the call originated (e.g., 'Berlin').",
						"nullable": true
					},
					"userComment": {
						"type": "string",
						"description": "The comment that the requesting user previously stored for this number, or null. Only populated when the request is authenticated and a comment was previously submitted by the same user (e.g., via the /rate endpoint). This is the user's own note about the number, not a community comment.",
						"nullable": true
					}
				}
			},
			"SearchResult": {
				"type": "object",
				"description": "Extensive information about a phone number including community ratings, comments, and search history.",
				"properties": {
					"phoneId": {
						"type": "string",
						"description": "The phone ID in database format."
					},
					"info": {
						"$ref": "#/components/schemas/PhoneInfo"
					},
					"topRating": {
						"type": "string",
						"description": "The most common rating for this number.",
						"enum": [
							"A_LEGITIMATE",
							"B_MISSED",
							"C_PING",
							"D_POLL",
							"E_ADVERTISING",
							"F_GAMBLE",
							"G_FRAUD"
						],
						"nullable": true
					},
					"ratings": {
						"type": "object",
						"description": "Map of rating codes to vote counts.",
						"additionalProperties": {
							"type": "integer"
						}
					},
					"comments": {
						"type": "array",
						"description": "User comments about this number.",
						"items": {
							"type": "object",
							"properties": {
								"rating": {
									"type": "string",
									"enum": [
										"A_LEGITIMATE",
										"B_MISSED",
										"C_PING",
										"D_POLL",
										"E_ADVERTISING",
										"F_GAMBLE",
										"G_FRAUD"
									]
								},
								"comment": {
									"type": "string"
								},
								"created": {
									"type": "integer",
									"format": "int64",
									"description": "Timestamp in milliseconds since epoch."
								},
								"up": {
									"type": "integer",
									"description": "Number of upvotes."
								},
								"down": {
									"type": "integer",
									"description": "Number of downvotes."
								}
							}
						}
					},
					"searches": {
						"type": "array",
						"description": "Monthly search activity counts (12 entries, most recent first).",
						"items": {
							"type": "integer"
						}
					},
					"aiSummary": {
						"type": "string",
						"description": "AI-generated summary of user comments about this number.",
						"nullable": true
					},
					"relatedNumbers": {
						"type": "array",
						"description": "Related phone numbers in the same range.",
						"items": {
							"type": "string"
						}
					},
					"prev": {
						"type": "string",
						"description": "Previous phone number in the database.",
						"nullable": true
					},
					"next": {
						"type": "string",
						"description": "Next phone number in the database.",
						"nullable": true
					}
				}
			},
			"UpdateAccountRequest": {
				"type": "object",
				"description": "Request to update user account settings. All fields are optional.",
				"properties": {
					"lang": {
						"type": "string",
						"description": "Preferred language tag (e.g., 'de', 'en-US', 'pt-BR')",
						"example": "de-DE",
						"nullable": true
					},
					"dialPrefix": {
						"type": "string",
						"description": "User's country dial prefix (e.g., '+49', '+1', '+351')",
						"example": "+49",
						"nullable": true
					},
					"displayName": {
						"type": "string",
						"description": "User's display name",
						"example": "John Doe",
						"nullable": true
					},
					"countryCode": {
						"type": "string",
						"description": "ISO 3166-1 alpha-2 country code (e.g., 'DE', 'US', 'BR'). If provided, the server will convert it to the corresponding dial prefix.",
						"example": "DE",
						"nullable": true
					}
				}
			},
			"AccountSettings": {
				"type": "object",
				"description": "User account settings",
				"properties": {
					"lang": {
						"type": "string",
						"description": "Preferred language tag",
						"example": "de-DE",
						"nullable": true
					},
					"dialPrefix": {
						"type": "string",
						"description": "User's country dial prefix",
						"example": "+49",
						"nullable": true
					},
					"displayName": {
						"type": "string",
						"description": "User's display name",
						"example": "John Doe",
						"nullable": true
					},
					"email": {
						"type": "string",
						"description": "User's email address",
						"example": "john.doe@example.com",
						"nullable": true
					}
				}
			},
			"NumberList": {
				"type": "object",
				"description": "A list of phone numbers with optional comments (used for blacklist/whitelist responses)",
				"properties": {
					"numbers": {
						"type": "array",
						"description": "List of phone numbers with optional comments",
						"items": {
							"$ref": "#/components/schemas/PersonalizedNumber"
						}
					}
				}
			},
			"PersonalizedNumber": {
				"type": "object",
				"description": "A phone number with optional user comment, display label, and rating",
				"properties": {
					"phone": {
						"type": "string",
						"description": "The phone number in international format",
						"example": "+491234567890"
					},
					"label": {
						"type": "string",
						"description": "The phone number formatted for local display (e.g., '(DE) 01234 567890')",
						"example": "(DE) 01234 567890",
						"nullable": true
					},
					"comment": {
						"type": "string",
						"description": "User's comment for this number (may be null)",
						"example": "Spam caller",
						"nullable": true
					},
					"rating": {
						"type": "string",
						"description": "The user's personal rating for this number (may be null if no rating was given)",
						"enum": [
							"A_LEGITIMATE",
							"B_MISSED",
							"C_PING",
							"D_POLL",
							"E_ADVERTISING",
							"F_GAMBLE",
							"G_FRAUD"
						],
						"nullable": true
					},
					"created": {
						"type": "integer",
						"format": "int64",
						"description": "Timestamp when this entry was added to the personalization list (milliseconds since epoch).",
						"example": 1709913600000
					}
				},
				"required": [
					"phone"
				]
			}
		},
		"securitySchemes": {
			"PhoneBlockUser": {
				"description": "A PhoneBlock user name and its password. The method of authorization is deprecated. Better use an API key.",
				"type": "http",
				"scheme": "Basic"
			},
			"APIKey": {
				"description": "A PhoneBlock API key created for API access. You can create one from the settings page.",
				"type": "http",
				"scheme": "Bearer"
			}
		}
	}
}
