{
  "openapi": "3.1.0",
  "info": {
    "title": "3D Warden integration API",
    "version": "1",
    "description": "Connect a membership system to 3D Warden. A server to server API: Warden sends no CORS headers. A key may make 60 requests a minute, however many records each carries; past that Warden answers 429 with a Retry-After header in seconds. Dates are ISO 8601 in UTC with an offset, and the number of decimal places varies, so parse them rather than compare them as strings. The API is part of the Creator and Studio plans."
  },
  "servers": [
    {
      "url": "https://3dwarden.com"
    }
  ],
  "security": [
    {
      "integrationKey": []
    }
  ],
  "paths": {
    "/api/v1/integration/entitlements": {
      "post": {
        "operationId": "syncEntitlements",
        "summary": "Sync entitlements",
        "description": "Creates or updates up to 500 records, idempotent on external_id. One bad record costs only itself, and one bad identity costs only itself: its record is still written. On a plan without the API, records Warden already holds still update, a new record is returned in rejected, and a new credential is not minted.",
        "parameters": [
          {
            "name": "dry_run",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true",
                "0",
                "false"
              ]
            },
            "description": "Check the request and store nothing. Send 1; true is read the same way. Leaving it out, or sending 0 or false, makes a real request. Any other value is refused with 400 invalid_request."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SyncRequest"
              },
              "examples": {
                "full": {
                  "summary": "One full record",
                  "value": {
                    "records": [
                      {
                        "external_id": "member-1001",
                        "status": "active",
                        "normalized_state": "active",
                        "tier": "commercial",
                        "capabilities": [
                          "physical_sales"
                        ],
                        "effective_from": "2025-12-03T01:02:03Z",
                        "expires_at": null,
                        "credential": {
                          "title": "Commercial license",
                          "holder_name": "Example Print Shop",
                          "registration_id": "EX-1A67D8F2",
                          "registered_at": "2025-12-03T01:02:03Z",
                          "publish_terms": false,
                          "retired": false
                        },
                        "identities": [
                          {
                            "source": "website",
                            "url": "https://shop.example.com",
                            "public": true
                          },
                          {
                            "source": "yourshop_storefront",
                            "handle": "example",
                            "url": "https://yourshop.example/s/example",
                            "public": true,
                            "assurance": "issuer_hosted"
                          }
                        ]
                      }
                    ]
                  }
                },
                "minimal": {
                  "summary": "The three required fields",
                  "value": {
                    "records": [
                      {
                        "external_id": "member-1002",
                        "status": "canceled",
                        "normalized_state": "inactive"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What was written and what was refused. With dry_run=1, what would be.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/SyncResponse"
                    },
                    {
                      "$ref": "#/components/schemas/DryRunResponse"
                    }
                  ]
                },
                "examples": {
                  "written": {
                    "value": {
                      "records": [
                        {
                          "external_id": "member-1001",
                          "public_id": "q3Zr8mW1x0aB4cD5eF6gHi",
                          "normalized_state": "active",
                          "capabilities": [
                            "physical_sales"
                          ],
                          "last_synced_at": "2026-09-21T18:04:11.123456+00:00",
                          "retired": false,
                          "verify_url": "https://3dwarden.com/verify/q3Zr8mW1x0aB4cD5eF6gHi"
                        }
                      ],
                      "rejected": []
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not {\"records\": [...]}, is empty, or has more than 500 records, or dry_run is a value Warden does not read. Code invalid_request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed, unknown or revoked. Code invalid_key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "The key is over its allowance of 60 requests a minute. Code rate_limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait."
              }
            }
          },
          "500": {
            "description": "Something went wrong on Warden's side. Code internal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Warden cannot do this right now. Try again later. Code unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listEntitlements",
        "summary": "List entitlements",
        "description": "Every record this key's provider holds, in external_id order.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 500
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "The next of the page before. Opaque."
          }
        ],
        "responses": {
          "200": {
            "description": "One page.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListResponse"
                },
                "examples": {
                  "page": {
                    "value": {
                      "records": [
                        {
                          "external_id": "member-1001",
                          "public_id": "q3Zr8mW1x0aB4cD5eF6gHi",
                          "normalized_state": "active",
                          "capabilities": [
                            "physical_sales"
                          ],
                          "last_synced_at": "2026-09-21T18:04:11.123456+00:00",
                          "retired": false,
                          "verify_url": "https://3dwarden.com/verify/q3Zr8mW1x0aB4cD5eF6gHi"
                        }
                      ],
                      "next": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "limit is out of range, or the cursor is not one this API returned. Code invalid_request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed, unknown or revoked. Code invalid_key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "The key is over its allowance of 60 requests a minute. Code rate_limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait."
              }
            }
          },
          "500": {
            "description": "Something went wrong on Warden's side. Code internal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Warden cannot do this right now. Try again later. Code unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/integration/entitlements/{external_id}": {
      "get": {
        "operationId": "getEntitlement",
        "summary": "Get an entitlement",
        "description": "One record, read back by the id you wrote it under.",
        "parameters": [
          {
            "name": "external_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The record.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Entitlement"
                },
                "examples": {
                  "one": {
                    "value": {
                      "external_id": "member-1001",
                      "public_id": "q3Zr8mW1x0aB4cD5eF6gHi",
                      "normalized_state": "active",
                      "capabilities": [
                        "physical_sales"
                      ],
                      "last_synced_at": "2026-09-21T18:04:11.123456+00:00",
                      "retired": false,
                      "verify_url": "https://3dwarden.com/verify/q3Zr8mW1x0aB4cD5eF6gHi"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed, unknown or revoked. Code invalid_key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "This key's provider never wrote that id. Code not_found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "The key is over its allowance of 60 requests a minute. Code rate_limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait."
              }
            }
          },
          "500": {
            "description": "Something went wrong on Warden's side. Code internal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Warden cannot do this right now. Try again later. Code unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "eraseEntitlement",
        "summary": "Erase an entitlement",
        "description": "Erases one record by the id you wrote it under: the entitlement, its credential and every identity on it, including ones the member added through an enrollment link. The credential's public address then reads as no longer published and names nobody. Idempotent.",
        "parameters": [
          {
            "name": "external_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          },
          {
            "name": "dry_run",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true",
                "0",
                "false"
              ]
            },
            "description": "Say what would be erased and erase nothing. Send 1; true is read the same way. Leaving it out, or sending 0 or false, makes a real request. Any other value is refused with 400 invalid_request."
          }
        ],
        "responses": {
          "200": {
            "description": "What was erased, or would be on a dry run. A record already gone answers found false.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EraseResponse"
                },
                "examples": {
                  "erased": {
                    "value": {
                      "external_id": "member-1001",
                      "found": true,
                      "erased": true,
                      "dry_run": false,
                      "credential": true,
                      "identities": 2
                    }
                  },
                  "already_gone": {
                    "value": {
                      "external_id": "member-1001",
                      "found": false,
                      "erased": false,
                      "dry_run": false,
                      "credential": false,
                      "identities": 0
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The id is empty or longer than 200 characters, or dry_run is a value Warden does not read. Code invalid_request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed, unknown or revoked. Code invalid_key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "The key is over its allowance of 60 requests a minute. Code rate_limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait."
              }
            }
          },
          "500": {
            "description": "Something went wrong on Warden's side. Code internal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Warden cannot do this right now. Try again later. Code unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/integration/enrollments": {
      "post": {
        "operationId": "createEnrollment",
        "summary": "Create an enrollment invitation",
        "description": "A link one member can use to add where they sell. Send it to them yourself.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EnrollmentRequest"
              },
              "examples": {
                "thirty": {
                  "value": {
                    "external_id": "member-1001",
                    "days": 30
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The invitation. The secret is in this response once.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Enrollment"
                }
              }
            }
          },
          "400": {
            "description": "The body is not JSON, has an unknown field, or days is out of range. Code invalid_request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed, unknown or revoked. Code invalid_key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The organization's plan does not allow invitations. Code plan_not_allowed, with capability enrollments.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "This key's provider does not hold that member. Code not_found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "The key is over its allowance of 60 requests a minute. Code rate_limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait."
              }
            }
          },
          "500": {
            "description": "Something went wrong on Warden's side. Code internal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Warden cannot do this right now. Try again later. Code unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/integration/shop-status": {
      "post": {
        "operationId": "syncShopStatus",
        "summary": "Send shop registration deadlines",
        "description": "Tells Warden until when a shop that is not yet one of your members has to register. Needs a key granted shop_status. Idempotent on the shop. A deadline changes no finding's state: a finding whose shop is inside it stays in Needs you and says until when.",
        "parameters": [
          {
            "name": "dry_run",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true",
                "0",
                "false"
              ]
            },
            "description": "Check the key and read every record, and store nothing. Send 1; true is read the same way. Leaving it out, or sending 0 or false, makes a real request. Any other value is refused with 400 invalid_request."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ShopStatusRequest"
              },
              "examples": {
                "one_shop": {
                  "value": {
                    "records": [
                      {
                        "url": "https://www.etsy.com/shop/ExamplePrints",
                        "grace_until": "2026-10-23T00:00:00Z",
                        "letter_sent_at": "2026-09-23T00:00:00Z"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What was written, or would be on a dry run, and what was refused.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShopStatusResponse"
                },
                "examples": {
                  "written": {
                    "value": {
                      "written": 1,
                      "records": [
                        {
                          "index": 0,
                          "source": "etsy",
                          "shop": "exampleprints",
                          "grace_until": "2026-10-23T00:00:00.000Z"
                        }
                      ],
                      "rejected": []
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not { \"records\": [...] }, is empty, or holds more than 500 records, or dry_run is a value Warden does not read. Code invalid_request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed, unknown or revoked. Code invalid_key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key was not granted shop_status. Code scope_not_granted, with scope naming it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "The key is over its allowance of 60 requests a minute. Code rate_limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait."
              }
            }
          },
          "500": {
            "description": "Something went wrong on Warden's side. Code internal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Warden cannot do this right now. Try again later. Code unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "integrationKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "wik_ and 64 hexadecimal characters",
        "description": "An integration key, made by a signed-in admin. One organization and one provider."
      }
    },
    "schemas": {
      "SyncRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "records"
        ],
        "properties": {
          "records": {
            "type": "array",
            "minItems": 1,
            "maxItems": 500,
            "items": {
              "$ref": "#/components/schemas/Record"
            }
          }
        }
      },
      "Record": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "external_id",
          "status",
          "normalized_state"
        ],
        "properties": {
          "external_id": {
            "type": "string",
            "maxLength": 200,
            "description": "Your stable id for the member. Never changes and is never reused."
          },
          "status": {
            "type": "string",
            "maxLength": 200,
            "description": "Your own wording, for example active, lapsed, revoked or withdrawn. Stored, never interpreted, and never shown to anybody outside your organization."
          },
          "normalized_state": {
            "type": "string",
            "enum": [
              "active",
              "inactive",
              "unknown"
            ],
            "description": "You decide it. Warden never works it out from status."
          },
          "tier": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200,
            "description": "Your own label. Stored and not interpreted."
          },
          "capabilities": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "physical_sales"
              ]
            },
            "description": "What the membership grants, from a closed list, whether or not it is in force."
          },
          "effective_from": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "ISO 8601. Stored as an instant and answered in UTC."
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "ISO 8601. Stored as an instant and answered in UTC."
          },
          "credential": {
            "$ref": "#/components/schemas/Credential"
          },
          "identities": {
            "type": "array",
            "maxItems": 50,
            "items": {
              "$ref": "#/components/schemas/Identity"
            },
            "description": "The full set your system vouches for. An empty list withdraws every identity you supplied. Omit it to leave identities as they are. A bad identity is refused alone and the record is still written. Published only while the record reads active."
          }
        }
      },
      "Credential": {
        "type": [
          "object",
          "null"
        ],
        "additionalProperties": false,
        "required": [
          "holder_name"
        ],
        "description": "Creates the public verification page. Leaving it out of a later sync changes nothing; send retired true to withdraw it.",
        "properties": {
          "title": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 80,
            "description": "\"Commercial license\" when left out."
          },
          "holder_name": {
            "type": "string",
            "maxLength": 200,
            "description": "Printed publicly. A value that is entirely an email address is refused."
          },
          "registration_id": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 64,
            "description": "Your own printed reference. Displayed, never looked up."
          },
          "registered_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "ISO 8601. Stored as an instant and answered in UTC."
          },
          "publish_terms": {
            "type": "boolean",
            "default": false,
            "description": "Accepted and stored. The public page draws nothing from it."
          },
          "retired": {
            "type": "boolean",
            "default": false,
            "description": "Withdraws the credential itself. The page keeps resolving."
          }
        }
      },
      "Identity": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "source"
        ],
        "description": "Where the member sells. Needs a handle or a url.",
        "properties": {
          "source": {
            "type": "string",
            "maxLength": 80,
            "description": "The platform. Warden lowercases it."
          },
          "handle": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200,
            "description": "Read from url when source is etsy or ebay and the url is a shop's own page."
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 600,
            "description": "http or https."
          },
          "public": {
            "type": "boolean",
            "default": false,
            "description": "Whether the verification page lists it."
          },
          "assurance": {
            "type": "string",
            "enum": [
              "registered",
              "issuer_hosted"
            ],
            "default": "registered",
            "description": "issuer_hosted only for a storefront the creator's own platform hosts and controls."
          }
        }
      },
      "Entitlement": {
        "type": "object",
        "required": [
          "external_id",
          "public_id",
          "normalized_state",
          "capabilities",
          "last_synced_at",
          "retired",
          "verify_url"
        ],
        "properties": {
          "external_id": {
            "type": "string"
          },
          "public_id": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[A-Za-z0-9_-]{22}$",
            "description": "Minted once for a record with a credential. Never changes while Warden holds the record."
          },
          "normalized_state": {
            "type": "string",
            "enum": [
              "active",
              "inactive",
              "unknown"
            ]
          },
          "capabilities": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "last_synced_at": {
            "type": "string",
            "format": "date-time",
            "description": "When Warden last heard about this record. ISO 8601 in UTC with an offset, such as 2026-09-21T18:04:11.123456+00:00."
          },
          "retired": {
            "type": "boolean"
          },
          "verify_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          }
        }
      },
      "Rejection": {
        "type": "object",
        "required": [
          "index",
          "external_id",
          "refused",
          "because"
        ],
        "properties": {
          "index": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The record's position in the request."
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "refused": {
            "type": "string",
            "enum": [
              "record",
              "identity",
              "identities"
            ],
            "description": "record: nothing of the record was stored. identity: the record was written and only the identity at `identity` was not. identities: the record was written and its identities field could not be read, so stored identities were left as they were."
          },
          "identity": {
            "type": "integer",
            "description": "The identity's position in the record's identities. Present when refused is identity."
          },
          "because": {
            "type": "string",
            "description": "A sentence for a person. Do not branch on it."
          }
        }
      },
      "DryRunRecord": {
        "type": "object",
        "required": [
          "external_id",
          "would",
          "normalized_state",
          "capabilities",
          "credential",
          "identities"
        ],
        "properties": {
          "external_id": {
            "type": "string"
          },
          "would": {
            "type": "string",
            "enum": [
              "create",
              "update"
            ]
          },
          "normalized_state": {
            "type": "string",
            "enum": [
              "active",
              "inactive",
              "unknown"
            ]
          },
          "capabilities": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "credential": {
            "type": "boolean",
            "description": "Whether the record carried a credential."
          },
          "identities": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "object",
              "required": [
                "position",
                "source",
                "handle",
                "assurance",
                "public",
                "identity"
              ],
              "properties": {
                "position": {
                  "type": "integer",
                  "description": "The identity's position in the record you sent."
                },
                "source": {
                  "type": "string"
                },
                "handle": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "assurance": {
                  "type": "string",
                  "enum": [
                    "registered",
                    "issuer_hosted"
                  ]
                },
                "public": {
                  "type": "boolean"
                },
                "identity": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "description": "What Warden will compare this storefront on, or null.",
                  "required": [
                    "kind",
                    "key"
                  ],
                  "properties": {
                    "kind": {
                      "type": "string",
                      "enum": [
                        "etsy",
                        "ebay",
                        "instagram",
                        "tiktok",
                        "vinted",
                        "domain"
                      ]
                    },
                    "key": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Each identity as Warden read it. Null when the record had no identities key, or when identities could not be read."
          }
        }
      },
      "SyncResponse": {
        "type": "object",
        "required": [
          "records",
          "rejected"
        ],
        "properties": {
          "records": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Entitlement"
            }
          },
          "rejected": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Rejection"
            }
          }
        }
      },
      "DryRunResponse": {
        "type": "object",
        "required": [
          "dry_run",
          "records",
          "rejected"
        ],
        "properties": {
          "dry_run": {
            "type": "boolean",
            "const": true
          },
          "records": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DryRunRecord"
            }
          },
          "rejected": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Rejection"
            },
            "description": "What Warden can refuse before it writes. A write can refuse more, such as a new record the organization's plan does not include."
          }
        }
      },
      "ListResponse": {
        "type": "object",
        "required": [
          "records",
          "next"
        ],
        "properties": {
          "records": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Entitlement"
            }
          },
          "next": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass back as cursor. Null on the last page."
          }
        }
      },
      "EnrollmentRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "external_id"
        ],
        "properties": {
          "external_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "days": {
            "type": "integer",
            "minimum": 1,
            "maximum": 90,
            "default": 30
          }
        }
      },
      "Enrollment": {
        "type": "object",
        "required": [
          "external_id",
          "token",
          "expires_at",
          "enroll_url"
        ],
        "properties": {
          "external_id": {
            "type": "string"
          },
          "token": {
            "type": "string",
            "pattern": "^wen_[0-9a-f]{64}$"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 in UTC with an offset, such as 2026-10-21T18:04:11.123456+00:00."
          },
          "enroll_url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error",
          "code"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "A sentence for a person. It can be reworded at any time."
          },
          "code": {
            "type": "string",
            "description": "Stable. New codes may be added, so treat one you do not know by its HTTP status.",
            "enum": [
              "unauthenticated",
              "invalid_key",
              "forbidden",
              "organization_required",
              "invalid_request",
              "not_found",
              "refused",
              "rate_limited",
              "unavailable",
              "internal",
              "plan_not_allowed",
              "scope_not_granted"
            ]
          },
          "scope": {
            "type": "string",
            "description": "The scope the key was not granted. Present with scope_not_granted."
          },
          "capability": {
            "type": "string",
            "description": "What the organization's plan refused. Present with plan_not_allowed."
          }
        }
      },
      "EraseResponse": {
        "type": "object",
        "required": [
          "external_id",
          "found",
          "erased",
          "dry_run",
          "credential",
          "identities"
        ],
        "properties": {
          "external_id": {
            "type": "string"
          },
          "found": {
            "type": "boolean",
            "description": "Whether this key's provider held the record. False when it was already erased or never written."
          },
          "erased": {
            "type": "boolean",
            "description": "Whether it was erased by this request. False on a dry run and when nothing was found."
          },
          "dry_run": {
            "type": "boolean"
          },
          "credential": {
            "type": "boolean",
            "description": "Whether the record had a public credential. Once erased, its address reads as no longer published."
          },
          "identities": {
            "type": "integer",
            "description": "How many identities were on the record, whoever added them."
          }
        }
      },
      "ShopStatusRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "records"
        ],
        "properties": {
          "records": {
            "type": "array",
            "minItems": 1,
            "maxItems": 500,
            "items": {
              "$ref": "#/components/schemas/ShopStatus"
            }
          }
        }
      },
      "ShopStatus": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "url",
          "grace_until"
        ],
        "properties": {
          "url": {
            "type": "string",
            "maxLength": 500,
            "description": "The shop's own page: https://www.etsy.com/shop/<name>, <name>.etsy.com, or an eBay /usr/ or /str/ address. A listing address names no shop and is refused."
          },
          "grace_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the shop's time to register ends. Null clears a date sent earlier."
          },
          "letter_sent_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When you wrote to the shop, if you did. Kept, and shown to nobody outside your organization."
          }
        }
      },
      "ShopStatusResponse": {
        "type": "object",
        "required": [
          "records",
          "rejected"
        ],
        "properties": {
          "written": {
            "type": "integer",
            "description": "How many shops were written. Absent on a dry run, and when every record in the batch was rejected."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Present and true on a dry run."
          },
          "records": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "index",
                "source",
                "shop",
                "grace_until"
              ],
              "properties": {
                "index": {
                  "type": "integer"
                },
                "source": {
                  "type": "string",
                  "enum": [
                    "etsy",
                    "ebay"
                  ]
                },
                "shop": {
                  "type": "string",
                  "description": "The shop name Warden read from the address, lowercased. This is the name it compares."
                },
                "grace_until": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                }
              }
            }
          },
          "rejected": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "index",
                "url",
                "because"
              ],
              "properties": {
                "index": {
                  "type": "integer"
                },
                "url": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "because": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  }
}
