{
  "openapi": "3.1.0",
  "info": {
    "title": "SelfBadge lookup API",
    "version": "1.0.0",
    "summary": "Identify people, tell namesakes apart, and read verified facts with their sources.",
    "description": "SelfBadge holds one public profile per person, with every fact marked by its verification level (0 seeded from public sources, 1 self-declared, 2 account-verified, 3 document-verified, 4 ID-verified) and its source. The API reads only what each person made public.\n\nRate limits: 120 calls an hour per network without a key; 5000 an hour with a free API key (create one at https://selfbadge.com/account). Every answer has X-RateLimit-Limit and X-RateLimit-Remaining; a 429 has Retry-After.\n\nErrors are JSON: {\"error\": {\"code\": \"...\", \"message\": \"...\"}} with the HTTP status. Addresses under /api that do not exist answer {\"error\": \"not_found\", \"message\": \"...\"} (404), or {\"error\": \"method_not_allowed\", ...} (405) for a method an operation does not take.\n\nThe same lookups are tools on the MCP server at https://selfbadge.com/mcp. Documentation: https://selfbadge.com/developers.\n\nPartners (with a client from SelfBadge) also have the partner API under /api/partners/v1: OAuth 2.0 client credentials at https://selfbadge.com/oauth/token (security scheme partnerOAuth, scopes profiles:read, profiles:write, claim-links:write, profiles:update, claims:attest, plans:write, profiles:read_public), to submit draft profiles that stay private until the person claims them, create signed claim links, and, with capabilities SelfBadge switches on per client (attested_claims, update_facts, plans, revoke, mute_emails, declaration_publish), claim profiles for people whose identity the partner confirmed, update its own facts, give Pro for an order and revoke it. Partner errors are {\"error\": \"<code>\", \"message\": \"...\", \"fields\": {...}}; create calls take an Idempotency-Key header; three webhooks (below) are signed with HMAC-SHA256. The public lookup API above is unchanged: open, with optional keys.",
    "contact": {
      "name": "SelfBadge support",
      "email": "support@selfbadge.com",
      "url": "https://selfbadge.com/contact"
    },
    "termsOfService": "https://selfbadge.com/terms"
  },
  "servers": [
    {
      "url": "https://selfbadge.com"
    }
  ],
  "externalDocs": {
    "description": "Developers: API and MCP",
    "url": "https://selfbadge.com/developers"
  },
  "security": [
    {},
    {
      "bearerAuth": []
    },
    {
      "apiKeyHeader": []
    }
  ],
  "tags": [
    {
      "name": "people",
      "description": "Find and read people's profiles."
    },
    {
      "name": "organizations",
      "description": "Organizations and the people linked to them."
    },
    {
      "name": "partners",
      "description": "For partners with an OAuth client: draft profiles, signed claim links, attested claims, plans and revocations."
    }
  ],
  "paths": {
    "/api/v1/search": {
      "get": {
        "operationId": "searchPeople",
        "tags": [
          "people"
        ],
        "summary": "Find people by name",
        "description": "Public profiles with this name, claimed or created from public sources (seeded, level 0, unconfirmed), best match first. Pass what you know (job title, organization, location) so the right person ranks first; each candidate carries the hints that tell namesakes apart.",
        "parameters": [
          {
            "name": "name",
            "in": "query",
            "required": true,
            "description": "The person's full name (2 to 80 characters). Accents and case are ignored.",
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 80
            },
            "example": "Noa Bergman"
          },
          {
            "name": "job_title",
            "in": "query",
            "required": false,
            "description": "Optional: what you know of their job title (jobTitle is accepted too).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "organization",
            "in": "query",
            "required": false,
            "description": "Optional: an organization you associate with them.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "location",
            "in": "query",
            "required": false,
            "description": "Optional: a city or country you associate with them.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "How many candidates to return.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 25,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Candidates, best match first.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResult"
                }
              }
            }
          },
          "400": {
            "description": "The name is missing or not 2 to 80 characters (code invalid_name).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key sent is not valid (code invalid_key).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many calls in the last hour (code rate_limited). Wait Retry-After seconds, or use a free API key.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "Retry-After": {
                "description": "Seconds until a call is allowed again.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong (code server_error).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The database is not reachable right now (code unavailable). Try again shortly.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/people": {
      "get": {
        "operationId": "getPersonById",
        "tags": [
          "people"
        ],
        "summary": "Get a person by SelfBadge id",
        "description": "The same as /api/v1/people/{handle}, by the stable Person @id (https://selfbadge.com/<handle>#person), the profile address, a bare handle or the internal person id (a UUID).",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "A SelfBadge id.",
            "schema": {
              "type": "string"
            },
            "example": "https://selfbadge.com/noa-bergman#person"
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "jsonld returns only the schema.org JSON-LD (as does Accept: application/ld+json).",
            "schema": {
              "type": "string",
              "enum": [
                "jsonld"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The profile. With format=jsonld (or Accept: application/ld+json), only its JSON-LD.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Person"
                }
              },
              "application/ld+json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonLd"
                }
              }
            }
          },
          "400": {
            "description": "Not a SelfBadge handle or id (code invalid_id or missing_parameter).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key sent is not valid (code invalid_key).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No public SelfBadge profile has that handle or id (code not_found).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many calls in the last hour (code rate_limited). Wait Retry-After seconds, or use a free API key.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "Retry-After": {
                "description": "Seconds until a call is allowed again.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong (code server_error).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The database is not reachable right now (code unavailable). Try again shortly.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/people/{handle}": {
      "get": {
        "operationId": "getPerson",
        "tags": [
          "people"
        ],
        "summary": "Get a person's profile",
        "description": "Every public fact with its verification level and source, the verifications, and the schema.org JSON-LD. Profiles marked seeded were created from public sources and are not claimed yet: treat their facts as unconfirmed.",
        "parameters": [
          {
            "name": "handle",
            "in": "path",
            "required": true,
            "description": "The profile handle, as in https://selfbadge.com/<handle>. A handle that moved after a merge still answers.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9](?:[a-z0-9-]{1,38}[a-z0-9])$"
            },
            "example": "noa-bergman"
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "jsonld returns only the schema.org JSON-LD (as does Accept: application/ld+json).",
            "schema": {
              "type": "string",
              "enum": [
                "jsonld"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The profile. With format=jsonld (or Accept: application/ld+json), only its JSON-LD.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Person"
                }
              },
              "application/ld+json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonLd"
                }
              }
            }
          },
          "400": {
            "description": "Not a SelfBadge handle or id (code invalid_id or missing_parameter).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key sent is not valid (code invalid_key).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No public SelfBadge profile has that handle or id (code not_found).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many calls in the last hour (code rate_limited). Wait Retry-After seconds, or use a free API key.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "Retry-After": {
                "description": "Seconds until a call is allowed again.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong (code server_error).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The database is not reachable right now (code unavailable). Try again shortly.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/organizations/{slug}": {
      "get": {
        "operationId": "getOrganization",
        "tags": [
          "organizations"
        ],
        "summary": "Get an organization",
        "description": "An organization's public facts and JSON-LD, with its team members who accepted their invitation and its founders on SelfBadge.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug, as in https://selfbadge.com/org/<slug>.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$"
            },
            "example": "harbor-ventures"
          }
        ],
        "responses": {
          "200": {
            "description": "The organization.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organization"
                }
              }
            }
          },
          "400": {
            "description": "Not an organization slug (code invalid_slug).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The API key sent is not valid (code invalid_key).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug (code not_found).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many calls in the last hour (code rate_limited). Wait Retry-After seconds, or use a free API key.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "Retry-After": {
                "description": "Seconds until a call is allowed again.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong (code server_error).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The database is not reachable right now (code unavailable). Try again shortly.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/oauth/token": {
      "post": {
        "operationId": "partnerToken",
        "tags": [
          "partners"
        ],
        "summary": "Get a partner access token",
        "description": "OAuth 2.0 client credentials (RFC 6749, section 4.4). Authenticate with HTTP Basic (client_id:client_secret) or send both in the body. The token is a bearer token for /api/partners/v1, valid one hour. Without scope, the token has every scope the client may use. A 401 has WWW-Authenticate: Basic only when the request used HTTP Basic.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/TokenRequest"
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TokenRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The access token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                },
                "example": {
                  "access_token": "sbt_eyJjIjoi...",
                  "token_type": "Bearer",
                  "expires_in": 3600,
                  "scope": "profiles:read profiles:write claims:attest"
                }
              }
            }
          },
          "400": {
            "description": "invalid_request, unsupported_grant_type or invalid_scope (RFC 6749, section 5.2).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          },
          "401": {
            "description": "invalid_client: the id or secret is wrong, or the client is disabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                },
                "example": {
                  "error": "invalid_client",
                  "error_description": "The client id or secret is not valid."
                }
              }
            }
          },
          "429": {
            "description": "slow_down: too many tokens or wrong secrets in the last hour.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          }
        }
      }
    },
    "/api/partners/v1/profiles": {
      "post": {
        "operationId": "createDraftProfile",
        "tags": [
          "partners"
        ],
        "summary": "Submit a draft profile for a person",
        "description": "Creates a draft that is never public (no page, sitemap, search, API or MCP) until the person claims it. Facts are unverified (level 0), labelled \"Submitted via <client name>\"; values from closed lists use the same validation as the editor. When a profile with the same name and organization exists, it is returned as a possible match instead of creating a duplicate, unless create_anyway is true: claim that one (POST /claims) or send its claim link. Drafts nobody claims are deleted after 60 days (a test client's after 24 hours).",
        "security": [
          {
            "partnerOAuth": [
              "profiles:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Any unique string (a UUID). The same key and body within 24 hours answers the first answer again, with Idempotency-Replayed: true; the same key with another body answers 409.",
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "example": "8f14e45f-ceea-467a-9a36-dedd4bea2543"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DraftRequest"
              },
              "example": {
                "name": "Lena Marlowe",
                "email": "lena@marlowe.example",
                "other_names": [
                  "Lena M. Marlowe"
                ],
                "job_title": "Head of Research",
                "organization": "Northwind Labs",
                "city": "Lisbon, Portugal",
                "same_as": [
                  "https://www.linkedin.com/in/lena-marlowe-example"
                ],
                "facts": [
                  {
                    "property": "knowsLanguage",
                    "value": {
                      "code": "pt",
                      "level": "Fluent"
                    }
                  },
                  {
                    "property": "alumniOf",
                    "organization": "University of Porto",
                    "value": {
                      "credential": "MSc, Data Science"
                    },
                    "source_url": "https://lena.marlowe.example/cv"
                  },
                  {
                    "property": "url",
                    "value": "https://lena.marlowe.example",
                    "visibility": "public"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Possible matches: nothing was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PossibleMatches"
                }
              }
            }
          },
          "201": {
            "description": "The draft was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DraftCreated"
                },
                "example": {
                  "result": "created",
                  "profile": {
                    "handle": "lena-marlowe",
                    "status": "draft",
                    "created_at": "2026-11-16T09:30:00.000Z",
                    "expires_at": "2027-01-15T09:30:00.000Z",
                    "url": null
                  },
                  "claim_link": null
                }
              }
            }
          },
          "400": {
            "description": "Fields missing or not valid (invalid_fields, with every problem in fields), or an unknown field (unknown_field).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                },
                "example": {
                  "error": "invalid_fields",
                  "message": "facts[0].value.code: Choose a language from the list.",
                  "fields": {
                    "facts[0].value.code": "Choose a language from the list.",
                    "email": "Give the person's email address (it stays private)."
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or a token that is not valid, has expired or was made with a rotated secret (unauthorized or invalid_token).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer resource_metadata=\"https://selfbadge.com/.well-known/oauth-protected-resource\", with error=\"invalid_token\" when one was sent.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this call needs (insufficient_scope), or SelfBadge has not switched on the capability for this client (capability_not_enabled).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "409": {
            "description": "idempotency_key_reused (the key was used with another body), idempotency_key_in_use (that call is still running), or a conflict named in the code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "429": {
            "description": "Too many calls from this client in the last hour (rate_limited); see Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "503": {
            "description": "The database is not reachable right now (unavailable).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          }
        }
      }
    },
    "/api/partners/v1/profiles/{handle}": {
      "get": {
        "operationId": "getDraftProfile",
        "tags": [
          "partners"
        ],
        "summary": "Read the status of a profile this client submitted or claimed, or its whole public page",
        "description": "Without view: the status (scope profiles:read). With view=public (scope profiles:read_public, granted per client): for a profile this client created or manages, exactly what its public page shows, from the same data as /<handle>.json: the name, headline, description, every public fact with its source and level, the links, not_confused_with, who_is and updated_at. Nothing private or hidden: a profile that is not public answers its status only (not_published or flagged), and one that was removed answers removed with the reason code of the removed webhook.",
        "security": [
          {
            "partnerOAuth": [
              "profiles:read"
            ]
          },
          {
            "partnerOAuth": [
              "profiles:read_public"
            ]
          }
        ],
        "parameters": [
          {
            "name": "handle",
            "in": "path",
            "required": true,
            "description": "A handle this client submitted or claimed.",
            "schema": {
              "type": "string"
            },
            "example": "lena-marlowe"
          },
          {
            "name": "view",
            "in": "query",
            "required": false,
            "description": "public: the whole public page (scope profiles:read_public).",
            "schema": {
              "type": "string",
              "enum": [
                "public"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The status, or with view=public the public page (PublicProfile).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ProfileStatus"
                    },
                    {
                      "$ref": "#/components/schemas/PublicProfile"
                    }
                  ]
                },
                "example": {
                  "handle": "lena-marlowe",
                  "status": "published",
                  "created_at": "2026-11-16T09:30:00.000Z",
                  "expires_at": null,
                  "url": "https://selfbadge.com/lena-marlowe",
                  "verification": {
                    "level": 4,
                    "method": "id_check"
                  },
                  "published": true,
                  "indexable": true,
                  "pro_until": "2027-02-16T09:40:00.000Z",
                  "indexnow_at": "2026-11-16T09:40:01.000Z",
                  "bing_at": "2026-11-17T06:12:00.000Z",
                  "crawlers_30d": {
                    "Googlebot": 3,
                    "Bingbot": 2,
                    "GPTBot": 1
                  },
                  "facts": [
                    {
                      "id": "6f1c2e8a-0d4b-4c55-9f3e-2b7a1c9d8e01",
                      "property": "jobTitle",
                      "value": "Head of Research",
                      "visibility": "public",
                      "source_url": "https://northwind.example",
                      "verification_level": 1
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "No token, or a token that is not valid, has expired or was made with a rotated secret (unauthorized or invalid_token).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer resource_metadata=\"https://selfbadge.com/.well-known/oauth-protected-resource\", with error=\"invalid_token\" when one was sent.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this call needs (insufficient_scope), or SelfBadge has not switched on the capability for this client (capability_not_enabled).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "404": {
            "description": "This client has no profile with that handle.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "429": {
            "description": "Too many calls from this client in the last hour (rate_limited); see Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "503": {
            "description": "The database is not reachable right now (unavailable).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updatePartnerProfile",
        "tags": [
          "partners"
        ],
        "summary": "Change or remove the facts this client added",
        "description": "Needs the update facts capability. On this client's draft or a profile claimed through it: adds facts as this client's, changes or removes only facts this client added that nobody changed since. Verified facts, the person's own facts and facts from public sources are refused, each with its field (403 not_allowed). not_confused_with (the namesake text, at most 120 characters) needs an active Pro plan from this client for this person; null removes it. A real change on a published profile is announced to IndexNow at once and queued first for Bing.",
        "security": [
          {
            "partnerOAuth": [
              "profiles:update"
            ]
          }
        ],
        "parameters": [
          {
            "name": "handle",
            "in": "path",
            "required": true,
            "description": "A handle this client submitted or claimed.",
            "schema": {
              "type": "string"
            },
            "example": "lena-marlowe"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProfilePatch"
              },
              "example": {
                "add": [
                  {
                    "property": "award",
                    "value": {
                      "name": "Iberian Data Award",
                      "year": "2025"
                    }
                  }
                ],
                "change": [
                  {
                    "id": "6f1c2e8a-0d4b-4c55-9f3e-2b7a1c9d8e01",
                    "value": "Director of Research"
                  }
                ],
                "remove": [
                  "0b9e7d1a-6c2f-4a8e-8d3b-5f4e2a1c7b90"
                ],
                "not_confused_with": "Lena Marlowe, the illustrator from Porto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PatchResult"
                },
                "example": {
                  "handle": "lena-marlowe",
                  "added": [
                    "2c4e6a8b-1d3f-4b5c-9e7a-0f2d4b6c8e1a"
                  ],
                  "changed": [
                    "6f1c2e8a-0d4b-4c55-9f3e-2b7a1c9d8e01"
                  ],
                  "removed": [
                    "0b9e7d1a-6c2f-4a8e-8d3b-5f4e2a1c7b90"
                  ],
                  "announced": true
                }
              }
            }
          },
          "400": {
            "description": "Fields not valid (invalid_fields, with fields).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "401": {
            "description": "No token, or a token that is not valid, has expired or was made with a rotated secret (unauthorized or invalid_token).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer resource_metadata=\"https://selfbadge.com/.well-known/oauth-protected-resource\", with error=\"invalid_token\" when one was sent.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "A fact this client may not change (not_allowed, with fields), Pro needed (pro_required), or the capability is off (capability_not_enabled).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                },
                "example": {
                  "error": "not_allowed",
                  "message": "change[0].id: A verified fact: only the person can change it.",
                  "fields": {
                    "change[0].id": "A verified fact: only the person can change it."
                  }
                }
              }
            }
          },
          "404": {
            "description": "This client has no profile with that handle.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "409": {
            "description": "The profile waits for review, or the claim was revoked (not_editable).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "429": {
            "description": "Too many calls from this client in the last hour (rate_limited); see Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "503": {
            "description": "The database is not reachable right now (unavailable).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteDraftProfile",
        "tags": [
          "partners"
        ],
        "summary": "Delete a draft (only while nobody has claimed it)",
        "security": [
          {
            "partnerOAuth": [
              "profiles:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "handle",
            "in": "path",
            "required": true,
            "description": "A handle this client submitted or claimed.",
            "schema": {
              "type": "string"
            },
            "example": "lena-marlowe"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "deleted",
                    "handle"
                  ],
                  "properties": {
                    "deleted": {
                      "type": "boolean",
                      "const": true
                    },
                    "handle": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "deleted": true,
                  "handle": "lena-marlowe"
                }
              }
            }
          },
          "401": {
            "description": "No token, or a token that is not valid, has expired or was made with a rotated secret (unauthorized or invalid_token).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer resource_metadata=\"https://selfbadge.com/.well-known/oauth-protected-resource\", with error=\"invalid_token\" when one was sent.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this call needs (insufficient_scope), or SelfBadge has not switched on the capability for this client (capability_not_enabled).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "404": {
            "description": "This client submitted no profile with that handle.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "409": {
            "description": "The person has claimed it (not_a_draft): only they can change or delete it now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "429": {
            "description": "Too many calls from this client in the last hour (rate_limited); see Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "503": {
            "description": "The database is not reachable right now (unavailable).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          }
        }
      }
    },
    "/api/partners/v1/claim-links": {
      "post": {
        "operationId": "createClaimLink",
        "tags": [
          "partners"
        ],
        "summary": "Create a signed claim link",
        "description": "A link to /claim/<handle> for one of this client's drafts, or for a public profile made from public sources, bound to one email: valid 30 days, single use. When the person signs in (LinkedIn or Google) with that email verified, the claim is approved at once; otherwise a person at SelfBadge reviews it.",
        "security": [
          {
            "partnerOAuth": [
              "claim-links:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Any unique string (a UUID). The same key and body within 24 hours answers the first answer again, with Idempotency-Replayed: true; the same key with another body answers 409.",
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "example": "8f14e45f-ceea-467a-9a36-dedd4bea2543"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClaimLinkRequest"
              },
              "example": {
                "handle": "lena-marlowe",
                "email": "lena@marlowe.example"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClaimLinkCreated"
                }
              }
            }
          },
          "400": {
            "description": "The handle or email is missing (invalid_fields).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "401": {
            "description": "No token, or a token that is not valid, has expired or was made with a rotated secret (unauthorized or invalid_token).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer resource_metadata=\"https://selfbadge.com/.well-known/oauth-protected-resource\", with error=\"invalid_token\" when one was sent.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this call needs (insufficient_scope), or SelfBadge has not switched on the capability for this client (capability_not_enabled).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "404": {
            "description": "No profile with that handle can be claimed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "409": {
            "description": "idempotency_key_reused (the key was used with another body), idempotency_key_in_use (that call is still running), or a conflict named in the code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "429": {
            "description": "Too many calls from this client in the last hour (rate_limited); see Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "503": {
            "description": "The database is not reachable right now (unavailable).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          }
        }
      }
    },
    "/api/partners/v1/claims": {
      "post": {
        "operationId": "attestClaim",
        "tags": [
          "partners"
        ],
        "summary": "Claim a profile for a person whose identity this client confirmed",
        "description": "Needs the attested claims capability. The profile is this client's draft, or a profile from public sources it received as a possible match. SelfBadge makes an account for the owner's email (they sign in later with LinkedIn or Google using that email). Published at once (201) when the method confirms the name (linkedin, facebook, id_check or manual), the attested name matches the profile's name or one of its other names (accents, case and initials ignored; a name in another script only through the other names), the profile is this client's own draft and the account has no profile yet. Otherwise 202: a person at SelfBadge reviews it with the evidence (webhooks review_needed, review_decided, published). work_email and website always go to review (reason method_needs_review), whatever name is sent: they prove an address, not a name. Methods: linkedin and facebook (level 2, with the matching account link), work_email and website (level 2, after review), manual (a named reviewer, level 3), id_check (Stripe Identity, level 4). manual needs reviewer_id and reviewer_name of a person; a reviewer_id that reads as an automatic process (ending in -auto, or naming a bot, a system, a script or a service) is refused (400 invalid_evidence): use partner_declaration. partner_declaration (needs the declaration_publish capability instead of attested claims): the person's own declaration. It verifies nothing: the name and every fact stay self-declared (level 1), labelled \"Declared by the person at <client name>\", with no verification. It is published at once only for this client's own draft whose name matches and whose account has no profile; otherwise it is reviewed, marked partner declaration. The email is not verified by it. Google sign-in is never accepted. The evidence stays private: never on the page, in the JSON-LD or in .json.",
        "security": [
          {
            "partnerOAuth": [
              "claims:attest"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Any unique string (a UUID). The same key and body within 24 hours answers the first answer again, with Idempotency-Replayed: true; the same key with another body answers 409.",
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "example": "8f14e45f-ceea-467a-9a36-dedd4bea2543"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AttestedClaimRequest"
              },
              "examples": {
                "id_check": {
                  "summary": "An ID check",
                  "value": {
                    "handle": "lena-marlowe",
                    "email": "lena@marlowe.example",
                    "method": "id_check",
                    "evidence": {
                      "verification_session_id": "vs_1QxExample000000",
                      "verified_name": "Lena Marlowe"
                    },
                    "declaration": {
                      "text": "I confirm I am this person and that the facts are about me",
                      "accepted_at": "2026-11-16T09:38:00Z",
                      "ip": "203.0.113.24"
                    }
                  }
                },
                "partner_declaration": {
                  "summary": "The person's own declaration (published at level 1)",
                  "value": {
                    "handle": "lena-marlowe",
                    "email": "lena@marlowe.example",
                    "method": "partner_declaration",
                    "evidence": {
                      "declared_at": "2026-11-16T09:37:40Z",
                      "declared_name": "Lena Marlowe",
                      "email": "lena@marlowe.example",
                      "name_rule": "The name typed at checkout must equal the name on the card."
                    },
                    "declaration": {
                      "text": "I confirm I am this person and that the facts are about me",
                      "accepted_at": "2026-11-16T09:38:00Z",
                      "ip": "203.0.113.24"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A test client's claim decided as rejected (test_outcome).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AttestedClaimRejected"
                }
              }
            }
          },
          "201": {
            "description": "Published at once.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AttestedClaimPublished"
                },
                "example": {
                  "result": "published",
                  "handle": "lena-marlowe",
                  "url": "https://selfbadge.com/lena-marlowe",
                  "verification": {
                    "level": 4,
                    "method": "id_check"
                  }
                }
              }
            }
          },
          "202": {
            "description": "Sent to SelfBadge's review.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AttestedClaimReview"
                },
                "example": {
                  "result": "review",
                  "handle": "miriam-adler-stone",
                  "claim_id": "3a5c7e9b-2d4f-4a6b-8c0e-1f3a5c7e9b2d",
                  "reasons": [
                    "public_source"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Fields not valid (invalid_fields, with fields); Google sign-in is refused here; a manual reviewer that is not a person (invalid_evidence).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                },
                "example": {
                  "error": "invalid_fields",
                  "message": "method: Google sign-in is not accepted as evidence of who someone is.",
                  "fields": {
                    "method": "Google sign-in is not accepted as evidence of who someone is."
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or a token that is not valid, has expired or was made with a rotated secret (unauthorized or invalid_token).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer resource_metadata=\"https://selfbadge.com/.well-known/oauth-protected-resource\", with error=\"invalid_token\" when one was sent.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this call needs (insufficient_scope), or SelfBadge has not switched on the capability for this client (capability_not_enabled).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "404": {
            "description": "No profile with that handle can be claimed by this client.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "409": {
            "description": "already_claimed, review_pending, claim_pending, or claim_revoked (a revoked partner claim: no partner can claim it again).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "429": {
            "description": "Too many calls from this client in the last hour (rate_limited); see Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "503": {
            "description": "The database is not reachable right now (unavailable).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          }
        }
      }
    },
    "/api/partners/v1/profiles/{handle}/edit-link": {
      "post": {
        "operationId": "createEditLink",
        "tags": [
          "partners"
        ],
        "summary": "A one-time link to the editor for the profile's owner",
        "description": "For a profile this client created or manages that its person has claimed. The link works once, for 15 minutes. It never signs anyone in: the owner, signed in, goes straight to the editor; anyone else signs in first with LinkedIn or Google (Google is given the owner's email as a hint) and comes back to the editor. A used or expired link shows a page with the normal sign-in. Opening the address (GET) only shows \"Open your SelfBadge profile\" with one button; the button uses the link, so email scanners and link previews that fetch the address never use it up. When the declared email is not a Google or LinkedIn account, the person signs in with another account; if the account SelfBadge made for the claim has never been signed in to, a 6-digit code goes to the declared email and the right code moves the profile to the account they signed in with (webhook owner_changed). An account that already has a profile is refused. Test clients: their claims make no account and no owner, so a profile their test claim published (active) gets a link too, with \"test\": true. It opens a \"Test edit link\" page that says where the person would sign in with their declared email and land in their editor, with no sign-in and no editor; the same one-time and 15-minute rules apply, and a sample of the code step is shown; nothing is sent and nothing moves.",
        "security": [
          {
            "partnerOAuth": [
              "profiles:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "handle",
            "in": "path",
            "required": true,
            "description": "A handle this client submitted or claimed.",
            "schema": {
              "type": "string"
            },
            "example": "lena-marlowe"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Any unique string (a UUID). The same key and body within 24 hours answers the first answer again, with Idempotency-Replayed: true; the same key with another body answers 409.",
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "example": "8f14e45f-ceea-467a-9a36-dedd4bea2543"
          }
        ],
        "responses": {
          "201": {
            "description": "The link (a test client's: \"test\": true, opening the Test edit link page).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EditLink"
                },
                "examples": {
                  "owner": {
                    "summary": "A production client",
                    "value": {
                      "url": "https://selfbadge.com/edit-link/q7Xc...",
                      "expires_at": "2026-11-16T09:55:00.000Z"
                    }
                  },
                  "test_client": {
                    "summary": "A test client",
                    "value": {
                      "url": "https://selfbadge.com/edit-link/Hn4w...",
                      "expires_at": "2026-11-16T09:55:00.000Z",
                      "test": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or a token that is not valid, has expired or was made with a rotated secret (unauthorized or invalid_token).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer resource_metadata=\"https://selfbadge.com/.well-known/oauth-protected-resource\", with error=\"invalid_token\" when one was sent.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this call needs (insufficient_scope), or SelfBadge has not switched on the capability for this client (capability_not_enabled).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "404": {
            "description": "This client has no profile with that handle.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "409": {
            "description": "Nobody owns this profile yet (not_claimed; for a test client: no test claim yet), or idempotency_key_reused or idempotency_key_in_use.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "429": {
            "description": "Too many calls from this client in the last hour (rate_limited); see Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "503": {
            "description": "The database is not reachable right now (unavailable).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          }
        }
      }
    },
    "/api/partners/v1/preview": {
      "post": {
        "operationId": "createPreview",
        "tags": [
          "partners"
        ],
        "summary": "Preview a draft for a checkout",
        "description": "The same body and validation as POST /api/partners/v1/profiles. Creates and publishes nothing and never touches profiles, organizations or search. Returns a signed address valid 30 minutes that shows the profile exactly as its public page would look once the person publishes it on their declaration, with a \"Preview, not live yet\" bar: not indexed, no JSON-LD, no claim, edit, report or remove links, never cached, no analytics. Only the https origins set for this client in SelfBadge (preview framing) may show it in a frame; with none, no site may. At most 60 a minute per client.",
        "security": [
          {
            "partnerOAuth": [
              "profiles:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Any unique string (a UUID). The same key and body within 24 hours answers the first answer again, with Idempotency-Replayed: true; the same key with another body answers 409.",
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "example": "8f14e45f-ceea-467a-9a36-dedd4bea2543"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DraftRequest"
              },
              "example": {
                "name": "Lena Marlowe",
                "email": "lena@marlowe.example",
                "job_title": "Head of Research",
                "organization": "Northwind Labs",
                "city": "Lisbon, Portugal"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The preview's address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Preview"
                },
                "example": {
                  "url": "https://selfbadge.com/preview/0d7f...a1.9kQ...",
                  "expires_at": "2026-11-16T10:10:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Fields missing or not valid (invalid_fields, with every problem in fields), or an unknown field (unknown_field).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "401": {
            "description": "No token, or a token that is not valid, has expired or was made with a rotated secret (unauthorized or invalid_token).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer resource_metadata=\"https://selfbadge.com/.well-known/oauth-protected-resource\", with error=\"invalid_token\" when one was sent.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this call needs (insufficient_scope), or SelfBadge has not switched on the capability for this client (capability_not_enabled).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "409": {
            "description": "idempotency_key_reused (the key was used with another body), idempotency_key_in_use (that call is still running), or a conflict named in the code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "429": {
            "description": "Too many calls from this client in the last hour (rate_limited); see Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "503": {
            "description": "The database is not reachable right now (unavailable).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          }
        }
      }
    },
    "/api/partners/v1/plans": {
      "post": {
        "operationId": "createPartnerPlan",
        "tags": [
          "partners"
        ],
        "summary": "Give Pro for an order",
        "description": "Needs the plans capability. Pro for 1, 3 or 12 months for the person of this client's profile, from the latest end date they already have (never overlapping), or from now. Idempotent by order_id: the same order answers 200 with the first period. Before the person's claim the plan waits for their email. Pro bought through a partner follows that partner's refund terms.",
        "security": [
          {
            "partnerOAuth": [
              "plans:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Any unique string (a UUID). The same key and body within 24 hours answers the first answer again, with Idempotency-Replayed: true; the same key with another body answers 409.",
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "example": "8f14e45f-ceea-467a-9a36-dedd4bea2543"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlanRequest"
              },
              "example": {
                "handle": "lena-marlowe",
                "months": 3,
                "order_id": "pm_order_10482"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "This order was already given (result exists).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanResult"
                }
              }
            }
          },
          "201": {
            "description": "Pro created or extended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanResult"
                },
                "example": {
                  "result": "created",
                  "order_id": "pm_order_10482",
                  "handle": "lena-marlowe",
                  "pro_until": "2027-02-16T09:40:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Fields not valid (invalid_fields).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "401": {
            "description": "No token, or a token that is not valid, has expired or was made with a rotated secret (unauthorized or invalid_token).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer resource_metadata=\"https://selfbadge.com/.well-known/oauth-protected-resource\", with error=\"invalid_token\" when one was sent.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this call needs (insufficient_scope), or SelfBadge has not switched on the capability for this client (capability_not_enabled).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "404": {
            "description": "This client has no profile with that handle.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "409": {
            "description": "idempotency_key_reused (the key was used with another body), idempotency_key_in_use (that call is still running), or a conflict named in the code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "429": {
            "description": "Too many calls from this client in the last hour (rate_limited); see Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "503": {
            "description": "The database is not reachable right now (unavailable).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          }
        }
      }
    },
    "/api/partners/v1/revocations": {
      "post": {
        "operationId": "revokePartnerOrder",
        "tags": [
          "partners"
        ],
        "summary": "Revoke an order after a refund or a withdrawal",
        "description": "Needs the revoke capability. Two scopes. scope \"profile\" (the default, for a refund or the withdrawal of the first purchase): Pro from this client ends at once. A profile this client created is removed like an account deletion (410 Gone, out of sitemaps, llms files, search, the API and MCP, IndexNow told; the account deleted when it holds nothing else). A profile from public sources claimed through this client goes back to unclaimed with its public-source facts (not 410); this client's facts, the attested verification and the claim go. After a revocation no partner can claim that profile again. scope \"order\" (for example the withdrawal of a later renewal): only this order's Pro is cancelled; this client's later orders for the person move earlier by the time it had not used, and pro_until is the end of what remains (null: Pro has ended). The profile, the claim, the verifications and the facts stay; a published \"not to be confused with\" stays, and adding or changing it needs Pro again. The same order again answers already_revoked.",
        "security": [
          {
            "partnerOAuth": [
              "plans:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Any unique string (a UUID). The same key and body within 24 hours answers the first answer again, with Idempotency-Replayed: true; the same key with another body answers 409.",
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "example": "8f14e45f-ceea-467a-9a36-dedd4bea2543"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RevocationRequest"
              },
              "examples": {
                "profile": {
                  "summary": "A refund: Pro ends and the profile goes (the default scope)",
                  "value": {
                    "order_id": "pm_order_10482",
                    "reason": "refund"
                  }
                },
                "order": {
                  "summary": "A withdrawal from a later renewal: only this order",
                  "value": {
                    "order_id": "pm_order_10517",
                    "reason": "withdrawal",
                    "scope": "order"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Revoked (or already).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RevocationResult"
                },
                "examples": {
                  "profile": {
                    "summary": "Scope profile",
                    "value": {
                      "result": "revoked",
                      "scope": "profile",
                      "order_id": "pm_order_10482",
                      "pro_ended": 1,
                      "profile": "removed"
                    }
                  },
                  "order": {
                    "summary": "Scope order, Pro continues until the earlier order ends",
                    "value": {
                      "result": "revoked",
                      "scope": "order",
                      "order_id": "pm_order_10517",
                      "pro_ended": 1,
                      "profile": "none",
                      "pro_until": "2027-01-24T09:30:00.000Z"
                    }
                  },
                  "order_last": {
                    "summary": "Scope order, the only order: Pro has ended",
                    "value": {
                      "result": "revoked",
                      "scope": "order",
                      "order_id": "pm_order_10533",
                      "pro_ended": 1,
                      "profile": "none",
                      "pro_until": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Fields not valid (invalid_fields).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "401": {
            "description": "No token, or a token that is not valid, has expired or was made with a rotated secret (unauthorized or invalid_token).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer resource_metadata=\"https://selfbadge.com/.well-known/oauth-protected-resource\", with error=\"invalid_token\" when one was sent.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope this call needs (insufficient_scope), or SelfBadge has not switched on the capability for this client (capability_not_enabled).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "404": {
            "description": "This client has no order with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "409": {
            "description": "idempotency_key_reused (the key was used with another body), idempotency_key_in_use (that call is still running), or a conflict named in the code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "429": {
            "description": "Too many calls from this client in the last hour (rate_limited); see Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          },
          "503": {
            "description": "The database is not reachable right now (unavailable).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerError"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "review_needed": {
      "post": {
        "operationId": "webhook_review_needed",
        "tags": [
          "partners"
        ],
        "summary": "A claim waits for SelfBadge's review",
        "description": "Sent to the client's webhook address (set in /admin/partners). Headers: SelfBadge-Event, SelfBadge-Delivery (its id), SelfBadge-Signature: t=<unix seconds>,v1=<hex>, the HMAC-SHA256 of \"<t>.<raw body>\" with the client's webhook secret. Check it and that t is within 5 minutes. Answer 2xx; anything else is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. See https://selfbadge.com/developers#partner-webhooks.",
        "parameters": [
          {
            "name": "SelfBadge-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "t=1763285820,v1=5f2b..."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "id": "evt_8c1f0d3e5a7b49c2a0e6f4d2b8c1a3e5",
                "event": "review_needed",
                "created_at": "2026-11-16T09:37:00.000Z",
                "data": {
                  "handle": "miriam-adler-stone",
                  "claim_id": "3a5c7e9b-2d4f-4a6b-8c0e-1f3a5c7e9b2d",
                  "reasons": [
                    "public_source"
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received."
          }
        }
      }
    },
    "review_decided": {
      "post": {
        "operationId": "webhook_review_decided",
        "tags": [
          "partners"
        ],
        "summary": "SelfBadge decided a claim",
        "description": "Sent to the client's webhook address (set in /admin/partners). Headers: SelfBadge-Event, SelfBadge-Delivery (its id), SelfBadge-Signature: t=<unix seconds>,v1=<hex>, the HMAC-SHA256 of \"<t>.<raw body>\" with the client's webhook secret. Check it and that t is within 5 minutes. Answer 2xx; anything else is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. See https://selfbadge.com/developers#partner-webhooks.",
        "parameters": [
          {
            "name": "SelfBadge-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "t=1763285820,v1=5f2b..."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "id": "evt_8c1f0d3e5a7b49c2a0e6f4d2b8c1a3e5",
                "event": "review_decided",
                "created_at": "2026-11-16T09:37:00.000Z",
                "data": {
                  "handle": "miriam-adler-stone",
                  "claim_id": "3a5c7e9b-2d4f-4a6b-8c0e-1f3a5c7e9b2d",
                  "decision": "approved",
                  "reason": null
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received."
          }
        }
      }
    },
    "published": {
      "post": {
        "operationId": "webhook_published",
        "tags": [
          "partners"
        ],
        "summary": "A profile is public",
        "description": "Sent to the client's webhook address (set in /admin/partners). Headers: SelfBadge-Event, SelfBadge-Delivery (its id), SelfBadge-Signature: t=<unix seconds>,v1=<hex>, the HMAC-SHA256 of \"<t>.<raw body>\" with the client's webhook secret. Check it and that t is within 5 minutes. Answer 2xx; anything else is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. See https://selfbadge.com/developers#partner-webhooks.",
        "parameters": [
          {
            "name": "SelfBadge-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "t=1763285820,v1=5f2b..."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "id": "evt_8c1f0d3e5a7b49c2a0e6f4d2b8c1a3e5",
                "event": "published",
                "created_at": "2026-11-16T09:37:00.000Z",
                "data": {
                  "handle": "miriam-adler-stone",
                  "url": "https://selfbadge.com/miriam-adler-stone"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received."
          }
        }
      }
    },
    "flagged": {
      "post": {
        "operationId": "webhook_flagged",
        "tags": [
          "partners"
        ],
        "summary": "A report put the profile under review, or SelfBadge hid it (reason_code report_under_review or hidden_by_admin)",
        "description": "Sent to the client's webhook address (set in /admin/partners). Headers: SelfBadge-Event, SelfBadge-Delivery (its id), SelfBadge-Signature: t=<unix seconds>,v1=<hex>, the HMAC-SHA256 of \"<t>.<raw body>\" with the client's webhook secret. Check it and that t is within 5 minutes. Answer 2xx; anything else is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. See https://selfbadge.com/developers#partner-webhooks.",
        "parameters": [
          {
            "name": "SelfBadge-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "t=1763285820,v1=5f2b..."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "id": "evt_8c1f0d3e5a7b49c2a0e6f4d2b8c1a3e5",
                "event": "flagged",
                "created_at": "2026-11-16T09:37:00.000Z",
                "data": {
                  "handle": "lena-marlowe",
                  "reason_code": "report_under_review",
                  "at": "2026-11-20T08:12:00.000Z"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received."
          }
        }
      }
    },
    "disputed": {
      "post": {
        "operationId": "webhook_disputed",
        "tags": [
          "partners"
        ],
        "summary": "An impersonation report, or another person's claim (reason_code impersonation_report or other_claim)",
        "description": "Sent to the client's webhook address (set in /admin/partners). Headers: SelfBadge-Event, SelfBadge-Delivery (its id), SelfBadge-Signature: t=<unix seconds>,v1=<hex>, the HMAC-SHA256 of \"<t>.<raw body>\" with the client's webhook secret. Check it and that t is within 5 minutes. Answer 2xx; anything else is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. See https://selfbadge.com/developers#partner-webhooks.",
        "parameters": [
          {
            "name": "SelfBadge-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "t=1763285820,v1=5f2b..."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "id": "evt_8c1f0d3e5a7b49c2a0e6f4d2b8c1a3e5",
                "event": "disputed",
                "created_at": "2026-11-16T09:37:00.000Z",
                "data": {
                  "handle": "lena-marlowe",
                  "reason_code": "impersonation_report",
                  "at": "2026-11-20T08:12:00.000Z"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received."
          }
        }
      }
    },
    "restored": {
      "post": {
        "operationId": "webhook_restored",
        "tags": [
          "partners"
        ],
        "summary": "Public again after flagged or disputed (reason_code review_closed)",
        "description": "Sent to the client's webhook address (set in /admin/partners). Headers: SelfBadge-Event, SelfBadge-Delivery (its id), SelfBadge-Signature: t=<unix seconds>,v1=<hex>, the HMAC-SHA256 of \"<t>.<raw body>\" with the client's webhook secret. Check it and that t is within 5 minutes. Answer 2xx; anything else is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. See https://selfbadge.com/developers#partner-webhooks.",
        "parameters": [
          {
            "name": "SelfBadge-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "t=1763285820,v1=5f2b..."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "id": "evt_8c1f0d3e5a7b49c2a0e6f4d2b8c1a3e5",
                "event": "restored",
                "created_at": "2026-11-16T09:37:00.000Z",
                "data": {
                  "handle": "lena-marlowe",
                  "reason_code": "review_closed",
                  "at": "2026-11-21T10:02:00.000Z"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received."
          }
        }
      }
    },
    "removed": {
      "post": {
        "operationId": "webhook_removed",
        "tags": [
          "partners"
        ],
        "summary": "The profile answers 410 Gone, for any reason but this client's own revocation (reason_code removed_after_report, removed_by_admin or account_deleted)",
        "description": "Sent to the client's webhook address (set in /admin/partners). Headers: SelfBadge-Event, SelfBadge-Delivery (its id), SelfBadge-Signature: t=<unix seconds>,v1=<hex>, the HMAC-SHA256 of \"<t>.<raw body>\" with the client's webhook secret. Check it and that t is within 5 minutes. Answer 2xx; anything else is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. See https://selfbadge.com/developers#partner-webhooks.",
        "parameters": [
          {
            "name": "SelfBadge-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "t=1763285820,v1=5f2b..."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "id": "evt_8c1f0d3e5a7b49c2a0e6f4d2b8c1a3e5",
                "event": "removed",
                "created_at": "2026-11-16T09:37:00.000Z",
                "data": {
                  "handle": "lena-marlowe",
                  "reason_code": "account_deleted",
                  "at": "2026-11-22T16:45:00.000Z"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received."
          }
        }
      }
    },
    "owner_changed": {
      "post": {
        "operationId": "webhook_owner_changed",
        "tags": [
          "partners"
        ],
        "summary": "The person confirmed a code sent to the declared email and the profile moved to the Google or LinkedIn account they signed in with (no emails in the event)",
        "description": "Sent to the client's webhook address (set in /admin/partners). Headers: SelfBadge-Event, SelfBadge-Delivery (its id), SelfBadge-Signature: t=<unix seconds>,v1=<hex>, the HMAC-SHA256 of \"<t>.<raw body>\" with the client's webhook secret. Check it and that t is within 5 minutes. Answer 2xx; anything else is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. See https://selfbadge.com/developers#partner-webhooks.",
        "parameters": [
          {
            "name": "SelfBadge-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "t=1763285820,v1=5f2b..."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "id": "evt_8c1f0d3e5a7b49c2a0e6f4d2b8c1a3e5",
                "event": "owner_changed",
                "created_at": "2026-11-16T09:37:00.000Z",
                "data": {
                  "handle": "lena-marlowe",
                  "at": "2026-11-23T09:30:00.000Z"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received."
          }
        }
      }
    },
    "verification_changed": {
      "post": {
        "operationId": "webhook_verification_changed",
        "tags": [
          "partners"
        ],
        "summary": "A public fact gained, lost or changed its verification (a sign-in or connected account, an email code, DNS, a link-back, a document, an ID check, or a verification that expired). At most one event a minute per profile, with every changed fact id; level and method are the name's current verification. Your own attestation is not reported",
        "description": "Sent to the client's webhook address (set in /admin/partners). Headers: SelfBadge-Event, SelfBadge-Delivery (its id), SelfBadge-Signature: t=<unix seconds>,v1=<hex>, the HMAC-SHA256 of \"<t>.<raw body>\" with the client's webhook secret. Check it and that t is within 5 minutes. Answer 2xx; anything else is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. See https://selfbadge.com/developers#partner-webhooks.",
        "parameters": [
          {
            "name": "SelfBadge-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "t=1763285820,v1=5f2b..."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "id": "evt_8c1f0d3e5a7b49c2a0e6f4d2b8c1a3e5",
                "event": "verification_changed",
                "created_at": "2026-11-16T09:37:00.000Z",
                "data": {
                  "handle": "lena-marlowe",
                  "level": 3,
                  "method": "document",
                  "fact_ids": [
                    "2c4e6a8b-1d3f-4b5c-9e7a-0f2d4b6c8e1a",
                    "6f1c2e8a-0d4b-4c55-9f3e-2b7a1c9d8e01"
                  ],
                  "at": "2026-11-24T11:05:00.000Z"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Optional. A free SelfBadge API key (sbk_...) from https://selfbadge.com/account raises the limit to 5000 calls an hour."
      },
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Optional. The same API key, in this header instead of Authorization."
      },
      "partnerOAuth": {
        "type": "oauth2",
        "description": "For partners only (a client from SelfBadge). Client credentials at https://selfbadge.com/oauth/token; metadata at https://selfbadge.com/.well-known/oauth-authorization-server.",
        "flows": {
          "clientCredentials": {
            "tokenUrl": "https://selfbadge.com/oauth/token",
            "scopes": {
              "profiles:read": "Read the status of the profiles this client submitted.",
              "profiles:write": "Submit draft profiles, and delete them while they are still drafts.",
              "claim-links:write": "Create signed claim links for this client's drafts and for profiles made from public sources.",
              "profiles:update": "Change or remove the facts this client added, on its drafts and on profiles claimed through it (needs the update facts capability).",
              "claims:attest": "Claim a profile for a person whose identity this client confirmed (needs the attested claims capability).",
              "plans:write": "Give Pro for an order, and revoke it after a refund or a withdrawal (needs the plans or revoke capability).",
              "profiles:read_public": "Read the whole public page of the profiles this client created or manages (?view=public)."
            }
          }
        }
      }
    },
    "headers": {
      "RateLimitLimit": {
        "description": "Calls allowed an hour for this caller.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitRemaining": {
        "description": "Calls left in the current hour.",
        "schema": {
          "type": "integer"
        }
      }
    },
    "schemas": {
      "PartnerError": {
        "type": "object",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "A stable code, such as invalid_fields, not_allowed, not_found, capability_not_enabled, idempotency_key_reused, rate_limited."
          },
          "message": {
            "type": "string",
            "description": "Plain text for people."
          },
          "fields": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "When fields are at fault: each field (with its path, like facts[2].value) and its problem."
          }
        }
      },
      "OAuthError": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "error_description": {
            "type": "string"
          }
        }
      },
      "TokenRequest": {
        "type": "object",
        "required": [
          "grant_type"
        ],
        "properties": {
          "grant_type": {
            "type": "string",
            "enum": [
              "client_credentials"
            ]
          },
          "scope": {
            "type": "string",
            "description": "Space-separated, from: profiles:read profiles:write claim-links:write profiles:update claims:attest plans:write profiles:read_public."
          },
          "client_id": {
            "type": "string",
            "description": "When not sent with HTTP Basic."
          },
          "client_secret": {
            "type": "string",
            "description": "When not sent with HTTP Basic."
          }
        }
      },
      "TokenResponse": {
        "type": "object",
        "required": [
          "access_token",
          "token_type",
          "expires_in",
          "scope"
        ],
        "properties": {
          "access_token": {
            "type": "string"
          },
          "token_type": {
            "type": "string",
            "enum": [
              "Bearer"
            ]
          },
          "expires_in": {
            "type": "integer",
            "example": 3600
          },
          "scope": {
            "type": "string"
          }
        }
      },
      "PartnerFact": {
        "type": "object",
        "required": [
          "property",
          "value"
        ],
        "additionalProperties": false,
        "properties": {
          "property": {
            "type": "string",
            "enum": [
              "givenName",
              "familyName",
              "honorificPrefix",
              "additionalName",
              "honorificSuffix",
              "alternateName",
              "description",
              "disambiguatingDescription",
              "jobTitle",
              "worksFor",
              "founder",
              "workLocation",
              "hasOccupation",
              "memberOf",
              "affiliation",
              "alumniOf",
              "hasCredential",
              "award",
              "url",
              "sameAs",
              "identifier",
              "knowsAbout",
              "knowsLanguage",
              "authored",
              "subjectOf"
            ]
          },
          "value": {
            "description": "As the editor takes it: a string for one-line fields, or the field's object (for example {code, level} for knowsLanguage, {name, year} for award)."
          },
          "organization": {
            "description": "For worksFor, founder, memberOf, affiliation and alumniOf: a name or {name, url}.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "url": {
                    "type": "string",
                    "format": "uri"
                  }
                }
              }
            ]
          },
          "source_url": {
            "type": "string",
            "format": "uri",
            "description": "Where the fact can be checked; the client's website when absent."
          },
          "visibility": {
            "type": "string",
            "enum": [
              "public",
              "private"
            ],
            "description": "The field's default when absent."
          }
        }
      },
      "DraftRequest": {
        "type": "object",
        "required": [
          "name",
          "email"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "description": "The person's full name.",
            "example": "Lena Marlowe"
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Private: used only to match the claim, cleared when the person claims.",
            "example": "lena@marlowe.example"
          },
          "other_names": {
            "type": "array",
            "maxItems": 5,
            "items": {
              "type": "string"
            },
            "description": "Other names the person uses (a name in another script matches an attested claim only through these)."
          },
          "description": {
            "type": "string",
            "maxLength": 160
          },
          "job_title": {
            "type": "string",
            "maxLength": 120,
            "example": "Head of Research"
          },
          "organization": {
            "type": "string",
            "maxLength": 120,
            "example": "Northwind Labs"
          },
          "city": {
            "description": "A city from the GeoNames list: \"Lisbon, Portugal\" or {name, country}.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "country": {
                    "type": "string"
                  }
                }
              }
            ]
          },
          "same_as": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "type": "string",
              "format": "uri"
            },
            "description": "The person's own pages and accounts."
          },
          "facts": {
            "type": "array",
            "maxItems": 30,
            "items": {
              "$ref": "#/components/schemas/PartnerFact"
            },
            "description": "Facts from the editor's closed lists, with an organization where the property names one."
          },
          "not_confused_with": {
            "description": "The \"not to be confused with\" text: a namesake's public description (at most 120 characters), or {description, url}. Needs an active Pro plan from this client for this person.",
            "oneOf": [
              {
                "type": "string",
                "maxLength": 120
              },
              {
                "type": "object",
                "required": [
                  "description"
                ],
                "properties": {
                  "description": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "url": {
                    "type": "string",
                    "format": "uri"
                  }
                }
              }
            ]
          },
          "create_anyway": {
            "type": "boolean",
            "default": false,
            "description": "Create even when a profile with the same name and organization exists."
          }
        }
      },
      "Draft": {
        "type": "object",
        "required": [
          "handle",
          "status",
          "created_at",
          "expires_at",
          "url"
        ],
        "properties": {
          "handle": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "review",
              "claimed",
              "published",
              "revoked"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When an unclaimed draft is deleted."
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "description": "The public address, once published."
          }
        }
      },
      "ProfileStatus": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Draft"
          },
          {
            "type": "object",
            "required": [
              "verification",
              "claimed_via",
              "published",
              "indexable",
              "pro_until",
              "indexnow_at",
              "bing_at",
              "crawlers_30d",
              "facts",
              "all_facts",
              "completeness",
              "has_photo"
            ],
            "properties": {
              "verification": {
                "$ref": "#/components/schemas/NameVerification"
              },
              "claimed_via": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "How the profile was claimed through this client (the claim's method, for example partner_declaration or id_check). It does not change when the person verifies."
              },
              "published": {
                "type": "boolean"
              },
              "indexable": {
                "type": "boolean",
                "description": "Open to search engines and listed in the sitemaps."
              },
              "pro_until": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The end of Pro from this client."
              },
              "indexnow_at": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "When the page was last sent to IndexNow for this client."
              },
              "bing_at": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "When the page was last sent to Bing URL Submission."
              },
              "crawlers_30d": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                },
                "description": "Reads of the page in the last 30 days by search engine and AI service."
              },
              "facts": {
                "type": "array",
                "description": "The facts this client added (and nobody changed since): the ids for PATCH.",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "property": {
                      "type": "string"
                    },
                    "value": {},
                    "visibility": {
                      "type": "string"
                    },
                    "source_url": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "verification_level": {
                      "type": "integer"
                    }
                  }
                }
              },
              "all_facts": {
                "type": "array",
                "description": "Every public fact of the profile, the person's own included (no private fact).",
                "items": {
                  "$ref": "#/components/schemas/PublicFact"
                }
              },
              "completeness": {
                "$ref": "#/components/schemas/Completeness"
              },
              "has_photo": {
                "type": "boolean",
                "description": "The profile has a public photo."
              },
              "test": {
                "type": "boolean",
                "description": "A test client: nothing is public."
              }
            }
          }
        ]
      },
      "NameVerification": {
        "type": "object",
        "required": [
          "level",
          "method"
        ],
        "description": "The name's current verification, read from the name fact, so it changes when the person verifies (or a verification expires). level: 0 public source, 1 self-declared, 2 account, 3 document, 4 ID. A test client's claim shows the level and method the claim would give.",
        "properties": {
          "level": {
            "type": "integer",
            "minimum": 0,
            "maximum": 4
          },
          "method": {
            "oneOf": [
              {
                "type": "string",
                "enum": [
                  "oauth_google",
                  "oauth_linkedin",
                  "oauth_github",
                  "oauth_x",
                  "oauth_orcid",
                  "oauth_youtube",
                  "oauth_searchconsole",
                  "oauth_facebook",
                  "oauth_instagram",
                  "oauth_tiktok",
                  "email_code",
                  "work_email",
                  "link_back",
                  "dns",
                  "open_badge",
                  "document",
                  "id_check",
                  "organization",
                  "partner_attestation",
                  "other",
                  "linkedin",
                  "facebook",
                  "work_email",
                  "id_check",
                  "website",
                  "manual",
                  "partner_declaration"
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "How the name was verified (see VerificationMethod); null when it is not verified (level 0 or 1)."
          }
        }
      },
      "VerificationMethod": {
        "type": "string",
        "enum": [
          "oauth_google",
          "oauth_linkedin",
          "oauth_github",
          "oauth_x",
          "oauth_orcid",
          "oauth_youtube",
          "oauth_searchconsole",
          "oauth_facebook",
          "oauth_instagram",
          "oauth_tiktok",
          "email_code",
          "work_email",
          "link_back",
          "dns",
          "open_badge",
          "document",
          "id_check",
          "organization",
          "partner_attestation",
          "other"
        ],
        "description": "How a fact was verified. oauth_*: the person signed in or connected that account; email_code: a code to the email declared with a partner; work_email: a code to a work address; link_back and dns: a website proven by a link or a DNS record; open_badge: an Open Badges credential; document: a document an admin reviewed; id_check: an identity document check; organization: the person's organization confirmed the role; partner_attestation: a partner's attested claim; other: a method added later."
      },
      "PublicFact": {
        "type": "object",
        "required": [
          "id",
          "property",
          "value",
          "source_url",
          "verification_level",
          "verification_method",
          "verified_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The fact's id, as in verification_changed webhooks and ?view=public."
          },
          "property": {
            "type": "string"
          },
          "value": {
            "description": "As on the public page; a photo's contentUrl is its selfbadge.com address."
          },
          "source_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "verification_level": {
            "type": "integer",
            "minimum": 0,
            "maximum": 4
          },
          "verification_method": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/VerificationMethod"
              },
              {
                "type": "null"
              }
            ]
          },
          "verified_at": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Completeness": {
        "type": "object",
        "required": [
          "percent",
          "missing"
        ],
        "description": "The profile strength the person sees in the editor, from the same checks and points. missing, as stable codes: photo (a photo), about (one line about the person), work (job title and organization), work_email (a verified work email), verified_accounts (two verified accounts), website (a verified website), education, expertise (topics). On ?view=public it counts the public facts only.",
        "properties": {
          "percent": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "missing": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "photo",
                "about",
                "work",
                "work_email",
                "verified_accounts",
                "website",
                "education",
                "expertise"
              ]
            }
          }
        }
      },
      "ProfilePatch": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "add": {
            "type": "array",
            "maxItems": 30,
            "items": {
              "$ref": "#/components/schemas/PartnerFact"
            }
          },
          "change": {
            "type": "array",
            "maxItems": 30,
            "items": {
              "type": "object",
              "required": [
                "id"
              ],
              "properties": {
                "id": {
                  "type": "string"
                },
                "value": {},
                "organization": {},
                "source_url": {
                  "type": "string",
                  "format": "uri"
                },
                "visibility": {
                  "type": "string",
                  "enum": [
                    "public",
                    "private"
                  ]
                }
              }
            }
          },
          "remove": {
            "type": "array",
            "maxItems": 30,
            "items": {
              "type": "string",
              "description": "A fact id."
            }
          },
          "not_confused_with": {
            "description": "The \"not to be confused with\" text: a namesake's public description (at most 120 characters), or {description, url}. Needs an active Pro plan from this client for this person.",
            "oneOf": [
              {
                "type": "string",
                "maxLength": 120
              },
              {
                "type": "object",
                "required": [
                  "description"
                ],
                "properties": {
                  "description": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "url": {
                    "type": "string",
                    "format": "uri"
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "PatchResult": {
        "type": "object",
        "required": [
          "handle",
          "added",
          "changed",
          "removed",
          "announced"
        ],
        "properties": {
          "handle": {
            "type": "string"
          },
          "added": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "changed": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "removed": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "announced": {
            "type": "boolean"
          }
        }
      },
      "ClaimLink": {
        "type": "object",
        "required": [
          "url",
          "expires_at"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "example": "https://selfbadge.com/claim/lena-marlowe?t=sbl_..."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DraftCreated": {
        "type": "object",
        "required": [
          "result",
          "profile",
          "claim_link"
        ],
        "properties": {
          "result": {
            "type": "string",
            "const": "created"
          },
          "profile": {
            "$ref": "#/components/schemas/Draft"
          },
          "claim_link": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ClaimLink"
              },
              {
                "type": "null"
              }
            ]
          },
          "test": {
            "type": "boolean"
          }
        }
      },
      "PossibleMatches": {
        "type": "object",
        "required": [
          "result",
          "matches",
          "message"
        ],
        "properties": {
          "result": {
            "type": "string",
            "const": "possible_match"
          },
          "message": {
            "type": "string"
          },
          "matches": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "handle",
                "url",
                "name",
                "status",
                "claim_link"
              ],
              "properties": {
                "handle": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "headline": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "seeded",
                    "claimed",
                    "draft"
                  ]
                },
                "claim_link": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ClaimLink"
                    },
                    {
                      "type": "null"
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "ClaimLinkRequest": {
        "type": "object",
        "required": [
          "handle"
        ],
        "properties": {
          "handle": {
            "type": "string",
            "example": "lena-marlowe"
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Required for a profile made from public sources; a draft's own email is used when absent."
          }
        }
      },
      "ClaimLinkCreated": {
        "type": "object",
        "required": [
          "handle",
          "claim_link"
        ],
        "properties": {
          "handle": {
            "type": "string"
          },
          "claim_link": {
            "$ref": "#/components/schemas/ClaimLink"
          }
        }
      },
      "AttestedClaimRequest": {
        "type": "object",
        "required": [
          "handle",
          "email",
          "method",
          "evidence",
          "declaration"
        ],
        "additionalProperties": false,
        "properties": {
          "handle": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "The owner's email: SelfBadge makes their account for it (private)."
          },
          "method": {
            "type": "string",
            "enum": [
              "linkedin",
              "facebook",
              "work_email",
              "id_check",
              "website",
              "manual",
              "partner_declaration"
            ]
          },
          "evidence": {
            "type": "object",
            "description": "linkedin, facebook: {account_id, profile_url (optional), name (as the provider returned it)}. work_email: {email, name}. id_check: {verification_session_id (vs_...), verified_name}. website: {url (where the code was found), name}. manual: {reviewer_id and reviewer_name (a named person on this client's staff, never an automatic process), note, name}. partner_declaration: {declared_at (ISO time), declared_name, email (the same as email), name_rule (this client's rule for the name, short text)}.",
            "additionalProperties": {
              "type": "string"
            }
          },
          "declaration": {
            "type": "object",
            "required": [
              "text",
              "accepted_at",
              "ip"
            ],
            "properties": {
              "text": {
                "type": "string",
                "const": "I confirm I am this person and that the facts are about me"
              },
              "accepted_at": {
                "type": "string",
                "format": "date-time"
              },
              "ip": {
                "type": "string",
                "description": "The person's network address when they accepted (kept 24 months)."
              }
            }
          },
          "test_outcome": {
            "type": "string",
            "enum": [
              "approved",
              "rejected"
            ],
            "description": "Test clients only: how a claim that needs review is decided."
          }
        }
      },
      "AttestedClaimPublished": {
        "type": "object",
        "required": [
          "result",
          "handle",
          "url",
          "verification"
        ],
        "properties": {
          "result": {
            "type": "string",
            "const": "published"
          },
          "handle": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "verification": {
            "type": "object",
            "properties": {
              "level": {
                "type": "integer"
              },
              "method": {
                "type": "string"
              }
            }
          },
          "test": {
            "type": "boolean"
          }
        }
      },
      "AttestedClaimReview": {
        "type": "object",
        "required": [
          "result",
          "handle",
          "claim_id",
          "reasons"
        ],
        "properties": {
          "result": {
            "type": "string",
            "const": "review"
          },
          "handle": {
            "type": "string"
          },
          "claim_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "reasons": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "method_needs_review",
                "name_differs",
                "public_source",
                "account_has_profile"
              ]
            }
          },
          "test": {
            "type": "boolean"
          }
        }
      },
      "PublicProfile": {
        "type": "object",
        "required": [
          "handle",
          "status"
        ],
        "description": "The public page of a profile this client created or manages (?view=public). Only status, updated_at and handle when it is not public; removed_at and reason_code when removed.",
        "properties": {
          "handle": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "published",
              "not_published",
              "flagged",
              "removed"
            ]
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "name": {
            "type": "string"
          },
          "headline": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "facts": {
            "type": "array",
            "description": "Every public fact, as /<handle>.json gives it, with its id: id, property, value, verification {level, name, label, method, verifiedAt, expiresAt}, source {type, label, url, retrievedAt}.",
            "items": {
              "type": "object",
              "required": [
                "id"
              ],
              "properties": {
                "id": {
                  "type": "string"
                }
              }
            }
          },
          "photo_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "The public photo's selfbadge.com address."
          },
          "has_photo": {
            "type": "boolean"
          },
          "completeness": {
            "$ref": "#/components/schemas/Completeness"
          },
          "links": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "label": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "handle": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          },
          "not_confused_with": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "who_is": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "removed_at": {
            "type": "string",
            "format": "date-time"
          },
          "reason_code": {
            "type": "string"
          },
          "test": {
            "type": "boolean"
          }
        }
      },
      "EditLink": {
        "type": "object",
        "required": [
          "url",
          "expires_at"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "test": {
            "type": "boolean",
            "description": "A test client's link: it opens the Test edit link page (no sign-in, no editor)."
          }
        }
      },
      "Preview": {
        "type": "object",
        "required": [
          "url",
          "expires_at"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "test": {
            "type": "boolean"
          }
        }
      },
      "AttestedClaimRejected": {
        "type": "object",
        "required": [
          "result",
          "handle",
          "reason",
          "test"
        ],
        "properties": {
          "result": {
            "type": "string",
            "const": "rejected"
          },
          "handle": {
            "type": "string"
          },
          "reason": {
            "type": "string"
          },
          "test": {
            "type": "boolean"
          }
        }
      },
      "PlanRequest": {
        "type": "object",
        "required": [
          "handle",
          "months",
          "order_id"
        ],
        "additionalProperties": false,
        "properties": {
          "handle": {
            "type": "string"
          },
          "months": {
            "type": "integer",
            "enum": [
              1,
              3,
              12
            ]
          },
          "order_id": {
            "type": "string",
            "maxLength": 120,
            "description": "Your order id; the same id is never given twice."
          }
        }
      },
      "PlanResult": {
        "type": "object",
        "required": [
          "result",
          "order_id",
          "handle",
          "pro_until"
        ],
        "properties": {
          "result": {
            "type": "string",
            "enum": [
              "created",
              "extended",
              "exists"
            ]
          },
          "order_id": {
            "type": "string"
          },
          "handle": {
            "type": "string"
          },
          "pro_until": {
            "type": "string",
            "format": "date-time"
          },
          "test": {
            "type": "boolean"
          }
        }
      },
      "RevocationRequest": {
        "type": "object",
        "required": [
          "order_id",
          "reason"
        ],
        "additionalProperties": false,
        "properties": {
          "order_id": {
            "type": "string"
          },
          "reason": {
            "type": "string",
            "enum": [
              "refund",
              "withdrawal"
            ]
          },
          "scope": {
            "type": "string",
            "enum": [
              "profile",
              "order"
            ],
            "default": "profile",
            "description": "profile: Pro ends and the profile goes. order: only this order's Pro is cancelled."
          }
        }
      },
      "RevocationResult": {
        "type": "object",
        "required": [
          "result",
          "scope",
          "order_id",
          "pro_ended",
          "profile"
        ],
        "properties": {
          "result": {
            "type": "string",
            "enum": [
              "revoked",
              "already_revoked"
            ]
          },
          "scope": {
            "type": "string",
            "enum": [
              "profile",
              "order"
            ]
          },
          "pro_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Scope order: the end of this client's remaining Pro for the person, or null when none remains."
          },
          "order_id": {
            "type": "string"
          },
          "pro_ended": {
            "type": "integer"
          },
          "profile": {
            "type": "string",
            "enum": [
              "removed",
              "unclaimed",
              "detached",
              "none"
            ]
          },
          "test": {
            "type": "boolean"
          }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "required": [
          "id",
          "event",
          "created_at",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "event": {
            "type": "string",
            "enum": [
              "review_needed",
              "review_decided",
              "published",
              "flagged",
              "disputed",
              "restored",
              "removed",
              "owner_changed",
              "verification_changed"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "test": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "properties": {
              "handle": {
                "type": "string"
              },
              "claim_id": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "reasons": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "decision": {
                "type": "string",
                "enum": [
                  "approved",
                  "rejected"
                ]
              },
              "reason": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "url": {
                "type": "string"
              },
              "reason_code": {
                "type": "string",
                "description": "flagged, disputed, restored, removed: why, as a code.",
                "enum": [
                  "report_under_review",
                  "hidden_by_admin",
                  "impersonation_report",
                  "other_claim",
                  "review_closed",
                  "removed_after_report",
                  "removed_by_admin",
                  "account_deleted"
                ]
              },
              "at": {
                "type": "string",
                "format": "date-time",
                "description": "flagged, disputed, restored, removed, owner_changed, verification_changed: when it happened."
              },
              "level": {
                "type": "integer",
                "minimum": 0,
                "maximum": 4,
                "description": "verification_changed: the name's current level, as on GET /profiles/{handle}."
              },
              "method": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/VerificationMethod"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "verification_changed: how the name is verified now, or null."
              },
              "fact_ids": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "verification_changed: the public facts whose verification changed since the last event (at most one event a minute per profile)."
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "A stable code: missing_parameter, invalid_id, invalid_name, invalid_slug, not_found, invalid_key, rate_limited, unavailable, server_error."
              },
              "message": {
                "type": "string",
                "description": "What happened, in plain words."
              }
            }
          }
        }
      },
      "JsonLd": {
        "type": "object",
        "description": "schema.org JSON-LD with an @graph.",
        "required": [
          "@context",
          "@graph"
        ],
        "properties": {
          "@context": {
            "type": "string"
          },
          "@graph": {
            "type": "array",
            "items": {
              "type": "object"
            }
          }
        }
      },
      "PersonSummary": {
        "type": "object",
        "required": [
          "id",
          "handle",
          "url",
          "json",
          "name",
          "headline",
          "jobTitle",
          "organization",
          "location",
          "description",
          "status",
          "facts"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uri",
            "description": "The stable Person @id: cite the person by it."
          },
          "handle": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The profile page."
          },
          "json": {
            "type": "string",
            "format": "uri",
            "description": "The profile as JSON with verification (/<handle>.json)."
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "headline": {
            "type": [
              "string",
              "null"
            ]
          },
          "jobTitle": {
            "type": [
              "string",
              "null"
            ]
          },
          "organization": {
            "type": [
              "string",
              "null"
            ]
          },
          "location": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "A line that tells the person apart from namesakes, or their description."
          },
          "status": {
            "type": "string",
            "description": "seeded (created from public sources, not claimed), or the claimed profile's state."
          },
          "facts": {
            "type": "object",
            "required": [
              "published",
              "verified"
            ],
            "properties": {
              "published": {
                "type": "integer"
              },
              "verified": {
                "type": "integer"
              }
            }
          }
        }
      },
      "SearchCandidate": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PersonSummary"
          },
          {
            "type": "object",
            "required": [
              "matchedHints",
              "score"
            ],
            "properties": {
              "seeded": {
                "type": "boolean",
                "description": "Present and true for a profile created from public sources and not claimed."
              },
              "note": {
                "type": "string"
              },
              "matchedHints": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "jobTitle",
                    "organization",
                    "location"
                  ]
                }
              },
              "score": {
                "type": "integer",
                "description": "How many of the given hints the profile matches."
              }
            }
          }
        ]
      },
      "SearchResult": {
        "type": "object",
        "required": [
          "query",
          "count",
          "results"
        ],
        "properties": {
          "query": {
            "type": "object",
            "required": [
              "name",
              "jobTitle",
              "organization",
              "location"
            ],
            "properties": {
              "name": {
                "type": "string"
              },
              "jobTitle": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "organization": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "location": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "count": {
            "type": "integer"
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SearchCandidate"
            }
          },
          "note": {
            "type": "string",
            "description": "Guidance when several people share the name, or none was found."
          }
        }
      },
      "Fact": {
        "type": "object",
        "required": [
          "property",
          "value",
          "verification",
          "source"
        ],
        "properties": {
          "property": {
            "type": "string",
            "description": "The schema.org Person property, for example jobTitle or sameAs."
          },
          "value": {
            "description": "The value: a string, or an object for structured facts (roles, education, accounts)."
          },
          "verification": {
            "type": "object",
            "required": [
              "level",
              "name",
              "label"
            ],
            "properties": {
              "level": {
                "type": "integer",
                "minimum": 0,
                "maximum": 4
              },
              "name": {
                "type": "string"
              },
              "label": {
                "type": "string"
              },
              "method": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "verifiedAt": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "expiresAt": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "source": {
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "description": "self, import, connector or seed."
              },
              "label": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "url": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "retrievedAt": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          }
        }
      },
      "Verification": {
        "type": "object",
        "required": [
          "method",
          "label",
          "checkedAt"
        ],
        "properties": {
          "method": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "checkedAt": {
            "type": [
              "string",
              "null"
            ]
          },
          "rechecked": {
            "type": "string"
          }
        }
      },
      "Person": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PersonSummary"
          },
          {
            "type": "object",
            "required": [
              "verificationLevels",
              "publicFacts",
              "verifications",
              "jsonld",
              "dateModified"
            ],
            "properties": {
              "seeded": {
                "type": "boolean"
              },
              "sample": {
                "type": "boolean",
                "description": "A fictional sample profile."
              },
              "note": {
                "type": "string"
              },
              "verificationLevels": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                },
                "description": "What each level means, keyed 0 to 4."
              },
              "publicFacts": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Fact"
                }
              },
              "verifications": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Verification"
                }
              },
              "jsonld": {
                "$ref": "#/components/schemas/JsonLd"
              },
              "dateModified": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          }
        ]
      },
      "Organization": {
        "type": "object",
        "required": [
          "id",
          "slug",
          "url",
          "name",
          "website",
          "description",
          "claimed",
          "team",
          "founders",
          "jsonld"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uri"
          },
          "slug": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "name": {
            "type": "string"
          },
          "website": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "claimed": {
            "type": "boolean"
          },
          "team": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "handle"
              ],
              "properties": {
                "id": {
                  "type": "string"
                },
                "handle": {
                  "type": "string"
                },
                "title": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          },
          "founders": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "handle"
              ],
              "properties": {
                "id": {
                  "type": "string"
                },
                "handle": {
                  "type": "string"
                }
              }
            }
          },
          "jsonld": {
            "$ref": "#/components/schemas/JsonLd"
          }
        }
      }
    }
  }
}
