{
  "openapi": "3.1.0",
  "info": {
    "title": "Attest register API",
    "version": "1.0.0",
    "summary": "South African trades businesses checked against their trade's public register, as dated attestations.",
    "description": "Query-shaped by design: there is no bulk export. Read every outcome as what was found at the source on the date shown. Businesses found on no register, with no other evidence of qualification, are withheld by default and counted in `withheld`; include=not_found returns them. Absence is weak evidence, because PIRB registration and ECB listing are voluntary. For plumbers, each response carries a jurisdiction block citing the municipal by-law that decides whether a Certificate of Compliance is required and who may issue it; where it says unknown, treat it as unknown. For electricians, coc_authority is never derived. Results are ordered by evidence tier and rotated within tier (declared in each response's ordering field); the order is not a ranking of quality. Full reading guide: https://attest.africa/llms.txt. The same query is available as an MCP server at https://attest.africa/mcp.",
    "contact": {
      "email": "hello@attest.africa",
      "url": "https://attest.africa"
    }
  },
  "servers": [
    {
      "url": "https://attest.africa"
    }
  ],
  "security": [
    {},
    {
      "bearer": []
    },
    {
      "attestKey": []
    }
  ],
  "paths": {
    "/api/search": {
      "get": {
        "operationId": "searchAttestedBusinesses",
        "summary": "Search attested plumbers, electricians, financial advisers, insurance brokers, debt counsellors, gas installers, tax practitioners, beetle inspectors, solar installers, security providers and psychologists and counsellors in South Africa",
        "description": "Anonymous callers get up to 10 results and 60 requests an hour per IP. A key raises this to 50 results and 600 requests an hour; agent hosts should hold their own, from hello@attest.africa.",
        "parameters": [
          {
            "name": "trade",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "plumber",
                "electrician",
                "financial_adviser",
                "insurance_broker",
                "debt_counsellor",
                "gas_installer",
                "tax_practitioner",
                "beetle_inspector",
                "solar_installer",
                "security_provider",
                "psychologist"
              ],
              "default": "plumber"
            },
            "description": " Defaults to plumber, except in a name search, where it is optional."
          },
          {
            "name": "name",
            "in": "query",
            "description": "Business name, at least 3 letters or digits, to look one business up. Searches every profession unless trade is given (trade then has no default); region narrows it. Exact names first, then names starting with the text, then names containing it; area, town and status are ignored. A business missing from the results has not been checked yet, which is not evidence it is unregistered.",
            "schema": {
              "type": "string",
              "minLength": 3
            },
            "example": "Marshall Plumbing"
          },
          {
            "name": "town",
            "in": "query",
            "description": "Town or suburb. Substring match on the town or the address, so \"cape town\" matches every metro suburb.",
            "schema": {
              "type": "string"
            },
            "example": "Paarl"
          },
          {
            "name": "area",
            "in": "query",
            "description": "Service area, exact match.",
            "schema": {
              "type": "string"
            },
            "example": "Cape Town Metro"
          },
          {
            "name": "region",
            "in": "query",
            "description": "Province slug.",
            "schema": {
              "type": "string"
            },
            "example": "western-cape"
          },
          {
            "name": "status",
            "in": "query",
            "description": "Only results with this outcome. status=not_found returns businesses that are otherwise withheld.",
            "schema": {
              "type": "string",
              "enum": [
                "verified",
                "consistent",
                "lapsed",
                "ambiguous",
                "not_found"
              ]
            }
          },
          {
            "name": "include",
            "in": "query",
            "description": "include=not_found returns businesses found on no register and with no other evidence of qualification. They are withheld by default, because an assistant given one tends to call the business unregistered, and absence under a trading name is not evidence of that.",
            "schema": {
              "type": "string",
              "enum": [
                "not_found"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Attestation objects for the query, with the ordering declaration and, for plumbers, the jurisdiction block.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid status value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Retry-After gives the seconds to wait.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "An Attest API key."
      },
      "attestKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Attest-Key"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "retry_after_seconds": {
            "type": "integer"
          }
        },
        "required": [
          "error"
        ]
      },
      "SearchResponse": {
        "type": "object",
        "properties": {
          "query": {
            "type": "object",
            "description": "The query as interpreted, after normalisation and the tier's limit."
          },
          "total": {
            "type": "integer",
            "description": "Matches before the limit."
          },
          "returned": {
            "type": "integer"
          },
          "ordering": {
            "type": "string",
            "description": "How these results were ordered, and the fairness promise behind it."
          },
          "match_note": {
            "type": "string",
            "description": "Present when a town is asked for: what each result's match value means (ADR 043)."
          },
          "jurisdiction": {
            "type": "array",
            "description": "Plumbers only: the rule for each municipality the results fall in, with the by-law and the date read.",
            "items": {
              "$ref": "#/components/schemas/Jurisdiction"
            }
          },
          "jurisdiction_note": {
            "type": "string"
          },
          "outcomes": {
            "type": "object",
            "description": "What each outcome value means.",
            "additionalProperties": {
              "type": "string"
            }
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Listing"
            }
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "docs": {
            "type": "string",
            "format": "uri"
          },
          "contact": {
            "type": "string"
          },
          "withheld": {
            "type": "object",
            "description": "Present when businesses were withheld. A business missing from results is not thereby unregistered or unqualified.",
            "properties": {
              "not_found": {
                "type": "integer"
              },
              "note": {
                "type": "string"
              }
            }
          }
        },
        "required": [
          "query",
          "total",
          "returned",
          "ordering",
          "outcomes",
          "results",
          "generated_at"
        ]
      },
      "Jurisdiction": {
        "type": "object",
        "properties": {
          "municipality": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "plumbing_coc_required": {
            "type": "string",
            "description": "required, not_required or unknown. Unknown means Attest has not read the by-law."
          },
          "coc_issuer_rule": {
            "type": "string"
          },
          "plumber_register_required": {
            "type": "string"
          },
          "pirb_sufficient": {
            "type": "string"
          },
          "source": {
            "type": "string"
          },
          "read_on": {
            "type": "string",
            "format": "date-time"
          },
          "note": {
            "type": "string"
          }
        }
      },
      "Listing": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "subject": {
            "type": "object",
            "description": "The business. Contact details come from public sources; practitioner names and registration numbers never appear.",
            "properties": {
              "type": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "trade": {
                "type": "string"
              },
              "town": {
                "type": "string",
                "description": "The suburb or town in the business's own Google address (e.g. Pinelands), else the city."
              },
              "service_area": {
                "type": "string",
                "description": "The pool ADR 016 rotates results within. It can be wider than town (Cape Town Metro covers many suburbs)."
              },
              "region": {
                "type": "string"
              },
              "region_slug": {
                "type": "string"
              },
              "country": {
                "type": "string"
              },
              "address": {
                "type": "string"
              },
              "phone": {
                "type": "string"
              },
              "website": {
                "type": "string"
              },
              "google_place_id": {
                "type": "string"
              },
              "whatsapp": {
                "type": "string",
                "description": "Owner-supplied; published only once it matches the Google listing's phone or Attest has confirmed it with the owner (ADR 031)."
              },
              "email": {
                "type": "string",
                "description": "Business email for customers. Owner-supplied; published only after Attest has confirmed it with the owner (ADR 031)."
              }
            }
          },
          "claims": {
            "type": "array",
            "description": "What was checked, where, when, and the outcome. claims[0] is the trade register check; for solar installers (ADR 050) it is whichever of pv_greencard_listed and ecb_listed decided the outcome, and the other follows.",
            "items": {
              "type": "object",
              "properties": {
                "claim": {
                  "type": "string",
                  "examples": [
                    "pirb_registered",
                    "ecb_listed",
                    "fsca_authorised"
                  ]
                },
                "outcome": {
                  "type": "string",
                  "enum": [
                    "verified",
                    "consistent",
                    "lapsed",
                    "ambiguous",
                    "not_found"
                  ]
                },
                "observed_at": {
                  "type": "string",
                  "format": "date-time"
                },
                "source": {
                  "type": "string"
                },
                "attestor_class": {
                  "type": "string"
                }
              }
            }
          },
          "qualified": {
            "type": "object",
            "description": "Whether the business evidences a state-recognised trade qualification, and from which source.",
            "properties": {
              "value": {
                "type": "boolean"
              },
              "evidence": {
                "type": "string"
              }
            }
          },
          "coc_authority": {
            "type": "object",
            "description": "Plumbers only: may the business issue the CoC required where it works (value: authorised, not_established, not_applicable, unknown). Absent for electricians, which is not a negative finding."
          },
          "badge": {
            "type": "object",
            "description": "Present where a source stronger than the trade register applies, e.g. the City of Cape Town plumber's register. claims[0] still reports the register check unchanged."
          },
          "listing_status": {
            "type": "string",
            "enum": [
              "unclaimed",
              "claimed",
              "renewing"
            ]
          },
          "owner_stated": {
            "type": "object",
            "description": "What the owner told Attest on stated_on. Not checked by Attest: weigh it as the business's own description (ADR 031). Present only on claimed listings.",
            "properties": {
              "stated_on": {
                "type": "string",
                "format": "date-time"
              },
              "areas_served": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Owner-stated: at most 15 suburbs or towns in or next to the business's own municipality (ADR 043). Not checked by Attest."
              },
              "services": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "blocked_drains",
                    "geysers",
                    "solar_geysers",
                    "leak_detection",
                    "burst_pipes",
                    "bathroom_kitchen",
                    "renovations",
                    "tanks_pumps",
                    "gas"
                  ]
                }
              },
              "services_note": {
                "type": "string",
                "description": "Present when services includes gas: gas work needs SAQCC Gas registration, which PIRB is no evidence of and Attest has not checked."
              },
              "emergency_callouts": {
                "type": "boolean"
              },
              "hours": {
                "type": "string",
                "description": "Free text, as the owner gave it."
              }
            }
          },
          "company": {
            "type": "object",
            "description": "CIPC record for the company registration number the owner supplied, checked at source (ADR 025). Checked only when a listing is claimed, so its absence says nothing. registered_since is the company's age, not necessarily the business's.",
            "properties": {
              "source": {
                "type": "string"
              },
              "observed_at": {
                "type": "string",
                "format": "date-time"
              },
              "registered_name": {
                "type": "string"
              },
              "enterprise_type": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "examples": [
                  "In Business"
                ]
              },
              "compliance_notice": {
                "type": "string",
                "examples": [
                  "NONE"
                ]
              },
              "registered_since": {
                "type": "string",
                "format": "date"
              }
            }
          },
          "customer_rating": {
            "type": "object",
            "description": "Google rating and review count as at the date read. Attributed; not an attestation."
          },
          "urls": {
            "type": "object",
            "properties": {
              "html": {
                "type": "string",
                "format": "uri"
              },
              "json": {
                "type": "string",
                "format": "uri"
              }
            }
          },
          "match": {
            "type": "string",
            "enum": [
              "located",
              "serves"
            ],
            "description": "located: based in the town asked for. serves: a claimed listing based elsewhere whose owner says it serves that town (owner-stated, not checked by Attest). Every located result comes before every serves result."
          }
        }
      }
    }
  }
}
