Pinning Monster

Unleash the power of certificate pinning!

🔌 Monster API

Monster has a REST API! Feed it domains programmatically and get certificate pins back in any format.

Endpoint

GET /api/pin/:domains

Parameters

ParameterTypeDescription
:domainspathComma-separated domains (max 10)
formatqueryOutput format — comma-separated for multiple (e.g. android,ios)
AcceptheaderContent negotiation alternative to format

Formats

FormatContent-TypeDescription
plaintext/plainHuman-readable text (default)
application/jsonapplication/jsonJSON array of pin objects
csvtext/csvCSV with host, subject, hash columns
xmlapplication/xmlXML document
yamltext/yamlYAML document
androidapplication/xmlAndroid network-security-config.xml
iosapplication/xmliOS TrustKit plist configuration

Multiple Formats

Request several formats at once with comma-separated values. Monster returns a JSON object keyed by format name:

curl "https://pinning.monster/api/pin/google.com?format=android,ios"

# Returns: { "android": "<network-security-config>...", "ios": "<plist>..." }

Code Examples

# Plain text (default)curl https://pinning.monster/api/pin/google.com# JSON formatcurl https://pinning.monster/api/pin/google.com?format=application/json# Multiple domainscurl https://pinning.monster/api/pin/google.com,github.com# Android network security configcurl https://pinning.monster/api/pin/google.com?format=android# iOS TrustKit configcurl https://pinning.monster/api/pin/google.com?format=ios# Content negotiationcurl -H "Accept: application/json" https://pinning.monster/api/pin/google.com
import requests# Get pins as JSONresponse = requests.get(    "https://pinning.monster/api/pin/google.com",    params={"format": "application/json"})pins = response.json()# Multiple domainsresponse = requests.get("https://pinning.monster/api/pin/google.com,github.com")print(response.text)# Android configresponse = requests.get(    "https://pinning.monster/api/pin/google.com",    params={"format": "android"})with open("network_security_config.xml", "w") as f:    f.write(response.text)
// Fetch pins as JSONconst res = await fetch(  "https://pinning.monster/api/pin/google.com?format=application/json");const pins = await res.json();console.log(pins);// Multiple domainsconst multi = await fetch(  "https://pinning.monster/api/pin/google.com,github.com");console.log(await multi.text());// Content negotiationconst json = await fetch("https://pinning.monster/api/pin/google.com", {  headers: { Accept: "application/json" },});
package mainimport (	"fmt"	"io"	"net/http")func main() {	resp, err := http.Get(		"https://pinning.monster/api/pin/google.com?format=application/json",	)	if err != nil {		panic(err)	}	defer resp.Body.Close()	body, _ := io.ReadAll(resp.Body)	fmt.Println(string(body))}
import Foundationlet url = URL(  string: "https://pinning.monster/api/pin/google.com?format=ios")!let task = URLSession.shared.dataTask(with: url) {  data, response, error in  guard let data = data else { return }  let config = String(data: data, encoding: .utf8)!  print(config)}task.resume()// Keep playground aliveRunLoop.main.run(until: Date(timeIntervalSinceNow: 10))
import java.net.URLfun main() {    // Get Android network security config    val url = URL(        "https://pinning.monster/api/pin/google.com?format=android"    )    val config = url.readText()    println(config)    // Get pins as JSON    val json = URL(        "https://pinning.monster/api/pin/google.com?format=application/json"    ).readText()    println(json)}
require "net/http"require "json"# Get pins as JSONuri = URI("https://pinning.monster/api/pin/google.com?format=application/json")response = Net::HTTP.get(uri)pins = JSON.parse(response)puts pins# Multiple domainsuri = URI("https://pinning.monster/api/pin/google.com,github.com")puts Net::HTTP.get(uri)# Save Android config to fileuri = URI("https://pinning.monster/api/pin/google.com?format=android")File.write("network_security_config.xml", Net::HTTP.get(uri))
import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;public class PinFetcher {    public static void main(String[] args) throws Exception {        var client = HttpClient.newHttpClient();        // Get pins as JSON        var request = HttpRequest.newBuilder()            .uri(URI.create(                "https://pinning.monster/api/pin/google.com?format=application/json"            ))            .build();        var response = client.send(            request,            HttpResponse.BodyHandlers.ofString()        );        System.out.println(response.body());    }}

Interactive Docs & OpenAPI

Want to try the API right in your browser? Open the Swagger UI playground — it lets you pick formats, enter domains, and fire real requests. You can also grab the machine-readable OpenAPI 3.0 spec to generate clients, import into Postman, or feed your own tooling.

CI/CD Integration

Keep your certificate pins fresh automatically! Drop one of these into your pipeline to update pins on every build.

GitHub Actions

# .github/workflows/update-pins.yml
name: Update certificate pins
on:
  schedule:
    - cron: "0 6 * * 1"  # Every Monday at 06:00 UTC
  workflow_dispatch:

jobs:
  update-pins:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Android
      - name: Update Android pins
        run: |
          curl -sf "https://pinning.monster/api/pin/${{ vars.PIN_DOMAINS }}?format=android" \
            -o app/src/main/res/xml/network_security_config.xml

      # iOS (TrustKit)
      - name: Update iOS pins
        run: |
          curl -sf "https://pinning.monster/api/pin/${{ vars.PIN_DOMAINS }}?format=ios" \
            -o ios/TrustKit.plist

      - name: Commit if changed
        run: |
          git diff --quiet || \
            (git add -A && git commit -m "chore: update certificate pins" && git push)

GitLab CI

# .gitlab-ci.yml
update-pins:
  stage: build
  script:
    - curl -sf "https://pinning.monster/api/pin/$PIN_DOMAINS?format=android"
        -o app/src/main/res/xml/network_security_config.xml
    - curl -sf "https://pinning.monster/api/pin/$PIN_DOMAINS?format=ios"
        -o ios/TrustKit.plist
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"

Fastlane

# fastlane/Fastfile
lane :update_pins do
  require "net/http"
  domains = ENV["PIN_DOMAINS"] || "api.example.com"

  # Android
  android = Net::HTTP.get(
    URI("https://pinning.monster/api/pin/#{domains}?format=android")
  )
  File.write("app/src/main/res/xml/network_security_config.xml", android)

  # iOS
  ios = Net::HTTP.get(
    URI("https://pinning.monster/api/pin/#{domains}?format=ios")
  )
  File.write("ios/TrustKit.plist", ios)

  git_add(path: ".")
  git_commit(
    path: ".",
    message: "chore: update certificate pins"
  )
end

Shell script (any CI)

#!/bin/sh
# update-pins.sh — works in any CI that has curl
set -e
DOMAINS="api.example.com,cdn.example.com"

curl -sf "https://pinning.monster/api/pin/$DOMAINS?format=android,ios" | \
  jq -r '.android' > network_security_config.xml

curl -sf "https://pinning.monster/api/pin/$DOMAINS?format=android,ios" | \
  jq -r '.ios' > TrustKit.plist

echo "Pins updated for: $DOMAINS"

Tip: Store your domains in a CI variable (PIN_DOMAINS) so you can update the list without changing pipeline config. Schedule weekly runs — certificates rotate, but not daily.

Limits