{
  "openapi": "3.1.0",
  "info": {
    "title": "Campaign API",
    "version": "2026-10-10",
    "summary": "Report an outcome, read it back, invite a person, mint a status card code, and read the public records.",
    "description": "Connect your product: invite people, report outcomes, and hear by webhook which are verified, then paid after the deadline.\n\nThis is version 1, in the path. We only add to it: new endpoints, new optional fields, new fields in answers and webhooks, new reasons and codes. Ignore what you do not know. Anything that would break a client comes as version 2, beside version 1, announced here at least 30 days ahead.\n\nLimits. Writes: 120 a minute per organization. Reads: 600 a minute per organization; a load code spends one. Before an API key is read: 600 a minute per address. The campaign read and the four certificate reads that name one certificate: 120 a minute per address. The signing key, the issuer document and the revocation list: none. Every 429 carries retry_after and a Retry-After header with the same seconds.\n\nErrors. Branch on code. message is a sentence for a person and may change. details carries the facts its code names.",
    "contact": {
      "email": "hello@udao.org"
    }
  },
  "servers": [
    {
      "url": "https://campaign.udao.org"
    }
  ],
  "externalDocs": {
    "description": "The reference, the quickstart and the webhooks, in words.",
    "url": "https://campaign.udao.org/developers"
  },
  "paths": {
    "/api/v1/outcomes": {
      "post": {
        "operationId": "reportOutcome",
        "summary": "Report one outcome",
        "description": "Answers 202 for any well-formed request the API key may make. A fact about the data comes back as a reason, never as an error. Idempotency-Key is accepted as a header and must equal external_id where both are sent. They disagree, 422.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Idempotency-Key is accepted as a header and must equal external_id where both are sent. They disagree, 422.",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "campaign_id": {
                    "type": "string",
                    "maxLength": 64,
                    "format": "uuid",
                    "description": "The campaign the outcome belongs to. A campaign of another organization answers 404."
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Any address the person has verified on Campaign. Hashed on arrival and never stored."
                  },
                  "email_hash": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 64,
                    "pattern": "^[0-9a-fA-F]{64}$",
                    "description": "The same address, folded, as HMAC-SHA256 under the Email salt shown beside your API key, hex encoded. Send email or email_hash, never both."
                  },
                  "reference": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "What they did, in your own words. The same word the file you import uses."
                  },
                  "occurred_at": {
                    "type": "string",
                    "maxLength": 64,
                    "pattern": "^\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}(:\\d{2}(\\.\\d{1,6})?)?(Z|[+-]\\d{2}:\\d{2})$",
                    "description": "RFC 3339 with a zone. The only input the window rules read."
                  },
                  "external_id": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Your own id for this outcome. The dedupe key: a second send of it stores nothing and answers the first reply again."
                  },
                  "kind": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "completed",
                      "revoked",
                      null
                    ],
                    "description": "completed, the default, or revoked to revoke one. A revocation names the external_id of the outcome it revokes and answers 202 with a reason."
                  }
                },
                "required": [
                  "campaign_id",
                  "reference",
                  "occurred_at",
                  "external_id"
                ],
                "additionalProperties": false,
                "oneOf": [
                  {
                    "required": [
                      "email"
                    ],
                    "properties": {
                      "email_hash": {
                        "type": "null"
                      }
                    }
                  },
                  {
                    "required": [
                      "email_hash"
                    ],
                    "properties": {
                      "email": {
                        "type": "null"
                      }
                    }
                  }
                ]
              },
              "example": {
                "campaign_id": "8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
                "email": "ada@example.com",
                "reference": "course-solidity-101",
                "occurred_at": "2026-09-11T14:03:22Z",
                "external_id": "your-own-id-8f3a",
                "kind": "completed"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Answers 202 for any well-formed request the API key may make. A fact about the data comes back as a reason, never as an error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OutcomeWrite"
                },
                "example": {
                  "external_id": "your-own-id-8f3a",
                  "campaign_id": "8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
                  "status": "pending",
                  "reason": "awaiting_review",
                  "received_at": "2026-09-11T14:03:25Z",
                  "outcome_url": "https://campaign.udao.org/api/v1/outcomes/your-own-id-8f3a?campaign_id=8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized. not_authenticated: No API key, or one that was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "not_authenticated"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "403": {
            "description": "Forbidden. power_not_granted: The API key tried to revoke a verified or paid outcome on a real campaign. An owner allows that in Integrations.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "power_not_granted"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "power": {
                              "const": "takes_back"
                            }
                          },
                          "required": [
                            "power"
                          ],
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message",
                        "details"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "404": {
            "description": "Not Found. campaign_not_found: No campaign of yours has that id. A campaign of another organization answers the same way, and so does one outside this API key’s scope.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "campaign_not_found"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "409": {
            "description": "Conflict. campaign_not_importable: That campaign is not at a point where evidence is accepted. trial_only_credential: An API key for trial campaigns only named a campaign that is not a trial. Change the API key, not the campaign.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "campaign_not_importable",
                            "trial_only_credential"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "adapter": {
                              "type": "string",
                              "enum": [
                                "attestation",
                                "feed_skillsvalley",
                                "onchain_credential",
                                "csv_import",
                                "org_api"
                              ]
                            },
                            "state": {
                              "type": "string"
                            },
                            "campaign_id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "is_trial": {
                              "const": false
                            }
                          },
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "413": {
            "description": "Content Too Large. payload_too_large: The body is past the bound. A write runs to at most 100 KB, and details.max_bytes carries the figure.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "payload_too_large"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "max_bytes": {
                              "type": "integer"
                            }
                          },
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "415": {
            "description": "Unsupported Media Type. invalid_request: The body was not sent as application/json. The endpoint reads one content type and names it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "invalid_request"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Content. invalid_request: A field is missing, unreadable or not one this endpoint takes. The answer names it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "invalid_request"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "field": {
                              "type": "string"
                            },
                            "unknown_fields": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          },
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                },
                "example": {
                  "error": {
                    "code": "invalid_request",
                    "message": "occurred_at is an RFC 3339 instant carrying its zone, such as 2026-09-11T14:03:22Z",
                    "details": {
                      "field": "occurred_at"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests. rate_limited: Past the quota. retry_after says when, in seconds.",
            "headers": {
              "Retry-After": {
                "description": "429 with retry_after, in seconds, also in the Retry-After header.",
                "required": true,
                "schema": {
                  "type": "string",
                  "pattern": "^[0-9]+$"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "rate_limited"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "retry_after": {
                              "type": "integer"
                            },
                            "limit": {
                              "type": "integer"
                            },
                            "window_seconds": {
                              "type": "integer"
                            }
                          },
                          "required": [
                            "retry_after"
                          ],
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message",
                        "details"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error. server_error: Our fault. Send the same request again: one external_id never counts twice. not_configured: This deployment lacks a setting it needs to answer. Nothing was recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "server_error",
                            "not_configured"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          }
        },
        "x-campaign-limits": [
          {
            "per": "address",
            "counts": "every request",
            "limit": 600,
            "window_seconds": 60,
            "description": "Counted from one address before the API key is read, per instance. Past it, 429 rate_limited."
          },
          {
            "per": "organization",
            "counts": "writes",
            "limit": 120,
            "window_seconds": 60,
            "description": "Every API key of the organization counts together. Past it, 429 rate_limited with retry_after."
          }
        ]
      }
    },
    "/api/v1/outcomes/{external_id}": {
      "get": {
        "operationId": "readOutcome",
        "summary": "Read one outcome back",
        "description": "The state, the reward in human units beside base units, and the certificate where the holder’s passport shows it.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "name": "external_id",
            "in": "path",
            "required": true,
            "description": "Your own id for the outcome, as you reported it.",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          },
          {
            "name": "campaign_id",
            "in": "query",
            "required": true,
            "description": "The campaign the outcome was reported to. Without it, 422.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The state, the reward in human units beside base units, and the certificate where the holder’s passport shows it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OutcomeRead"
                },
                "example": {
                  "external_id": "your-own-id-8f3a",
                  "campaign_id": "8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
                  "status": "verified",
                  "verified_at": "2026-09-11T14:06:02Z",
                  "reward": {
                    "base_units": "25000000000000000000",
                    "amount": "25.00",
                    "token_symbol": "UDAO",
                    "decimals": 18,
                    "simulated": false
                  },
                  "paid_at": "2026-09-11T14:31:40Z",
                  "certificate": {
                    "reference": "3b7e9d24-6c1a-4f85-b2d0-9e4a7c1f5b38",
                    "url": "https://campaign.udao.org/p/ada/3b7e9d24-6c1a-4f85-b2d0-9e4a7c1f5b38"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized. not_authenticated: No API key, or one that was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "not_authenticated"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "404": {
            "description": "Not Found. campaign_not_found: No campaign of yours has that id. A campaign of another organization answers the same way, and so does one outside this API key’s scope. outcome_not_found: The read: nothing was reported to that campaign under that id.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "campaign_not_found",
                            "outcome_not_found"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "409": {
            "description": "Conflict. trial_only_credential: An API key for trial campaigns only named a campaign that is not a trial. Change the API key, not the campaign.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "trial_only_credential"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "campaign_id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "is_trial": {
                              "const": false
                            }
                          },
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Content. invalid_request: A field is missing, unreadable or not one this endpoint takes. The answer names it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "invalid_request"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "field": {
                              "type": "string"
                            }
                          },
                          "required": [
                            "field"
                          ],
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message",
                        "details"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests. rate_limited: Past the quota. retry_after says when, in seconds.",
            "headers": {
              "Retry-After": {
                "description": "429 with retry_after, in seconds, also in the Retry-After header.",
                "required": true,
                "schema": {
                  "type": "string",
                  "pattern": "^[0-9]+$"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "rate_limited"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "retry_after": {
                              "type": "integer"
                            },
                            "limit": {
                              "type": "integer"
                            },
                            "window_seconds": {
                              "type": "integer"
                            }
                          },
                          "required": [
                            "retry_after"
                          ],
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message",
                        "details"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error. server_error: Our fault. Send the same request again: one external_id never counts twice. not_configured: This deployment lacks a setting it needs to answer. Nothing was recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "server_error",
                            "not_configured"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          }
        },
        "x-campaign-limits": [
          {
            "per": "address",
            "counts": "every request",
            "limit": 600,
            "window_seconds": 60,
            "description": "Counted from one address before the API key is read, per instance. Past it, 429 rate_limited."
          },
          {
            "per": "organization",
            "counts": "reads",
            "limit": 600,
            "window_seconds": 60,
            "description": "Every API key of the organization counts together. Past it, 429 rate_limited with retry_after."
          }
        ]
      }
    },
    "/api/v1/invitations": {
      "post": {
        "operationId": "inviteOne",
        "summary": "Invite one person",
        "description": "Answers 202 with the invitation as it stands and a reason, for any request the API key may make. An address already on the list is a reason, never an error. Idempotency-Key is accepted as a header and must equal external_id where both are sent. They disagree, 422.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Idempotency-Key is accepted as a header and must equal external_id where both are sent. They disagree, 422.",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "campaign_id": {
                    "type": "string",
                    "maxLength": 64,
                    "format": "uuid",
                    "description": "The campaign whose list the person goes on. A campaign of another organization answers 404."
                  },
                  "email": {
                    "type": "string",
                    "maxLength": 254,
                    "pattern": "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$",
                    "description": "The person’s address, as your dashboard’s Invite people takes it. It is kept on the list until they join or are taken off it, and never sent back. Never a hash: we mail the address when send is true, and the join checks it against the person’s own verified address."
                  },
                  "external_id": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Your own id for this invitation, unique on the campaign. Sent again with the same address it answers duplicate; the read and the webhook name it. Never the person’s address, which we drop when they join: an id that is an address, or carries this one, answers 422."
                  },
                  "send": {
                    "type": "boolean",
                    "description": "true sends the campaign’s invitation message, the one your dashboard sends; false only puts the address on the list. No default: a message cannot be taken back."
                  }
                },
                "required": [
                  "campaign_id",
                  "email",
                  "external_id",
                  "send"
                ],
                "additionalProperties": false
              },
              "example": {
                "campaign_id": "8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
                "email": "ada@example.com",
                "external_id": "your-invitation-id-5e21",
                "send": true
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Answers 202 with the invitation as it stands and a reason, for any request the API key may make. An address already on the list is a reason, never an error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvitationWrite"
                },
                "example": {
                  "external_id": "your-invitation-id-5e21",
                  "campaign_id": "8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
                  "status": "invited",
                  "reason": "added",
                  "sent": true,
                  "blocked": null,
                  "received_at": "2026-09-11T09:12:04Z",
                  "invitation_url": "https://campaign.udao.org/api/v1/invitations/your-invitation-id-5e21?campaign_id=8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
                  "join_url": "https://campaign.udao.org/campaigns/8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74/join?via=partner"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized. not_authenticated: No API key, or one that was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "not_authenticated"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "403": {
            "description": "Forbidden. browser_request: The request carried a browser Origin header. Call this from your server: an API key in a page is an API key you have published. power_not_granted: The API key tried to send an invitation on a real campaign. An owner allows that in Integrations.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "browser_request",
                            "power_not_granted"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "power": {
                              "const": "invites"
                            }
                          },
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "404": {
            "description": "Not Found. campaign_not_found: No campaign of yours has that id. A campaign of another organization answers the same way, and so does one outside this API key’s scope.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "campaign_not_found"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "409": {
            "description": "Conflict. trial_only_credential: An API key for trial campaigns only named a campaign that is not a trial. campaign_not_open: The campaign is settled, closed or canceled, so nobody else can be invited to it. invitation_cap_reached: The list is full. details.scope is campaign for the campaign’s own cap, or organization for the 20 an organization invites before Udao approves it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "trial_only_credential",
                            "campaign_not_open",
                            "invitation_cap_reached"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "campaign_id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "is_trial": {
                              "const": false
                            },
                            "state": {
                              "type": "string"
                            },
                            "cap": {
                              "type": "integer"
                            },
                            "held": {
                              "type": "integer"
                            },
                            "scope": {
                              "type": "string",
                              "enum": [
                                "campaign",
                                "organization"
                              ]
                            }
                          },
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "413": {
            "description": "Content Too Large. payload_too_large: The body is past the bound of 100 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "payload_too_large"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "max_bytes": {
                              "type": "integer"
                            }
                          },
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "415": {
            "description": "Unsupported Media Type. invalid_request: The body was not sent as application/json.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "invalid_request"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Content. invalid_request: A field is missing, unreadable or not one this call takes, and the answer names it. An external_id already naming another address on the campaign is one of them, and so is an external_id that is an address.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "invalid_request"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "field": {
                              "type": "string"
                            },
                            "unknown_fields": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          },
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests. rate_limited: Past the quota: the same 120 writes a minute as the outcomes write. retry_after says when.",
            "headers": {
              "Retry-After": {
                "description": "429 with retry_after, in seconds, also in the Retry-After header.",
                "required": true,
                "schema": {
                  "type": "string",
                  "pattern": "^[0-9]+$"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "rate_limited"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "retry_after": {
                              "type": "integer"
                            },
                            "limit": {
                              "type": "integer"
                            },
                            "window_seconds": {
                              "type": "integer"
                            }
                          },
                          "required": [
                            "retry_after"
                          ],
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message",
                        "details"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error. server_error: Our fault. Send the same request again: one external_id never adds twice. not_configured: This deployment lacks a setting it needs to keep a list. Nothing was recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "server_error",
                            "not_configured"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          }
        },
        "x-campaign-limits": [
          {
            "per": "address",
            "counts": "every request",
            "limit": 600,
            "window_seconds": 60,
            "description": "Counted from one address before the API key is read, per instance. Past it, 429 rate_limited."
          },
          {
            "per": "organization",
            "counts": "writes",
            "limit": 120,
            "window_seconds": 60,
            "description": "Every API key of the organization counts together. Past it, 429 rate_limited with retry_after."
          }
        ]
      }
    },
    "/api/v1/invitations/{external_id}": {
      "get": {
        "operationId": "readInvitation",
        "summary": "Read one invitation back",
        "description": "The invitation as it stands, by your own id, with the campaign named.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "name": "external_id",
            "in": "path",
            "required": true,
            "description": "Your own id for the invitation, as you sent it.",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          },
          {
            "name": "campaign_id",
            "in": "query",
            "required": true,
            "description": "The campaign the invitation belongs to. Without it, 422.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The invitation as it stands, by your own id, with the campaign named.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvitationRead"
                },
                "example": {
                  "external_id": "your-invitation-id-5e21",
                  "campaign_id": "8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
                  "status": "joined",
                  "invited_at": "2026-09-11T09:12:04Z",
                  "sends": 1,
                  "last_sent_at": "2026-09-11T09:12:05Z",
                  "joined_at": "2026-09-11T13:58:40Z",
                  "simulated": true
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized. not_authenticated: No API key, or one that was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "not_authenticated"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "404": {
            "description": "Not Found. campaign_not_found: No campaign of yours has that id. A campaign of another organization answers the same way, and so does one outside this API key’s scope. invitation_not_found: No invitation of that campaign has that id. An invitation added in your dashboard with no id answers the same way, and so does an id another organization sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "campaign_not_found",
                            "invitation_not_found"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "409": {
            "description": "Conflict. trial_only_credential: An API key for trial campaigns only named a campaign that is not a trial.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "trial_only_credential"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "campaign_id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "is_trial": {
                              "const": false
                            }
                          },
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Content. invalid_request: campaign_id is missing. The read names the campaign the invitation belongs to.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "invalid_request"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "field": {
                              "type": "string"
                            }
                          },
                          "required": [
                            "field"
                          ],
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message",
                        "details"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests. rate_limited: Past the quota: 600 reads a minute per organization, shared with every read your API keys make. retry_after says when.",
            "headers": {
              "Retry-After": {
                "description": "429 with retry_after, in seconds, also in the Retry-After header.",
                "required": true,
                "schema": {
                  "type": "string",
                  "pattern": "^[0-9]+$"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "rate_limited"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "retry_after": {
                              "type": "integer"
                            },
                            "limit": {
                              "type": "integer"
                            },
                            "window_seconds": {
                              "type": "integer"
                            }
                          },
                          "required": [
                            "retry_after"
                          ],
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message",
                        "details"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error. server_error: Our fault. Send the same request again: a read changes nothing. not_configured: This deployment lacks a setting it needs to answer. Nothing was recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "server_error",
                            "not_configured"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          }
        },
        "x-campaign-limits": [
          {
            "per": "address",
            "counts": "every request",
            "limit": 600,
            "window_seconds": 60,
            "description": "Counted from one address before the API key is read, per instance. Past it, 429 rate_limited."
          },
          {
            "per": "organization",
            "counts": "reads",
            "limit": 600,
            "window_seconds": 60,
            "description": "Every API key of the organization counts together. Past it, 429 rate_limited with retry_after."
          }
        ]
      }
    },
    "/api/v1/embed-codes": {
      "post": {
        "operationId": "mintEmbedCode",
        "summary": "Mint a load code for the status card",
        "description": "One person’s own outcome, read-only. This is the one widget with a server behind it. Your server mints a single-use load code, the loader posts it into the frame, and the frame draws from that code alone. The code never appears in a URL and the frame never holds a session. It answers 200 with a code for every well-formed address, whether or not that person holds a place. The frame decides which card to draw and draws both at one height, so the call never tells you who joined. Each code is a read of your door. It spends one of your 600 reads a minute and writes one row on your Integrations page. The mint refuses a request that carries a browser Origin. The API key belongs on your server: an API key in a bundle is an API key you have published. A partner with no server cannot ship this widget; the join card and the badge need none.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "A retry mints a second code rather than replaying the first. The code is never stored, only a digest of it, so there is nothing for an Idempotency-Key to hand back. A code nobody loads is a row that expires in 10 minutes and costs you one read. Send the header if your client sends it everywhere: this endpoint accepts it and does not branch on it.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "campaign_id": {
                    "type": "string",
                    "maxLength": 320,
                    "format": "uuid",
                    "description": "The campaign the card is about. It has to be one of yours, and the code is refused in any other campaign’s frame."
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The address you know the person by. Matched the way the outcomes door matches it."
                  },
                  "email_hash": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 320,
                    "pattern": "^[0-9a-fA-F]{64}$",
                    "description": "HMAC-SHA256 of the lowercased, trimmed address under your API key’s email salt (its menu, Show the email salt), hex encoded, where you hold a hash and not an address. Send email or email_hash, never both, exactly as the outcomes door takes them."
                  },
                  "origin": {
                    "type": "string",
                    "maxLength": 320,
                    "description": "The exact origin of the page the frame will stand on: scheme and host, https and port 443, no path. It has to be on your Allowed websites list, and it is what the frame’s frame-ancestors is set to."
                  }
                },
                "required": [
                  "campaign_id",
                  "origin"
                ],
                "additionalProperties": false,
                "oneOf": [
                  {
                    "required": [
                      "email"
                    ],
                    "properties": {
                      "email_hash": {
                        "type": "null"
                      }
                    }
                  },
                  {
                    "required": [
                      "email_hash"
                    ],
                    "properties": {
                      "email": {
                        "type": "null"
                      }
                    }
                  }
                ]
              },
              "example": {
                "campaign_id": "8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
                "email": "ada@example.com",
                "origin": "https://learn.example.ch"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "It answers 200 with a code for every well-formed address, whether or not that person holds a place. The frame decides which card to draw and draws both at one height, so the call never tells you who joined. Each code is a read of your door. It spends one of your 600 reads a minute and writes one row on your Integrations page.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmbedCode"
                },
                "example": {
                  "code": "emb_load_...",
                  "expires_in": 600
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized. not_authenticated: No API key, or one that was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "not_authenticated"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "403": {
            "description": "Forbidden. browser_request: The mint refuses a request that carries a browser Origin. The API key belongs on your server: an API key in a bundle is an API key you have published. A partner with no server cannot ship this widget; the join card and the badge need none.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "browser_request"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "404": {
            "description": "Not Found. campaign_not_found: No campaign of yours has that id. A campaign of another organization answers the same way, and so does one outside this API key’s scope.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "campaign_not_found"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "409": {
            "description": "Conflict. trial_only_credential: An API key for trial campaigns only named a campaign that is not a trial. Change the API key, not the campaign.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "trial_only_credential"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "campaign_id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "is_trial": {
                              "const": false
                            }
                          },
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "413": {
            "description": "Content Too Large. payload_too_large: The body is past the bound. A write runs to at most 100 KB, and details.max_bytes carries the figure.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "payload_too_large"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "max_bytes": {
                              "type": "integer"
                            }
                          },
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "415": {
            "description": "Unsupported Media Type. invalid_request: The body was not sent as application/json. The endpoint reads one content type and names it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "invalid_request"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Content. invalid_request: A field is missing, unreadable or not one this endpoint takes. The answer names it. origin_not_allowed: The origin you name has to be on your Allowed websites list, under Organization, then Integrations. An origin that is not there is answered 422 with the code origin_not_allowed and the list itself. The fix is one press in your dashboard, not a message to us. An API key for trial campaigns only may also name http://localhost with a port of yours, which is the one development address this door takes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "invalid_request",
                            "origin_not_allowed"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "field": {
                              "type": "string"
                            },
                            "unknown_fields": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            },
                            "origin": {
                              "type": "string"
                            },
                            "allowed": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          },
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests. rate_limited: Past the quota. retry_after says when, in seconds.",
            "headers": {
              "Retry-After": {
                "description": "429 with retry_after, in seconds, also in the Retry-After header.",
                "required": true,
                "schema": {
                  "type": "string",
                  "pattern": "^[0-9]+$"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "rate_limited"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "retry_after": {
                              "type": "integer"
                            },
                            "limit": {
                              "type": "integer"
                            },
                            "window_seconds": {
                              "type": "integer"
                            }
                          },
                          "required": [
                            "retry_after"
                          ],
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message",
                        "details"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error. server_error: Our fault. Send the same request again. not_configured: This deployment lacks a setting it needs to answer. Nothing was recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "server_error",
                            "not_configured"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          }
        },
        "x-campaign-limits": [
          {
            "per": "address",
            "counts": "every request",
            "limit": 600,
            "window_seconds": 60,
            "description": "Counted from one address before the API key is read, per instance. Past it, 429 rate_limited."
          },
          {
            "per": "organization",
            "counts": "reads",
            "limit": 600,
            "window_seconds": 60,
            "description": "Every API key of the organization counts together. Past it, 429 rate_limited with retry_after."
          }
        ]
      }
    },
    "/api/public/campaigns/{id}": {
      "get": {
        "operationId": "readCampaign",
        "summary": "Read a campaign’s public facts",
        "description": "No API key. It answers a campaign’s public facts: its title, reward, places, dates and who funds it, up to 120 requests a minute from one address. These get the same 404 campaign_not_found: a campaign before it goes live or after it is closed or canceled; an id nobody holds; while Udao has not approved the organization, any reader but its signed-in members, the people it invited and the people holding a place.",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign’s id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "No API key. It answers a campaign’s public facts: its title, reward, places, dates and who funds it, up to 120 requests a minute from one address.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "No API key. It answers a campaign’s public facts: its title, reward, places, dates and who funds it, up to 120 requests a minute from one address.",
                  "properties": {
                    "campaign": {
                      "$ref": "#/components/schemas/PublicCampaign"
                    }
                  },
                  "required": [
                    "campaign"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "404": {
            "description": "Not Found. campaign_not_found: These get the same 404 campaign_not_found: a campaign before it goes live or after it is closed or canceled; an id nobody holds; while Udao has not approved the organization, any reader but its signed-in members, the people it invited and the people holding a place.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "campaign_not_found"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests. rate_limited: 429 with retry_after, in seconds, also in the Retry-After header.",
            "headers": {
              "Retry-After": {
                "description": "429 with retry_after, in seconds, also in the Retry-After header.",
                "required": true,
                "schema": {
                  "type": "string",
                  "pattern": "^[0-9]+$"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "rate_limited"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "retry_after": {
                              "type": "integer"
                            }
                          },
                          "required": [
                            "retry_after"
                          ],
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message",
                        "details"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error. server_error: Our fault. Send the same request again. not_configured: This deployment lacks a setting it needs to answer. Nothing was recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "server_error",
                            "not_configured"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          }
        },
        "x-campaign-limits": [
          {
            "per": "address",
            "counts": "every request",
            "limit": 120,
            "window_seconds": 60,
            "description": "Counted from one address, per instance, with no API key. Past it, 429 rate_limited."
          }
        ]
      }
    },
    "/api/public/credentials/{id}/verify": {
      "get": {
        "operationId": "readSignedRecord",
        "summary": "Read a certificate’s signed record",
        "description": "The signed record, as application/vc+ld+json. The certificate’s own address answers the same document to a request whose Accept names JSON. The certificate’s own address, /p/{handle}/{id}, answers the same document to a request whose Accept names JSON.",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The certificate’s id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The signed record, as application/vc+ld+json. The certificate’s own address answers the same document to a request whose Accept names JSON.",
            "content": {
              "application/vc+ld+json": {
                "schema": {
                  "type": "object",
                  "description": "The signed record, as application/vc+ld+json. The certificate’s own address answers the same document to a request whose Accept names JSON. An Open Badges 3.0 AchievementCredential with a proof set: eddsa-jcs-2022 and eddsa-rdfc-2022 over the same document.",
                  "properties": {
                    "@context": {
                      "description": "As Open Badges 3.0 defines it."
                    },
                    "awardedDate": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "credentialStatus": {
                      "description": "As Bitstring Status List v1.0 defines it."
                    },
                    "credentialSubject": {
                      "description": "As Open Badges 3.0 defines it."
                    },
                    "evidence": {
                      "description": "As Open Badges 3.0 defines it."
                    },
                    "id": {
                      "type": "string",
                      "format": "uri"
                    },
                    "issuer": {
                      "description": "As Open Badges 3.0 defines it."
                    },
                    "type": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "validFrom": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "proof": {
                      "type": "array",
                      "items": {
                        "description": "As W3C Data Integrity 1.0 defines it."
                      },
                      "description": "Two proofs; checking either one is enough."
                    }
                  },
                  "required": [
                    "@context",
                    "awardedDate",
                    "credentialStatus",
                    "credentialSubject",
                    "id",
                    "issuer",
                    "type",
                    "validFrom",
                    "proof"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "404": {
            "description": "Not Found. credential_not_found: The first 3 answer 404 credential_not_found alike for an unpublished passport; a certificate its holder hid; an organization Udao has not approved, other than its own test organization; a number that names nothing. The second also answers it for a certificate issued before exact texts were kept. The third also answers it for one that is not on the public record.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "credential_not_found"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests. rate_limited: 429 with retry_after, in seconds, also in the Retry-After header.",
            "headers": {
              "Retry-After": {
                "description": "429 with retry_after, in seconds, also in the Retry-After header.",
                "required": true,
                "schema": {
                  "type": "string",
                  "pattern": "^[0-9]+$"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "rate_limited"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "retry_after": {
                              "type": "integer"
                            }
                          },
                          "required": [
                            "retry_after"
                          ],
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message",
                        "details"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error. server_error: Our fault. Send the same request again. not_configured: This deployment lacks a setting it needs to answer. Nothing was recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "server_error",
                            "not_configured"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          }
        },
        "x-campaign-limits": [
          {
            "per": "address",
            "counts": "every request",
            "limit": 120,
            "window_seconds": 60,
            "description": "Counted from one address, per instance, with no API key. Past it, 429 rate_limited."
          }
        ]
      }
    },
    "/api/public/credentials/{id}/document": {
      "get": {
        "operationId": "readExactText",
        "summary": "Read a certificate’s exact text",
        "description": "The exact text, as written when the certificate was issued. Hash the bytes as served; never parse and re-serialize them first.",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The certificate’s id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The exact text, as written when the certificate was issued.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "description": "Version 3: a record number and a salt, the issuer’s domain as it stood, the two dates as UTC days.",
                      "properties": {
                        "achieved_on": {
                          "type": "string"
                        },
                        "evidence_type": {
                          "type": "string"
                        },
                        "issued_on": {
                          "type": "string"
                        },
                        "issuer": {
                          "type": "object",
                          "properties": {
                            "domain": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "kind": {
                              "const": "campaign"
                            },
                            "org_name": {
                              "type": "string"
                            },
                            "org_slug": {
                              "type": "string"
                            },
                            "recorded_by": {
                              "const": "Campaign"
                            }
                          },
                          "required": [
                            "domain",
                            "kind",
                            "org_name",
                            "org_slug",
                            "recorded_by"
                          ],
                          "additionalProperties": false
                        },
                        "record_number": {
                          "type": "string",
                          "pattern": "^0x[0-9a-f]{64}$"
                        },
                        "salt": {
                          "type": "string",
                          "pattern": "^0x[0-9a-f]{64}$"
                        },
                        "schema": {
                          "const": "campaign.credential"
                        },
                        "subject": {
                          "type": "string"
                        },
                        "title": {
                          "type": "string"
                        },
                        "version": {
                          "const": 3
                        },
                        "watermarked": {
                          "type": "boolean"
                        }
                      },
                      "required": [
                        "achieved_on",
                        "evidence_type",
                        "issued_on",
                        "issuer",
                        "record_number",
                        "salt",
                        "schema",
                        "subject",
                        "title",
                        "version",
                        "watermarked"
                      ],
                      "additionalProperties": false
                    },
                    {
                      "type": "object",
                      "description": "Version 2, kept by a certificate issued before version 3.",
                      "properties": {
                        "achieved_at": {
                          "type": "string"
                        },
                        "campaign_id": {
                          "type": "string"
                        },
                        "evidence_type": {
                          "type": "string"
                        },
                        "id": {
                          "type": "string"
                        },
                        "issued_at": {
                          "type": "string"
                        },
                        "issuer": {
                          "type": "object",
                          "properties": {
                            "kind": {
                              "const": "campaign"
                            },
                            "org_name": {
                              "type": "string"
                            },
                            "org_slug": {
                              "type": "string"
                            },
                            "recorded_by": {
                              "const": "Campaign"
                            }
                          },
                          "required": [
                            "kind",
                            "org_name",
                            "org_slug",
                            "recorded_by"
                          ],
                          "additionalProperties": false
                        },
                        "recipient": {
                          "type": "object",
                          "properties": {
                            "credential_id": {
                              "type": "string"
                            },
                            "identity_id": {
                              "type": "string"
                            }
                          },
                          "required": [
                            "credential_id",
                            "identity_id"
                          ],
                          "additionalProperties": false
                        },
                        "schema": {
                          "const": "campaign.credential"
                        },
                        "subject": {
                          "type": "string"
                        },
                        "title": {
                          "type": "string"
                        },
                        "version": {
                          "const": 2
                        },
                        "watermarked": {
                          "type": "boolean"
                        }
                      },
                      "required": [
                        "achieved_at",
                        "campaign_id",
                        "evidence_type",
                        "id",
                        "issued_at",
                        "issuer",
                        "recipient",
                        "schema",
                        "subject",
                        "title",
                        "version",
                        "watermarked"
                      ],
                      "additionalProperties": false
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not Found. credential_not_found: The first 3 answer 404 credential_not_found alike for an unpublished passport; a certificate its holder hid; an organization Udao has not approved, other than its own test organization; a number that names nothing. The second also answers it for a certificate issued before exact texts were kept. The third also answers it for one that is not on the public record.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "credential_not_found"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests. rate_limited: 429 with retry_after, in seconds, also in the Retry-After header.",
            "headers": {
              "Retry-After": {
                "description": "429 with retry_after, in seconds, also in the Retry-After header.",
                "required": true,
                "schema": {
                  "type": "string",
                  "pattern": "^[0-9]+$"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "rate_limited"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "retry_after": {
                              "type": "integer"
                            }
                          },
                          "required": [
                            "retry_after"
                          ],
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message",
                        "details"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error. server_error: Our fault. Send the same request again. not_configured: This deployment lacks a setting it needs to answer. Nothing was recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "server_error",
                            "not_configured"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          }
        },
        "x-campaign-limits": [
          {
            "per": "address",
            "counts": "every request",
            "limit": 120,
            "window_seconds": 60,
            "description": "Counted from one address, per instance, with no API key. Past it, 429 rate_limited."
          }
        ]
      }
    },
    "/api/public/credentials/{id}/chain": {
      "get": {
        "operationId": "readPublicRecord",
        "summary": "Read a certificate’s public record",
        "description": "Its public record, read when you ask and kept a minute, unsigned: status is matches, revoked, does_not_match, revocation_pending or unreadable, with checked_at. Only once the entry answers the certificate’s record number and Campaign’s issuer does it say more than unreadable. An entry at the certificate’s own address also carries chain_id, registry, record_number, token_id and tx_hash.",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The certificate’s id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Its public record, read when you ask and kept a minute, unsigned: status is matches, revoked, does_not_match, revocation_pending or unreadable, with checked_at. Only once the entry answers the certificate’s record number and Campaign’s issuer does it say more than unreadable. An entry at the certificate’s own address also carries chain_id, registry, record_number, token_id and tx_hash.",
            "content": {
              "application/json": {
                "schema": {
                  "description": "Its public record, read when you ask and kept a minute, unsigned: status is matches, revoked, does_not_match, revocation_pending or unreadable, with checked_at. Only once the entry answers the certificate’s record number and Campaign’s issuer does it say more than unreadable. An entry at the certificate’s own address also carries chain_id, registry, record_number, token_id and tx_hash.",
                  "oneOf": [
                    {
                      "type": "object",
                      "description": "An entry at the certificate’s own address.",
                      "properties": {
                        "status": {
                          "type": "string",
                          "enum": [
                            "matches",
                            "revoked",
                            "does_not_match",
                            "revocation_pending",
                            "unreadable"
                          ]
                        },
                        "checked_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        },
                        "chain_id": {
                          "type": "integer"
                        },
                        "registry": {
                          "type": "string",
                          "pattern": "^0x[0-9a-f]{40}$"
                        },
                        "record_number": {
                          "type": "string",
                          "pattern": "^0x[0-9a-f]{64}$"
                        },
                        "token_id": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "pattern": "^[0-9]+$"
                        },
                        "tx_hash": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "pattern": "^0x[0-9a-f]{64}$"
                        }
                      },
                      "required": [
                        "status",
                        "checked_at",
                        "chain_id",
                        "registry",
                        "record_number",
                        "token_id",
                        "tx_hash"
                      ],
                      "additionalProperties": false
                    },
                    {
                      "type": "object",
                      "description": "An entry from before record numbers: the status and the instant alone.",
                      "properties": {
                        "status": {
                          "type": "string",
                          "enum": [
                            "matches",
                            "revoked",
                            "does_not_match",
                            "revocation_pending",
                            "unreadable"
                          ]
                        },
                        "checked_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        }
                      },
                      "required": [
                        "status",
                        "checked_at"
                      ],
                      "additionalProperties": false
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not Found. credential_not_found: The first 3 answer 404 credential_not_found alike for an unpublished passport; a certificate its holder hid; an organization Udao has not approved, other than its own test organization; a number that names nothing. The second also answers it for a certificate issued before exact texts were kept. The third also answers it for one that is not on the public record.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "credential_not_found"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests. rate_limited: 429 with retry_after, in seconds, also in the Retry-After header.",
            "headers": {
              "Retry-After": {
                "description": "429 with retry_after, in seconds, also in the Retry-After header.",
                "required": true,
                "schema": {
                  "type": "string",
                  "pattern": "^[0-9]+$"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "rate_limited"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "retry_after": {
                              "type": "integer"
                            }
                          },
                          "required": [
                            "retry_after"
                          ],
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message",
                        "details"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error. server_error: Our fault. Send the same request again. not_configured: This deployment lacks a setting it needs to answer. Nothing was recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "server_error",
                            "not_configured"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          }
        },
        "x-campaign-limits": [
          {
            "per": "address",
            "counts": "every request",
            "limit": 120,
            "window_seconds": 60,
            "description": "Counted from one address, per instance, with no API key. Past it, 429 rate_limited."
          }
        ]
      }
    },
    "/api/public/credentials/signing-key": {
      "get": {
        "operationId": "readSigningKey",
        "summary": "Read the signing key",
        "description": "The public key every proof names, as a Multikey. Its controller, Campaign’s own address, answers a request whose Accept names JSON with its controller document, which lists the key.",
        "security": [],
        "responses": {
          "200": {
            "description": "The public key every proof names, as a Multikey. Its controller, Campaign’s own address, answers a request whose Accept names JSON with its controller document, which lists the key.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "The public key every proof names, as a Multikey. Its controller, Campaign’s own address, answers a request whose Accept names JSON with its controller document, which lists the key. A Multikey (W3C Controlled Identifiers 1.0).",
                  "properties": {
                    "@context": {
                      "description": "As W3C Controlled Identifiers 1.0 defines it."
                    },
                    "controller": {
                      "type": "string",
                      "format": "uri"
                    },
                    "id": {
                      "type": "string",
                      "format": "uri"
                    },
                    "publicKeyMultibase": {
                      "type": "string"
                    },
                    "type": {
                      "const": "Multikey"
                    }
                  },
                  "required": [
                    "@context",
                    "controller",
                    "id",
                    "publicKeyMultibase",
                    "type"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error. server_error: Our fault. Send the same request again. not_configured: This deployment lacks a setting it needs to answer. Nothing was recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "server_error",
                            "not_configured"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          }
        },
        "x-campaign-limits": []
      }
    },
    "/api/public/credentials/issuer": {
      "get": {
        "operationId": "readIssuer",
        "summary": "Read the issuer’s controller document",
        "description": "The issuer’s controller document. The origin’s own / answers the same document to a request whose Accept names application/json or application/ld+json; a browser gets the homepage.",
        "security": [],
        "responses": {
          "200": {
            "description": "The issuer’s controlled identifier document (W3C Controlled Identifiers 1.0): its id, the key under verificationMethod, named under assertionMethod.",
            "content": {
              "application/ld+json": {
                "schema": {
                  "type": "object",
                  "description": "The issuer’s controlled identifier document (W3C Controlled Identifiers 1.0): its id, the key under verificationMethod, named under assertionMethod.",
                  "properties": {
                    "@context": {
                      "description": "As W3C Controlled Identifiers 1.0 defines it."
                    },
                    "assertionMethod": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "format": "uri"
                      }
                    },
                    "id": {
                      "type": "string",
                      "format": "uri"
                    },
                    "verificationMethod": {
                      "type": "array",
                      "items": {
                        "description": "As W3C Controlled Identifiers 1.0 defines it."
                      }
                    }
                  },
                  "required": [
                    "@context",
                    "assertionMethod",
                    "id",
                    "verificationMethod"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error. server_error: Our fault. Send the same request again. not_configured: This deployment lacks a setting it needs to answer. Nothing was recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "server_error",
                            "not_configured"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          }
        },
        "x-campaign-limits": []
      }
    },
    "/api/public/credentials/status/{list}": {
      "get": {
        "operationId": "readRevocationList",
        "summary": "Read the revocation list",
        "description": "The revocation list, signed by Campaign: one bit for each certificate, 1 once it is revoked. A copy of a record you were given still finds its bit after its holder hides the certificate.",
        "security": [],
        "parameters": [
          {
            "name": "list",
            "in": "path",
            "required": true,
            "description": "The list’s number. There is one, 1.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The revocation list, signed by Campaign: one bit for each certificate, 1 once it is revoked. A copy of a record you were given still finds its bit after its holder hides the certificate.",
            "content": {
              "application/vc+ld+json": {
                "schema": {
                  "type": "object",
                  "description": "The revocation list, signed by Campaign: one bit for each certificate, 1 once it is revoked. A copy of a record you were given still finds its bit after its holder hides the certificate. A BitstringStatusListCredential (Bitstring Status List v1.0) signed with eddsa-rdfc-2022.",
                  "properties": {
                    "@context": {
                      "description": "As the W3C Verifiable Credentials Data Model 2.0 defines it."
                    },
                    "credentialSubject": {
                      "description": "As Bitstring Status List v1.0 defines it."
                    },
                    "id": {
                      "type": "string",
                      "format": "uri"
                    },
                    "issuer": {
                      "description": "As the W3C Verifiable Credentials Data Model 2.0 defines it."
                    },
                    "type": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "validFrom": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "validUntil": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "proof": {
                      "description": "As W3C Data Integrity 1.0 defines it."
                    }
                  },
                  "required": [
                    "@context",
                    "credentialSubject",
                    "id",
                    "issuer",
                    "type",
                    "validFrom",
                    "validUntil",
                    "proof"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "404": {
            "description": "Not Found. status_list_not_found: The list answers 404 status_list_not_found to any number but 1.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "status_list_not_found"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error. server_error: Our fault. Send the same request again. not_configured: This deployment lacks a setting it needs to answer. Nothing was recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "server_error",
                            "not_configured"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          }
        },
        "x-campaign-limits": []
      }
    },
    "/r/{record}": {
      "get": {
        "operationId": "readExactTextByRecordNumber",
        "summary": "Read the exact text by its record number",
        "description": "The address a certificate’s entry on the public record names: its version 3 exact text, byte for byte, behind the same rule as the document read.",
        "security": [],
        "parameters": [
          {
            "name": "record",
            "in": "path",
            "required": true,
            "description": "The record number as 64 hex digits, with no 0x.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-fA-F]{64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The exact text, as written when the certificate was issued.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Version 3: a record number and a salt, the issuer’s domain as it stood, the two dates as UTC days.",
                  "properties": {
                    "achieved_on": {
                      "type": "string"
                    },
                    "evidence_type": {
                      "type": "string"
                    },
                    "issued_on": {
                      "type": "string"
                    },
                    "issuer": {
                      "type": "object",
                      "properties": {
                        "domain": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "kind": {
                          "const": "campaign"
                        },
                        "org_name": {
                          "type": "string"
                        },
                        "org_slug": {
                          "type": "string"
                        },
                        "recorded_by": {
                          "const": "Campaign"
                        }
                      },
                      "required": [
                        "domain",
                        "kind",
                        "org_name",
                        "org_slug",
                        "recorded_by"
                      ],
                      "additionalProperties": false
                    },
                    "record_number": {
                      "type": "string",
                      "pattern": "^0x[0-9a-f]{64}$"
                    },
                    "salt": {
                      "type": "string",
                      "pattern": "^0x[0-9a-f]{64}$"
                    },
                    "schema": {
                      "const": "campaign.credential"
                    },
                    "subject": {
                      "type": "string"
                    },
                    "title": {
                      "type": "string"
                    },
                    "version": {
                      "const": 3
                    },
                    "watermarked": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "achieved_on",
                    "evidence_type",
                    "issued_on",
                    "issuer",
                    "record_number",
                    "salt",
                    "schema",
                    "subject",
                    "title",
                    "version",
                    "watermarked"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "404": {
            "description": "Not Found. credential_not_found: The first 3 answer 404 credential_not_found alike for an unpublished passport; a certificate its holder hid; an organization Udao has not approved, other than its own test organization; a number that names nothing. The second also answers it for a certificate issued before exact texts were kept. The third also answers it for one that is not on the public record.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "credential_not_found"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests. rate_limited: 429 with retry_after, in seconds, also in the Retry-After header.",
            "headers": {
              "Retry-After": {
                "description": "429 with retry_after, in seconds, also in the Retry-After header.",
                "required": true,
                "schema": {
                  "type": "string",
                  "pattern": "^[0-9]+$"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "rate_limited"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        },
                        "details": {
                          "type": "object",
                          "description": "details carries the facts its code names.",
                          "properties": {
                            "retry_after": {
                              "type": "integer"
                            }
                          },
                          "required": [
                            "retry_after"
                          ],
                          "additionalProperties": false
                        }
                      },
                      "required": [
                        "code",
                        "message",
                        "details"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error. server_error: Our fault. Send the same request again. not_configured: This deployment lacks a setting it needs to answer. Nothing was recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "server_error",
                            "not_configured"
                          ],
                          "description": "Branch on code."
                        },
                        "message": {
                          "type": "string",
                          "description": "message is a sentence for a person and may change."
                        }
                      },
                      "required": [
                        "code",
                        "message"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          }
        },
        "x-campaign-limits": [
          {
            "per": "address",
            "counts": "every request",
            "limit": 120,
            "window_seconds": 60,
            "description": "Counted from one address, per instance, with no API key. Past it, 429 rate_limited."
          }
        ]
      }
    }
  },
  "webhooks": {
    "outcome.verified": {
      "post": {
        "operationId": "webhookOutcomeVerified",
        "summary": "outcome.verified",
        "description": "The evidence for that outcome is verified. The read is the truth. A webhook is a nudge that saves you polling for it. Sent as Content-Type: application/json.",
        "parameters": [
          {
            "name": "Campaign-Signature",
            "in": "header",
            "required": true,
            "description": "t=<unix seconds>,v1=<hex>, and a second v1= for 24 hours after a rotation. HMAC-SHA256 over the timestamp, a dot and the raw body, under your signing secret.",
            "schema": {
              "type": "string",
              "pattern": "^t=[0-9]+(,v1=[0-9a-f]{64})+$"
            }
          },
          {
            "name": "Campaign-Event-Id",
            "in": "header",
            "required": true,
            "description": "Your dedupe key. Delivery is at least once, so keep it and act once.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Campaign-Event-Type",
            "in": "header",
            "required": true,
            "description": "The same value as type in the body.",
            "schema": {
              "const": "outcome.verified"
            }
          },
          {
            "name": "User-Agent",
            "in": "header",
            "required": true,
            "schema": {
              "const": "Campaign-Notices/1"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookOutcomeVerified"
              },
              "example": {
                "id": "5d8a2f1c-4a90-4c3e-9f21-6b0d7e4a1c55",
                "type": "outcome.verified",
                "created_at": "2026-09-11T14:06:05.000Z",
                "data": {
                  "external_id": "your-own-id-8f3a",
                  "campaign_id": "8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
                  "status": "verified",
                  "occurred_at": "2026-09-11T14:03:22Z",
                  "simulated": false,
                  "verified_at": "2026-09-11T14:06:02Z"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Accepted: Any 2xx. A redirect is not followed and counts as a failure."
          },
          "default": {
            "description": "Attempts: 6 over a day: at once, +1 min, +5 min, +30 min, +3 h, +20 h."
          }
        },
        "x-campaign-signature": {
          "header": "Campaign-Signature",
          "format": "t=<unix seconds>,v1=<hex>[,v1=<hex>]",
          "algorithm": "HMAC-SHA256",
          "signed_payload": "{t}.{raw body}",
          "encoding": "hex",
          "tolerance_seconds": 300,
          "rotation_overlap_seconds": 86400,
          "description": "HMAC-SHA256 over the timestamp, a dot, and the raw body, under the signing secret your dashboard reveals. Compare in constant time, and refuse a timestamp more than 5 minutes from your own clock."
        },
        "x-campaign-delivery": {
          "attempt_delays_seconds": [
            0,
            60,
            300,
            1800,
            10800,
            72000
          ],
          "timeout_seconds": 10,
          "per_organization_per_minute": 60,
          "accepted": "2xx",
          "redirects_followed": false,
          "response_bytes_read": 8192,
          "headers": [
            "Campaign-Signature",
            "Campaign-Event-Id",
            "Campaign-Event-Type",
            "Content-Type",
            "User-Agent"
          ],
          "description": "Attempts: 6 over a day: at once, +1 min, +5 min, +30 min, +3 h, +20 h. Accepted: Any 2xx. A redirect is not followed and counts as a failure. Timeout: 10 seconds an attempt. Ceiling: 60 webhooks a minute per organization, delayed rather than dropped. Timestamp: Refuse a t more than 5 minutes from your own clock."
        }
      }
    },
    "outcome.paid": {
      "post": {
        "operationId": "webhookOutcomePaid",
        "summary": "outcome.paid",
        "description": "The reward went to the person: paid to their address, or credited to them to collect. On a trial, simulated is true: nothing moves. The read is the truth. A webhook is a nudge that saves you polling for it. Sent as Content-Type: application/json.",
        "parameters": [
          {
            "name": "Campaign-Signature",
            "in": "header",
            "required": true,
            "description": "t=<unix seconds>,v1=<hex>, and a second v1= for 24 hours after a rotation. HMAC-SHA256 over the timestamp, a dot and the raw body, under your signing secret.",
            "schema": {
              "type": "string",
              "pattern": "^t=[0-9]+(,v1=[0-9a-f]{64})+$"
            }
          },
          {
            "name": "Campaign-Event-Id",
            "in": "header",
            "required": true,
            "description": "Your dedupe key. Delivery is at least once, so keep it and act once.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Campaign-Event-Type",
            "in": "header",
            "required": true,
            "description": "The same value as type in the body.",
            "schema": {
              "const": "outcome.paid"
            }
          },
          {
            "name": "User-Agent",
            "in": "header",
            "required": true,
            "schema": {
              "const": "Campaign-Notices/1"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookOutcomePaid"
              },
              "example": {
                "id": "5d8a2f1c-4a90-4c3e-9f21-6b0d7e4a1c55",
                "type": "outcome.paid",
                "created_at": "2026-09-11T14:06:05.000Z",
                "data": {
                  "external_id": "your-own-id-8f3a",
                  "campaign_id": "8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
                  "status": "verified",
                  "occurred_at": "2026-09-11T14:03:22Z",
                  "simulated": false,
                  "verified_at": "2026-09-11T14:06:02Z",
                  "paid_at": "2026-09-11T14:31:40Z",
                  "reward": {
                    "base_units": "25000000000000000000",
                    "amount": "25.00",
                    "token_symbol": "UDAO",
                    "decimals": 18
                  },
                  "certificate": {
                    "reference": "3b7e9d24-6c1a-4f85-b2d0-9e4a7c1f5b38",
                    "url": "https://campaign.udao.org/p/ada/3b7e9d24-6c1a-4f85-b2d0-9e4a7c1f5b38"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Accepted: Any 2xx. A redirect is not followed and counts as a failure."
          },
          "default": {
            "description": "Attempts: 6 over a day: at once, +1 min, +5 min, +30 min, +3 h, +20 h."
          }
        },
        "x-campaign-signature": {
          "header": "Campaign-Signature",
          "format": "t=<unix seconds>,v1=<hex>[,v1=<hex>]",
          "algorithm": "HMAC-SHA256",
          "signed_payload": "{t}.{raw body}",
          "encoding": "hex",
          "tolerance_seconds": 300,
          "rotation_overlap_seconds": 86400,
          "description": "HMAC-SHA256 over the timestamp, a dot, and the raw body, under the signing secret your dashboard reveals. Compare in constant time, and refuse a timestamp more than 5 minutes from your own clock."
        },
        "x-campaign-delivery": {
          "attempt_delays_seconds": [
            0,
            60,
            300,
            1800,
            10800,
            72000
          ],
          "timeout_seconds": 10,
          "per_organization_per_minute": 60,
          "accepted": "2xx",
          "redirects_followed": false,
          "response_bytes_read": 8192,
          "headers": [
            "Campaign-Signature",
            "Campaign-Event-Id",
            "Campaign-Event-Type",
            "Content-Type",
            "User-Agent"
          ],
          "description": "Attempts: 6 over a day: at once, +1 min, +5 min, +30 min, +3 h, +20 h. Accepted: Any 2xx. A redirect is not followed and counts as a failure. Timeout: 10 seconds an attempt. Ceiling: 60 webhooks a minute per organization, delayed rather than dropped. Timestamp: Refuse a t more than 5 minutes from your own clock."
        }
      }
    },
    "outcome.rejected": {
      "post": {
        "operationId": "webhookOutcomeRejected",
        "summary": "outcome.rejected",
        "description": "The outcome does not count. No note travels with it. The read is the truth. A webhook is a nudge that saves you polling for it. Sent as Content-Type: application/json.",
        "parameters": [
          {
            "name": "Campaign-Signature",
            "in": "header",
            "required": true,
            "description": "t=<unix seconds>,v1=<hex>, and a second v1= for 24 hours after a rotation. HMAC-SHA256 over the timestamp, a dot and the raw body, under your signing secret.",
            "schema": {
              "type": "string",
              "pattern": "^t=[0-9]+(,v1=[0-9a-f]{64})+$"
            }
          },
          {
            "name": "Campaign-Event-Id",
            "in": "header",
            "required": true,
            "description": "Your dedupe key. Delivery is at least once, so keep it and act once.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Campaign-Event-Type",
            "in": "header",
            "required": true,
            "description": "The same value as type in the body.",
            "schema": {
              "const": "outcome.rejected"
            }
          },
          {
            "name": "User-Agent",
            "in": "header",
            "required": true,
            "schema": {
              "const": "Campaign-Notices/1"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookOutcomeRejected"
              },
              "example": {
                "id": "5d8a2f1c-4a90-4c3e-9f21-6b0d7e4a1c55",
                "type": "outcome.rejected",
                "created_at": "2026-09-11T14:06:05.000Z",
                "data": {
                  "external_id": "your-own-id-8f3a",
                  "campaign_id": "8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
                  "status": "rejected",
                  "occurred_at": "2026-09-11T14:03:22Z",
                  "simulated": false,
                  "rejected_at": "2026-09-11T14:07:44Z"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Accepted: Any 2xx. A redirect is not followed and counts as a failure."
          },
          "default": {
            "description": "Attempts: 6 over a day: at once, +1 min, +5 min, +30 min, +3 h, +20 h."
          }
        },
        "x-campaign-signature": {
          "header": "Campaign-Signature",
          "format": "t=<unix seconds>,v1=<hex>[,v1=<hex>]",
          "algorithm": "HMAC-SHA256",
          "signed_payload": "{t}.{raw body}",
          "encoding": "hex",
          "tolerance_seconds": 300,
          "rotation_overlap_seconds": 86400,
          "description": "HMAC-SHA256 over the timestamp, a dot, and the raw body, under the signing secret your dashboard reveals. Compare in constant time, and refuse a timestamp more than 5 minutes from your own clock."
        },
        "x-campaign-delivery": {
          "attempt_delays_seconds": [
            0,
            60,
            300,
            1800,
            10800,
            72000
          ],
          "timeout_seconds": 10,
          "per_organization_per_minute": 60,
          "accepted": "2xx",
          "redirects_followed": false,
          "response_bytes_read": 8192,
          "headers": [
            "Campaign-Signature",
            "Campaign-Event-Id",
            "Campaign-Event-Type",
            "Content-Type",
            "User-Agent"
          ],
          "description": "Attempts: 6 over a day: at once, +1 min, +5 min, +30 min, +3 h, +20 h. Accepted: Any 2xx. A redirect is not followed and counts as a failure. Timeout: 10 seconds an attempt. Ceiling: 60 webhooks a minute per organization, delayed rather than dropped. Timestamp: Refuse a t more than 5 minutes from your own clock."
        }
      }
    },
    "outcome.revoked": {
      "post": {
        "operationId": "webhookOutcomeRevoked",
        "summary": "outcome.revoked",
        "description": "A verified outcome was revoked, with the cause. The read is the truth. A webhook is a nudge that saves you polling for it. Sent as Content-Type: application/json.",
        "parameters": [
          {
            "name": "Campaign-Signature",
            "in": "header",
            "required": true,
            "description": "t=<unix seconds>,v1=<hex>, and a second v1= for 24 hours after a rotation. HMAC-SHA256 over the timestamp, a dot and the raw body, under your signing secret.",
            "schema": {
              "type": "string",
              "pattern": "^t=[0-9]+(,v1=[0-9a-f]{64})+$"
            }
          },
          {
            "name": "Campaign-Event-Id",
            "in": "header",
            "required": true,
            "description": "Your dedupe key. Delivery is at least once, so keep it and act once.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Campaign-Event-Type",
            "in": "header",
            "required": true,
            "description": "The same value as type in the body.",
            "schema": {
              "const": "outcome.revoked"
            }
          },
          {
            "name": "User-Agent",
            "in": "header",
            "required": true,
            "schema": {
              "const": "Campaign-Notices/1"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookOutcomeRevoked"
              },
              "example": {
                "id": "5d8a2f1c-4a90-4c3e-9f21-6b0d7e4a1c55",
                "type": "outcome.revoked",
                "created_at": "2026-09-11T14:06:05.000Z",
                "data": {
                  "external_id": "your-own-id-8f3a",
                  "campaign_id": "8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
                  "status": "revoked",
                  "occurred_at": "2026-09-11T14:03:22Z",
                  "simulated": false,
                  "revoked_at": "2026-09-12T09:15:10Z",
                  "cause": "revoked_by_partner",
                  "paid": true,
                  "payment_stands": true
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Accepted: Any 2xx. A redirect is not followed and counts as a failure."
          },
          "default": {
            "description": "Attempts: 6 over a day: at once, +1 min, +5 min, +30 min, +3 h, +20 h."
          }
        },
        "x-campaign-signature": {
          "header": "Campaign-Signature",
          "format": "t=<unix seconds>,v1=<hex>[,v1=<hex>]",
          "algorithm": "HMAC-SHA256",
          "signed_payload": "{t}.{raw body}",
          "encoding": "hex",
          "tolerance_seconds": 300,
          "rotation_overlap_seconds": 86400,
          "description": "HMAC-SHA256 over the timestamp, a dot, and the raw body, under the signing secret your dashboard reveals. Compare in constant time, and refuse a timestamp more than 5 minutes from your own clock."
        },
        "x-campaign-delivery": {
          "attempt_delays_seconds": [
            0,
            60,
            300,
            1800,
            10800,
            72000
          ],
          "timeout_seconds": 10,
          "per_organization_per_minute": 60,
          "accepted": "2xx",
          "redirects_followed": false,
          "response_bytes_read": 8192,
          "headers": [
            "Campaign-Signature",
            "Campaign-Event-Id",
            "Campaign-Event-Type",
            "Content-Type",
            "User-Agent"
          ],
          "description": "Attempts: 6 over a day: at once, +1 min, +5 min, +30 min, +3 h, +20 h. Accepted: Any 2xx. A redirect is not followed and counts as a failure. Timeout: 10 seconds an attempt. Ceiling: 60 webhooks a minute per organization, delayed rather than dropped. Timestamp: Refuse a t more than 5 minutes from your own clock."
        }
      }
    },
    "notice.test": {
      "post": {
        "operationId": "webhookNoticeTest",
        "summary": "notice.test",
        "description": "The delivery your dashboard sends to check the wiring. The read is the truth. A webhook is a nudge that saves you polling for it. Sent as Content-Type: application/json.",
        "parameters": [
          {
            "name": "Campaign-Signature",
            "in": "header",
            "required": true,
            "description": "t=<unix seconds>,v1=<hex>, and a second v1= for 24 hours after a rotation. HMAC-SHA256 over the timestamp, a dot and the raw body, under your signing secret.",
            "schema": {
              "type": "string",
              "pattern": "^t=[0-9]+(,v1=[0-9a-f]{64})+$"
            }
          },
          {
            "name": "Campaign-Event-Id",
            "in": "header",
            "required": true,
            "description": "Your dedupe key. Delivery is at least once, so keep it and act once.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Campaign-Event-Type",
            "in": "header",
            "required": true,
            "description": "The same value as type in the body.",
            "schema": {
              "const": "notice.test"
            }
          },
          {
            "name": "User-Agent",
            "in": "header",
            "required": true,
            "schema": {
              "const": "Campaign-Notices/1"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookNoticeTest"
              },
              "example": {
                "id": "5d8a2f1c-4a90-4c3e-9f21-6b0d7e4a1c55",
                "type": "notice.test",
                "created_at": "2026-09-11T14:06:05.000Z",
                "data": {
                  "campaign_id": "",
                  "status": "recorded",
                  "simulated": true
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Accepted: Any 2xx. A redirect is not followed and counts as a failure."
          },
          "default": {
            "description": "Attempts: 6 over a day: at once, +1 min, +5 min, +30 min, +3 h, +20 h."
          }
        },
        "x-campaign-signature": {
          "header": "Campaign-Signature",
          "format": "t=<unix seconds>,v1=<hex>[,v1=<hex>]",
          "algorithm": "HMAC-SHA256",
          "signed_payload": "{t}.{raw body}",
          "encoding": "hex",
          "tolerance_seconds": 300,
          "rotation_overlap_seconds": 86400,
          "description": "HMAC-SHA256 over the timestamp, a dot, and the raw body, under the signing secret your dashboard reveals. Compare in constant time, and refuse a timestamp more than 5 minutes from your own clock."
        },
        "x-campaign-delivery": {
          "attempt_delays_seconds": [
            0,
            60,
            300,
            1800,
            10800,
            72000
          ],
          "timeout_seconds": 10,
          "per_organization_per_minute": 60,
          "accepted": "2xx",
          "redirects_followed": false,
          "response_bytes_read": 8192,
          "headers": [
            "Campaign-Signature",
            "Campaign-Event-Id",
            "Campaign-Event-Type",
            "Content-Type",
            "User-Agent"
          ],
          "description": "Attempts: 6 over a day: at once, +1 min, +5 min, +30 min, +3 h, +20 h. Accepted: Any 2xx. A redirect is not followed and counts as a failure. Timeout: 10 seconds an attempt. Ceiling: 60 webhooks a minute per organization, delayed rather than dropped. Timestamp: Refuse a t more than 5 minutes from your own clock."
        }
      }
    },
    "invitation.joined": {
      "post": {
        "operationId": "webhookInvitationJoined",
        "summary": "invitation.joined",
        "description": "Somebody holding an address your app invited took a place. Its external_id is the invitation’s. The read is the truth. A webhook is a nudge that saves you polling for it. Sent as Content-Type: application/json.",
        "parameters": [
          {
            "name": "Campaign-Signature",
            "in": "header",
            "required": true,
            "description": "t=<unix seconds>,v1=<hex>, and a second v1= for 24 hours after a rotation. HMAC-SHA256 over the timestamp, a dot and the raw body, under your signing secret.",
            "schema": {
              "type": "string",
              "pattern": "^t=[0-9]+(,v1=[0-9a-f]{64})+$"
            }
          },
          {
            "name": "Campaign-Event-Id",
            "in": "header",
            "required": true,
            "description": "Your dedupe key. Delivery is at least once, so keep it and act once.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Campaign-Event-Type",
            "in": "header",
            "required": true,
            "description": "The same value as type in the body.",
            "schema": {
              "const": "invitation.joined"
            }
          },
          {
            "name": "User-Agent",
            "in": "header",
            "required": true,
            "schema": {
              "const": "Campaign-Notices/1"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookInvitationJoined"
              },
              "example": {
                "id": "5d8a2f1c-4a90-4c3e-9f21-6b0d7e4a1c55",
                "type": "invitation.joined",
                "created_at": "2026-09-11T14:06:05.000Z",
                "data": {
                  "external_id": "your-invitation-id-5e21",
                  "campaign_id": "8f2c6a1e-4b7d-4e39-a5c0-3d9e1f6b2a74",
                  "status": "joined",
                  "joined_at": "2026-09-11T13:58:40Z",
                  "simulated": true
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Accepted: Any 2xx. A redirect is not followed and counts as a failure."
          },
          "default": {
            "description": "Attempts: 6 over a day: at once, +1 min, +5 min, +30 min, +3 h, +20 h."
          }
        },
        "x-campaign-signature": {
          "header": "Campaign-Signature",
          "format": "t=<unix seconds>,v1=<hex>[,v1=<hex>]",
          "algorithm": "HMAC-SHA256",
          "signed_payload": "{t}.{raw body}",
          "encoding": "hex",
          "tolerance_seconds": 300,
          "rotation_overlap_seconds": 86400,
          "description": "HMAC-SHA256 over the timestamp, a dot, and the raw body, under the signing secret your dashboard reveals. Compare in constant time, and refuse a timestamp more than 5 minutes from your own clock."
        },
        "x-campaign-delivery": {
          "attempt_delays_seconds": [
            0,
            60,
            300,
            1800,
            10800,
            72000
          ],
          "timeout_seconds": 10,
          "per_organization_per_minute": 60,
          "accepted": "2xx",
          "redirects_followed": false,
          "response_bytes_read": 8192,
          "headers": [
            "Campaign-Signature",
            "Campaign-Event-Id",
            "Campaign-Event-Type",
            "Content-Type",
            "User-Agent"
          ],
          "description": "Attempts: 6 over a day: at once, +1 min, +5 min, +30 min, +3 h, +20 h. Accepted: Any 2xx. A redirect is not followed and counts as a failure. Timeout: 10 seconds an attempt. Ceiling: 60 webhooks a minute per organization, delayed rather than dropped. Timestamp: Refuse a t more than 5 minutes from your own clock."
        }
      }
    }
  },
  "components": {
    "schemas": {
      "OutcomeWrite": {
        "type": "object",
        "description": "Answers 202 for any well-formed request the API key may make. A fact about the data comes back as a reason, never as an error.",
        "properties": {
          "external_id": {
            "type": "string"
          },
          "campaign_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "recorded",
              "pending",
              "verified",
              "rejected",
              "revoked"
            ],
            "description": "recorded: The outcome is kept and nothing has been decided about it. pending: Evidence is written and a verifier has it. verified: A verifier approved it. The reward is committed to that place. rejected: The outcome does not count. A verifier decided so. revoked: A verified outcome was revoked."
          },
          "reason": {
            "type": "string",
            "enum": [
              "awaiting_review",
              "participant_not_joined",
              "completed_before_join",
              "outside_window",
              "duplicate",
              "decided_by_reviewer",
              "reference_not_counted",
              "auto_verified",
              "withdrew_pending",
              "revoked_for_cause",
              "payment_stands",
              "unknown_outcome"
            ],
            "description": "awaiting_review (pending): Recorded and waiting for a verifier. participant_not_joined (recorded): Nobody holding a place has verified that address. completed_before_join (recorded): The outcome is dated before the place was taken, so the campaign did not cause it. outside_window (recorded): The campaign stopped accepting evidence. duplicate (the status the first write answered): That external_id was written before. The event is stored once, and this is the first answer again, whatever has changed since. decided_by_reviewer (the place’s own state): A person already decided the outcome for this place. reference_not_counted (recorded): This campaign names the references it counts and reference was not one of them. The write is kept as a record and no evidence is written. auto_verified (verified): The campaign has auto-verify on, so the outcome verified on its own. Whoever turned it on is named on the record. withdrew_pending (rejected): kind revoked, on an outcome nobody had decided yet. It will not be verified. revoked_for_cause (revoked): kind revoked, from an API key allowed to revoke, on a verified outcome. It is verified no longer. payment_stands (revoked): kind revoked, from an API key allowed to revoke, on a paid outcome. Nothing is clawed back and the certificate takes its revoked state. unknown_outcome (recorded): kind revoked, naming an external_id this campaign never received."
          },
          "received_at": {
            "type": "string",
            "format": "date-time"
          },
          "outcome_url": {
            "type": "string",
            "format": "uri",
            "description": "The read of this outcome, with campaign_id in place."
          },
          "details": {
            "type": "object",
            "description": "The fact behind the reason, where there is one.",
            "properties": {
              "join_url": {
                "type": "string",
                "format": "uri",
                "description": "participant_not_joined: Send them the join URL in details. This id is spent: sent again it answers the first reply again. It is matched once someone who has verified that address holds a place, inside the campaign’s window."
              },
              "grace_ends_at": {
                "type": "string",
                "format": "date-time",
                "description": "outside_window: Read grace_ends_at in details. The outcome is recorded rather than refused, because a late outcome can still be attested by hand."
              },
              "paid": {
                "type": "boolean",
                "const": true,
                "description": "payment_stands: Nothing. The webhook and the read both say paid and payment_stands, so your own product can say the same."
              },
              "payment_stands": {
                "type": "boolean",
                "const": true,
                "description": "kind revoked, from an API key allowed to revoke, on a paid outcome. Nothing is clawed back and the certificate takes its revoked state."
              }
            },
            "additionalProperties": false
          }
        },
        "required": [
          "external_id",
          "campaign_id",
          "status",
          "reason",
          "received_at",
          "outcome_url"
        ],
        "additionalProperties": false
      },
      "OutcomeRead": {
        "type": "object",
        "description": "The state, the reward in human units beside base units, and the certificate where the holder’s passport shows it.",
        "properties": {
          "external_id": {
            "type": "string"
          },
          "campaign_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "recorded",
              "pending",
              "verified",
              "rejected",
              "revoked"
            ],
            "description": "The place’s own state, not the record this report made."
          },
          "verified_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "reward": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "base_units": {
                "type": "string",
                "pattern": "^[0-9]+$",
                "description": "The figure in the token’s own units, as a decimal string."
              },
              "amount": {
                "type": "string",
                "description": "The same figure in human units, as a decimal string."
              },
              "token_symbol": {
                "type": "string"
              },
              "decimals": {
                "type": "integer"
              },
              "simulated": {
                "type": "boolean",
                "description": "True on a trial campaign. Read it before you print a figure."
              }
            },
            "required": [
              "base_units",
              "amount",
              "token_symbol",
              "decimals",
              "simulated"
            ],
            "additionalProperties": false
          },
          "paid_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "certificate": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "reference": {
                "type": "string",
                "format": "uuid",
                "description": "The certificate’s id."
              },
              "url": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uri",
                "description": "Null with a reason where the holder keeps their passport unlisted."
              },
              "reason": {
                "type": "string",
                "description": "Why url is null, in words."
              }
            },
            "required": [
              "reference",
              "url"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "external_id",
          "campaign_id",
          "status",
          "verified_at",
          "reward",
          "paid_at",
          "certificate"
        ],
        "additionalProperties": false
      },
      "InvitationWrite": {
        "type": "object",
        "description": "Answers 202 with the invitation as it stands and a reason, for any request the API key may make. An address already on the list is a reason, never an error.",
        "properties": {
          "external_id": {
            "type": "string",
            "description": "Your own id for the invitation, as you sent it."
          },
          "campaign_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "invited",
              "joined",
              "removed"
            ],
            "description": "invited: On the list. Nobody holding the address has taken a place yet. joined: Somebody holding the invited address took a place, however they joined: from your invitation, the join link or the campaign’s page. joined_at on the read says when. removed: Taken off the list, by your organization or by the person."
          },
          "reason": {
            "type": "string",
            "enum": [
              "added",
              "already_listed",
              "duplicate",
              "removed_from_list"
            ],
            "description": "added: The address is on the list now. sent says whether the message went, and blocked names why not where you asked for it. already_listed: The address was on the list already. If it had no id, it takes yours; if it had another, details.external_id names it. Nothing was sent. duplicate: This id was sent before with this address. The answer is the invitation as it stands, and nothing was written or sent. A retry sent while the first call is still running can be answered before that call settles. removed_from_list: The address was taken off the list, by your organization or by the person from the message. The API never adds it back."
          },
          "sent": {
            "type": "boolean",
            "description": "True when this call sent the invitation message."
          },
          "blocked": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "send_not_available",
              "domain_proof_required",
              "not_configured",
              "send_refused",
              "send_cap_reached",
              "send_count_unreadable",
              "recipient_budget_reached",
              "resend_too_soon",
              "resend_exhausted",
              "invitation_bounced",
              null
            ],
            "description": "Why a message you asked for did not go, as a code below, or null."
          },
          "received_at": {
            "type": "string",
            "format": "date-time"
          },
          "invitation_url": {
            "type": "string",
            "format": "uri",
            "description": "The read of this invitation, with campaign_id in place: under details.external_id where the answer carries one, else under your own id."
          },
          "join_url": {
            "type": "string",
            "format": "uri",
            "description": "The campaign’s join link with your app’s mark, for a message of your own."
          },
          "details": {
            "type": "object",
            "description": "On already_listed only: the id the address is already listed under.",
            "properties": {
              "external_id": {
                "type": "string"
              }
            },
            "required": [
              "external_id"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "external_id",
          "campaign_id",
          "status",
          "reason",
          "sent",
          "blocked",
          "received_at",
          "invitation_url",
          "join_url"
        ],
        "additionalProperties": false
      },
      "InvitationRead": {
        "type": "object",
        "description": "The invitation as it stands, by your own id, with the campaign named.",
        "properties": {
          "external_id": {
            "type": "string"
          },
          "campaign_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "invited",
              "joined",
              "removed"
            ],
            "description": "invited: On the list. Nobody holding the address has taken a place yet. joined: Somebody holding the invited address took a place, however they joined: from your invitation, the join link or the campaign’s page. joined_at on the read says when. removed: Taken off the list, by your organization or by the person."
          },
          "invited_at": {
            "type": "string",
            "format": "date-time"
          },
          "sends": {
            "type": "integer",
            "description": "How many invitation messages went to the address, at most 3."
          },
          "last_sent_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the last message went, or null."
          },
          "joined_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the person took their place, or null before that."
          },
          "simulated": {
            "type": "boolean",
            "description": "True on a trial campaign."
          }
        },
        "required": [
          "external_id",
          "campaign_id",
          "status",
          "invited_at",
          "sends",
          "last_sent_at",
          "joined_at",
          "simulated"
        ],
        "additionalProperties": false
      },
      "EmbedCode": {
        "type": "object",
        "description": "It answers 200 with a code for every well-formed address, whether or not that person holds a place. The frame decides which card to draw and draws both at one height, so the call never tells you who joined. Each code is a read of your door. It spends one of your 600 reads a minute and writes one row on your Integrations page.",
        "properties": {
          "code": {
            "type": "string",
            "description": "A single-use load code. It lives ten minutes and is spent on first use."
          },
          "expires_in": {
            "type": "integer",
            "description": "Seconds until the code expires."
          }
        },
        "required": [
          "code",
          "expires_in"
        ],
        "additionalProperties": false
      },
      "PublicCampaign": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "summary": {
            "type": [
              "string",
              "null"
            ]
          },
          "description_md": {
            "type": [
              "string",
              "null"
            ]
          },
          "outcome_type": {
            "type": "string",
            "enum": [
              "course_completion",
              "certification",
              "attested",
              "external_credential",
              "hire_retained"
            ]
          },
          "adapter": {
            "type": "string",
            "enum": [
              "attestation",
              "feed_skillsvalley",
              "onchain_credential",
              "csv_import",
              "org_api"
            ]
          },
          "reward": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "base_units": {
                "type": "string",
                "pattern": "^[0-9]+$",
                "description": "The figure in the token’s own units, as a decimal string."
              },
              "amount": {
                "type": "string",
                "description": "The same figure in human units, as a decimal string."
              },
              "token_symbol": {
                "type": "string"
              },
              "decimals": {
                "type": "integer"
              }
            },
            "required": [
              "base_units",
              "amount",
              "token_symbol",
              "decimals"
            ],
            "additionalProperties": false
          },
          "recognition": {
            "type": "boolean",
            "description": "True on a certificate campaign: no reward, a certificate for each approved outcome."
          },
          "seats": {
            "type": "object",
            "description": "The places on the campaign.",
            "properties": {
              "total": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "taken": {
                "type": "integer"
              },
              "remaining": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            },
            "required": [
              "total",
              "taken",
              "remaining"
            ],
            "additionalProperties": false
          },
          "starts_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "ends_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "state": {
            "type": "string",
            "enum": [
              "live",
              "verifying",
              "settling",
              "settled"
            ]
          },
          "is_trial": {
            "type": "boolean"
          },
          "visibility": {
            "type": "string",
            "enum": [
              "unlisted",
              "public"
            ]
          },
          "join_policy": {
            "type": "string",
            "enum": [
              "open",
              "invited"
            ]
          },
          "evidence_accepted_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "faq_md": {
            "type": [
              "string",
              "null"
            ]
          },
          "contact_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "funder": {
            "type": "object",
            "description": "The organization that funds the campaign.",
            "properties": {
              "slug": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "website": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "logo_url": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "verification": {
                "type": "string",
                "enum": [
                  "domain_verified",
                  "approved",
                  "in_review"
                ]
              },
              "verified_domain": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "slug",
              "name",
              "website",
              "logo_url",
              "verification",
              "verified_domain"
            ],
            "additionalProperties": false
          },
          "join_open": {
            "type": "boolean"
          },
          "join_closed_reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "not_started",
              "ended",
              "full",
              "in_review",
              "finished",
              "not_open",
              null
            ]
          },
          "requires_linked_email": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "title",
          "summary",
          "description_md",
          "outcome_type",
          "adapter",
          "reward",
          "recognition",
          "seats",
          "starts_at",
          "ends_at",
          "state",
          "is_trial",
          "visibility",
          "join_policy",
          "evidence_accepted_until",
          "faq_md",
          "contact_url",
          "funder",
          "join_open",
          "join_closed_reason",
          "requires_linked_email",
          "created_at"
        ],
        "additionalProperties": false
      },
      "WebhookOutcomeVerified": {
        "type": "object",
        "description": "The evidence for that outcome is verified.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Our id for this delivery, the same as Campaign-Event-Id."
          },
          "type": {
            "const": "outcome.verified"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "properties": {
              "external_id": {
                "type": "string"
              },
              "campaign_id": {
                "type": "string",
                "format": "uuid"
              },
              "status": {
                "type": "string",
                "enum": [
                  "recorded",
                  "pending",
                  "verified",
                  "rejected",
                  "revoked"
                ]
              },
              "occurred_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "verified_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "simulated": {
                "type": "boolean"
              }
            },
            "required": [
              "external_id",
              "campaign_id",
              "status",
              "occurred_at",
              "verified_at",
              "simulated"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "id",
          "type",
          "created_at",
          "data"
        ],
        "additionalProperties": false
      },
      "WebhookOutcomePaid": {
        "type": "object",
        "description": "The reward went to the person: paid to their address, or credited to them to collect. On a trial, simulated is true: nothing moves.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Our id for this delivery, the same as Campaign-Event-Id."
          },
          "type": {
            "const": "outcome.paid"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "properties": {
              "external_id": {
                "type": "string"
              },
              "campaign_id": {
                "type": "string",
                "format": "uuid"
              },
              "status": {
                "type": "string",
                "enum": [
                  "recorded",
                  "pending",
                  "verified",
                  "rejected",
                  "revoked"
                ]
              },
              "occurred_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "verified_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "paid_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "reward": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "base_units": {
                    "type": "string",
                    "pattern": "^[0-9]+$",
                    "description": "The figure in the token’s own units, as a decimal string."
                  },
                  "amount": {
                    "type": "string",
                    "description": "The same figure in human units, as a decimal string."
                  },
                  "token_symbol": {
                    "type": "string"
                  },
                  "decimals": {
                    "type": "integer"
                  }
                },
                "required": [
                  "base_units",
                  "amount",
                  "token_symbol",
                  "decimals"
                ],
                "additionalProperties": false
              },
              "certificate": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "reference": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The certificate’s id."
                  },
                  "url": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uri"
                  },
                  "reason": {
                    "type": "string",
                    "description": "Why url is null, in words."
                  }
                },
                "required": [
                  "reference",
                  "url"
                ],
                "additionalProperties": false
              },
              "simulated": {
                "type": "boolean"
              }
            },
            "required": [
              "external_id",
              "campaign_id",
              "status",
              "occurred_at",
              "verified_at",
              "paid_at",
              "reward",
              "certificate",
              "simulated"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "id",
          "type",
          "created_at",
          "data"
        ],
        "additionalProperties": false
      },
      "WebhookOutcomeRejected": {
        "type": "object",
        "description": "The outcome does not count. No note travels with it.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Our id for this delivery, the same as Campaign-Event-Id."
          },
          "type": {
            "const": "outcome.rejected"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "properties": {
              "external_id": {
                "type": "string"
              },
              "campaign_id": {
                "type": "string",
                "format": "uuid"
              },
              "status": {
                "type": "string",
                "enum": [
                  "recorded",
                  "pending",
                  "verified",
                  "rejected",
                  "revoked"
                ]
              },
              "occurred_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "rejected_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "simulated": {
                "type": "boolean"
              }
            },
            "required": [
              "external_id",
              "campaign_id",
              "status",
              "occurred_at",
              "rejected_at",
              "simulated"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "id",
          "type",
          "created_at",
          "data"
        ],
        "additionalProperties": false
      },
      "WebhookOutcomeRevoked": {
        "type": "object",
        "description": "A verified outcome was revoked, with the cause.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Our id for this delivery, the same as Campaign-Event-Id."
          },
          "type": {
            "const": "outcome.revoked"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "properties": {
              "external_id": {
                "type": "string"
              },
              "campaign_id": {
                "type": "string",
                "format": "uuid"
              },
              "status": {
                "type": "string",
                "enum": [
                  "recorded",
                  "pending",
                  "verified",
                  "rejected",
                  "revoked"
                ]
              },
              "occurred_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "revoked_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "cause": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "revoked_by_reviewer",
                  "revoked_by_partner",
                  null
                ]
              },
              "paid": {
                "type": "boolean",
                "const": true
              },
              "payment_stands": {
                "type": "boolean",
                "const": true
              },
              "simulated": {
                "type": "boolean"
              }
            },
            "required": [
              "external_id",
              "campaign_id",
              "status",
              "occurred_at",
              "revoked_at",
              "cause",
              "simulated"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "id",
          "type",
          "created_at",
          "data"
        ],
        "additionalProperties": false
      },
      "WebhookNoticeTest": {
        "type": "object",
        "description": "The delivery your dashboard sends to check the wiring.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Our id for this delivery, the same as Campaign-Event-Id."
          },
          "type": {
            "const": "notice.test"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "properties": {
              "campaign_id": {
                "const": "",
                "description": "A test names no campaign."
              },
              "status": {
                "const": "recorded"
              },
              "simulated": {
                "type": "boolean"
              }
            },
            "required": [
              "campaign_id",
              "status",
              "simulated"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "id",
          "type",
          "created_at",
          "data"
        ],
        "additionalProperties": false
      },
      "WebhookInvitationJoined": {
        "type": "object",
        "description": "Somebody holding an address your app invited took a place. Its external_id is the invitation’s.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Our id for this delivery, the same as Campaign-Event-Id."
          },
          "type": {
            "const": "invitation.joined"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "properties": {
              "external_id": {
                "type": "string",
                "description": "Your own id for the invitation, as you sent it."
              },
              "campaign_id": {
                "type": "string",
                "format": "uuid"
              },
              "status": {
                "const": "joined"
              },
              "joined_at": {
                "type": "string",
                "format": "date-time"
              },
              "simulated": {
                "type": "boolean"
              }
            },
            "required": [
              "external_id",
              "campaign_id",
              "status",
              "joined_at",
              "simulated"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "id",
          "type",
          "created_at",
          "data"
        ],
        "additionalProperties": false
      }
    },
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "camp_sk_live_ or camp_sk_trial_ then 64 hex digits",
        "description": "Every call carries one header, your API key, and nothing else. An owner or a developer creates it, and needs a sign-in from the last 10 minutes. It is shown once; keep it on your server, never in a browser. Your organization keeps at most 10 live. An API key for trial campaigns only begins camp_sk_trial_ and writes to trial campaigns alone. Trial campaigns are the test mode: the loop runs whole and every figure comes back with simulated true."
      }
    }
  }
}
