{
  "openapi": "3.1.0",
  "info": {
    "title": "mxverify BV API",
    "version": "1.0.0",
    "summary": "Any website's BV score — what one new followed link from it is worth, in US dollars — as JSON.",
    "description": "The BV API returns the same figure a site's mxverify report and badge show. A free key from https://bklink.uk/api-keys gives 1,000 lookups and 25 new valuations a day; the partner tier adds volume and bulk valuation jobs, paid per call from a bklink wallet.\n\nWherever you show a BV, show \"BV by mxverify\" beside it, linked to the site's report (`attribution` in every valued or unrated result). BV is the ranking benefit of a link under a published model, not a price.\n\nFull documentation: https://bklink.uk/docs/api",
    "termsOfService": "https://bklink.uk/docs/api/attribution-and-terms",
    "contact": {
      "name": "bklink support",
      "email": "support@bklink.uk",
      "url": "https://bklink.uk/docs/api"
    },
    "license": {
      "name": "BV API terms of use",
      "url": "https://bklink.uk/docs/api/attribution-and-terms"
    }
  },
  "externalDocs": {
    "description": "BV API documentation",
    "url": "https://bklink.uk/docs/api"
  },
  "servers": [
    {
      "url": "https://mxverify.uk",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    },
    {
      "apiKeyHeader": []
    }
  ],
  "tags": [
    {
      "name": "Lookups",
      "description": "One site, or up to 100 in one request. Every key."
    },
    {
      "name": "Bulk jobs",
      "description": "Up to 1,000 sites valued in the background. Partner keys only."
    }
  ],
  "paths": {
    "/api/v1/bv/{domain}": {
      "get": {
        "tags": [
          "Lookups"
        ],
        "operationId": "lookupSite",
        "summary": "Look up one site",
        "description": "One site's BV. A site mxverify has never valued is valued on demand: the first answer is `valuing` (HTTP 202) with `retry_after`, and a lookup after that has the figure. Counts as one lookup.",
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "description": "A website address. A full URL or a www. form is reduced to the site; a subdomain is its own site. Reserved names such as example.com answer `invalid`.",
            "schema": {
              "type": "string",
              "examples": [
                "google.com"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A result: `valued`, `unrated`, `not_valued` or `not_found`.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "examples": {
                  "valued": {
                    "$ref": "#/components/examples/Valued"
                  },
                  "unrated": {
                    "$ref": "#/components/examples/Unrated"
                  }
                }
              }
            }
          },
          "202": {
            "description": "`valuing`: the site is being valued now. Ask again after `retry_after` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "examples": {
                  "valuing": {
                    "$ref": "#/components/examples/Valuing"
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid`: the target is not a website address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "examples": {
                  "invalid": {
                    "$ref": "#/components/examples/Invalid"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/LimitReached"
          }
        }
      }
    },
    "/api/v1/bv": {
      "post": {
        "tags": [
          "Lookups"
        ],
        "operationId": "lookupSites",
        "summary": "Look up many sites",
        "description": "Up to 100 targets in one request; one result per target, in the order sent. Each target counts as one lookup.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Targets100"
              },
              "examples": {
                "two": {
                  "value": {
                    "targets": [
                      "google.com",
                      "newly-launched-site.com"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One result per target.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Results"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/LimitReached"
          }
        }
      }
    },
    "/api/v1/bv/valuations": {
      "post": {
        "tags": [
          "Bulk jobs"
        ],
        "operationId": "startValuationJob",
        "summary": "Start a bulk valuation job",
        "description": "Partner keys only. Up to 1,000 targets; every target is a lookup, taken when the job starts. Sites mxverify already values are answered at once; sites it has never valued are valued as the job reaches them. A key can run 3 jobs at once; jobs are kept a week.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Targets1000"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The job has started.",
            "headers": {
              "Location": {
                "description": "The job's address (the same as `status_url`).",
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobStarted"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PartnerOnly"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/LimitReached"
          },
          "503": {
            "description": "The job could not be started just now (`unavailable`). Try again in a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/bv/valuations/{id}": {
      "get": {
        "tags": [
          "Bulk jobs"
        ],
        "operationId": "getValuationJob",
        "summary": "Read a bulk valuation job",
        "description": "Progress, and every site's answer so far (`pending` for sites not reached yet). Reading a job is not a lookup. While running, the answer carries `Retry-After: 30`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The job id from `startValuationJob`.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{8,40}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The job.",
            "headers": {
              "Retry-After": {
                "description": "Present while the job is running: read it no more often than this.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PartnerOnly"
          },
          "404": {
            "description": "No such job for this key (`not_found`). Jobs are kept a week.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "A key from https://bklink.uk/api-keys: `Authorization: Bearer mxk_…`."
      },
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "The same key, in the X-API-Key header."
      }
    },
    "headers": {
      "RateLimitLimit": {
        "description": "The key's lookups a day (UTC).",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitRemaining": {
        "description": "Lookups left today.",
        "schema": {
          "type": "integer"
        }
      },
      "RetryAfter": {
        "description": "Seconds until it is worth asking again.",
        "schema": {
          "type": "integer"
        }
      }
    },
    "schemas": {
      "Targets100": {
        "type": "object",
        "required": [
          "targets"
        ],
        "additionalProperties": false,
        "properties": {
          "targets": {
            "type": "array",
            "minItems": 1,
            "maxItems": 100,
            "items": {
              "type": "string"
            },
            "description": "Website addresses."
          }
        }
      },
      "Targets1000": {
        "type": "object",
        "required": [
          "targets"
        ],
        "additionalProperties": false,
        "properties": {
          "targets": {
            "type": "array",
            "minItems": 1,
            "maxItems": 1000,
            "items": {
              "type": "string"
            },
            "description": "Website addresses."
          }
        }
      },
      "Bv": {
        "type": "object",
        "required": [
          "usd",
          "per",
          "level",
          "caps",
          "version",
          "audited"
        ],
        "properties": {
          "usd": {
            "type": "number",
            "description": "The value of one new followed link from the site, in US dollars, to the cent."
          },
          "per": {
            "type": "string",
            "const": "followed link",
            "description": "The unit of `usd`."
          },
          "level": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "A",
              "B",
              "C",
              "D",
              null
            ],
            "description": "The level the link is priced at: A editorial, B guest or sponsored, C neutralised, D link network."
          },
          "caps": {
            "type": "string",
            "description": "What lowered the level, in plain words (for example \"nofollow\"); empty for none."
          },
          "version": {
            "type": "string",
            "description": "The BV model the figure was worked out under, for example \"2.8\"."
          },
          "audited": {
            "type": "boolean",
            "description": "Links placed on the site have been checked by bklink audits."
          }
        }
      },
      "Attribution": {
        "type": "object",
        "required": [
          "text",
          "url",
          "required"
        ],
        "description": "What to show beside the figure: the words, linked to the site's report.",
        "properties": {
          "text": {
            "type": "string",
            "const": "BV by mxverify"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "required": {
            "type": "boolean",
            "const": true
          }
        }
      },
      "Result": {
        "type": "object",
        "required": [
          "target",
          "status",
          "bv"
        ],
        "properties": {
          "target": {
            "type": "string",
            "description": "What was sent, as sent (up to 300 characters)."
          },
          "status": {
            "type": "string",
            "enum": [
              "valued",
              "unrated",
              "valuing",
              "not_valued",
              "not_found",
              "invalid"
            ],
            "description": "valued: the figure is in `bv`. unrated: no authority rating on record, so not valued (show nothing, never $0). valuing: being valued now. not_valued: a new site could not be valued now (`reason`). not_found: no website at the address. invalid: not a website address."
          },
          "bv": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Bv"
              },
              {
                "type": "null"
              }
            ]
          },
          "report_url": {
            "type": "string",
            "format": "uri",
            "description": "The site's mxverify report; the checker page when a valuation was refused."
          },
          "attribution": {
            "$ref": "#/components/schemas/Attribution"
          },
          "retry_after": {
            "type": "integer",
            "description": "Seconds until it is worth asking again."
          },
          "reason": {
            "type": "string",
            "description": "Why, in plain words."
          }
        }
      },
      "Results": {
        "type": "object",
        "required": [
          "results"
        ],
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Result"
            }
          }
        }
      },
      "Pending": {
        "type": "object",
        "required": [
          "target",
          "status",
          "bv"
        ],
        "properties": {
          "target": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "const": "pending"
          },
          "bv": {
            "type": "null"
          }
        }
      },
      "JobStarted": {
        "type": "object",
        "required": [
          "id",
          "status",
          "total",
          "done",
          "status_url"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "const": "running"
          },
          "total": {
            "type": "integer"
          },
          "done": {
            "type": "integer",
            "description": "Targets answered already (including any that are not website addresses)."
          },
          "status_url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "Job": {
        "type": "object",
        "required": [
          "id",
          "status",
          "total",
          "done",
          "created_at",
          "results"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "running",
              "done"
            ]
          },
          "total": {
            "type": "integer"
          },
          "done": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "finished_at": {
            "type": "string",
            "format": "date-time"
          },
          "results": {
            "type": "array",
            "items": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/Result"
                },
                {
                  "$ref": "#/components/schemas/Pending"
                }
              ]
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "missing_key",
              "invalid_key",
              "bad_request",
              "too_many_targets",
              "method_not_allowed",
              "daily_limit",
              "funds_needed",
              "partner_only",
              "too_many_jobs",
              "not_found",
              "unavailable"
            ]
          },
          "message": {
            "type": "string"
          },
          "docs": {
            "type": "string",
            "format": "uri"
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "`bad_request` (the body is not `{\"targets\": [...]}` with at least one target) or `too_many_targets`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "`missing_key` or `invalid_key` (does not exist, or was revoked).",
        "headers": {
          "WWW-Authenticate": {
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PartnerOnly": {
        "description": "`partner_only`: bulk jobs need a partner key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "`method_not_allowed`: the `Allow` header names the method for this path.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "LimitReached": {
        "description": "`daily_limit` (the key's lookups for the UTC day are used), `funds_needed` (a partner key's free lookups are used and its wallet does not cover more) or `too_many_jobs`.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "examples": {
      "Valued": {
        "summary": "A valued site",
        "value": {
          "target": "google.com",
          "status": "valued",
          "bv": {
            "usd": 182.92,
            "per": "followed link",
            "level": "C",
            "caps": "",
            "version": "2.8",
            "audited": false
          },
          "report_url": "https://mxverify.uk/publications/google.com",
          "attribution": {
            "text": "BV by mxverify",
            "url": "https://mxverify.uk/publications/google.com",
            "required": true
          }
        }
      },
      "Unrated": {
        "summary": "No authority rating on record",
        "value": {
          "target": "unrated-site.com",
          "status": "unrated",
          "bv": null,
          "report_url": "https://mxverify.uk/publications/unrated-site.com",
          "attribution": {
            "text": "BV by mxverify",
            "url": "https://mxverify.uk/publications/unrated-site.com",
            "required": true
          },
          "reason": "No authority rating is on record for this site, so a link from it is not valued."
        }
      },
      "Valuing": {
        "summary": "A site never valued, being valued now",
        "value": {
          "target": "newly-launched-site.com",
          "status": "valuing",
          "bv": null,
          "retry_after": 120
        }
      },
      "Invalid": {
        "summary": "Not a website address",
        "value": {
          "target": "not a site",
          "status": "invalid",
          "bv": null,
          "reason": "Not a website address. Send a domain such as example.com."
        }
      }
    }
  }
}
