Skip to content

Checking access...

Governance API Reference ​

Version: 3.0 Date: 2026-08-25 Canister: governance Candid Interface: governance.didREST wrapper: oracle-bridge /api/governance/proposals/* (see PLATFORM-004.3 REST Surface)

bl-1404 reconciliation (2026-08-25): this page is the single canonical governance API reference. It was merged from the former governance-api.md (comprehensive guide) and governance.md (Candid quick-ref) and every method name, argument, and return type below has been arbitrated against the live governance.did. Where the two prior pages disagreed, the .did won.


Overview ​

The governance canister provides a decentralized proposal system with time-windowed voting and ICRC-7 vote-pass NFT mechanics for the Hello World DAO ecosystem. It enforces 1-member-1-vote (1M1V) — each member casts exactly one vote per proposal, backed by a per-vote Soul-Bound vote-pass NFT.

Key Features ​

  • Proposal Management: create_proposal opens a proposal with a customizable voting window (24–168 hours); update_proposal + get_proposal_versions give proposers an append-only draft-revision history (PLATFORM-004.1).
  • Vote-Pass NFT System: casting a vote mints a per-proposal ICRC-7 vote-pass NFT to the voter; all passes for a proposal are burned at finalization.
  • 1-Member-1-Vote Enforcement: each vote increments the tally by exactly 1, independent of any token holdings.
  • Four-Choice Voting: Yes, No, Abstain, and NeedsRefinement (counts toward quorum but not approval — Story FOS-4.1.1).
  • Proposal Categories with Thresholds: Constitutional, Personnel, Operational, Treasury, SoftwareDevelopment, Immutable, Tier1Constitutional — each with configurable default quorum/approval percentages.
  • Three-Gate Immutable / Tier1Constitutional Proposals: Article XIV / OA §11.2 changes require a 90% supermajority + 180-day window + CLT Board ratification.
  • CLT Board Management: set_clt_board, ratify_proposal, get_clt_board, get_ratification_status, attest_clt_ratification.
  • Execution Orchestration (ADR-034 / arch-004-3a.1): approved Treasury-payout / Personnel-status proposals dispatch an orchestrated effect and enter Executing; the off-chain governance-service reports the terminal outcome via report_execution_result, with an on-chain immutable audit trail (get_execution_intents / get_execution_results).
  • §14.5 Amended-Proposal Appeal Path (BL-450, partial build): submit_amended_proposal / mark_proposal_invalid / get_invalidation file and disposition appeals against already-tallied proposals.
  • FOS Document Publish Flow (PLATFORM-004.3): POST /api/governance/proposals/from-document and PUT /api/governance/proposals/:id/from-document on oracle-bridge wrap create_proposal / update_proposal behind a FounderyOS-facing REST surface.

Proposal Lifecycle ​

mermaid
stateDiagram-v2
    [*] --> Draft: create_proposal()
    Draft --> Active: voting window opens (voting_start_at)
    Active --> Active: cast_vote() — mints a vote-pass NFT per voter
    Active --> Approved: finalize() — majority yes + quorum
    Active --> Rejected: finalize() — majority no / no quorum
    Approved --> Executed: execute_proposal() — self-contained category
    Approved --> Executing: execute_proposal() — orchestrated (Treasury Payout / Personnel StatusChange)
    Executing --> Executed: report_execution_result(Ok)
    Executing --> Failed: report_execution_result(Err)
    Approved --> Invalid: mark_proposal_invalid() — §14.5 appeal path
    Executed --> [*]
    Rejected --> [*]
    Failed --> [*]

    note right of Active
        Each voter mints one Soul-Bound
        vote-pass NFT on cast_vote;
        all are burned at finalize
    end note

Authentication ​

Governance is a CQRS command target (ADR-014). Member-initiated writes do not arrive from the member's own principal — they are forwarded by the oracle-bridge service principal, which authenticates the member's session and passes the resolved member identity as an explicit principal argument (the voter_principal / caller parameter on create_proposal, cast_vote, update_proposal, etc.). The canister verifies:

  • oracle-bridge caller gate (AUTH-003.6) — mutating proposal/vote methods require the configured oracle-bridge principal as ic_cdk::caller().
  • membership gate — the forwarded member principal must be an active member (checked against the membership canister).
  • principals.is_authorized gate (Layer 2, defense-in-depth) — privileged surfaces (finalize, execute_proposal, report_execution_result, mark_proposal_invalid, clear_test_proposals) additionally check a permission string on the principals canister.
  • controller-only — configuration/threshold/CLT-board setters.

Voting eligibility (the 30-day post-activation cooldown) is owned by the membership canister's voting_eligibility_status (BL-414/BL-428). Do not query governance for eligibility; the governance-side shims (set_member_sbt_issuance, check_voting_activation) are deprecated no-ops kept for one upgrade cycle.


Data Types ​

ProposalCategory ​

candid
type ProposalCategory = variant {
  Constitutional;        // Bylaws, membership rules
  Personnel;             // Hiring, roles, compensation (OA Article XIII)
  Operational;           // Day-to-day management
  Treasury;              // Spending, allocations
  SoftwareDevelopment;   // Features, infrastructure
  Immutable;             // Article XIV protected provisions (90% + 180 days + CLT ratification)
  Tier1Constitutional;   // Foundational amendments (90% / 180d / CLT) (OA Section 11.2)
};

ProposalStatus ​

candid
type ProposalStatus = variant {
  Draft;     // In review period (24 hours before voting)
  Active;    // Voting period is active
  Approved;  // Passed (majority yes)
  Rejected;  // Failed (majority no or no quorum)
  Executing; // Orchestrated effect (Treasury Payout / Personnel StatusChange) in flight (ADR-034)
  Executed;  // Successfully executed
  Failed;    // Execution failed
  Invalid;   // Retroactively invalidated via the §14.5 amended-proposal path (BL-450)
};

VoteChoice ​

candid
type VoteChoice = variant {
  Yes;
  No;
  Abstain;
  NeedsRefinement;  // Counts toward quorum but NOT approval (Story FOS-4.1.1)
};

Proposal ​

candid
type Proposal = record {
  id : nat64;
  category : ProposalCategory;
  title : text;
  description : text;
  proposer : principal;
  org_id : opt nat64;                 // None = HWDAO-wide; Some(id) = Org-scoped (org-002-1)
  created_at : nat64;                 // Nanoseconds
  voting_start_at : nat64;            // Nanoseconds
  voting_end_at : nat64;              // Nanoseconds
  status : ProposalStatus;
  yes_votes : nat64;
  no_votes : nat64;
  abstain_votes : nat64;
  needs_refinement_votes : nat64;     // Count of NeedsRefinement votes (Story FOS-4.1.1)
  quorum_threshold : nat64;           // Minimum total votes required (Story 4-3)
  approval_threshold : nat8;          // Percentage of yes votes required (0-100)
  execution_data : opt blob;          // Category-specific execution data (max 100KB)
  deliberation_end_at : opt nat64;    // End of deliberation for Tier1Constitutional (BL-086)
  clt_ratified : opt bool;            // CLT Board ratification flag (BL-086)
  version_number : opt nat32;         // Current content version number (PLATFORM-004.1)
  versions : opt vec ProposalVersion; // Append-only version history (PLATFORM-004.1)
  is_test_data : opt bool;            // Staging E2E regression marker (tcu-004-1.1)
};

CreateProposalArgs ​

candid
type CreateProposalArgs = record {
  category : ProposalCategory;
  title : text;
  description : text;
  voting_period_hours : nat64;  // 24-168 hours
  execution_data : opt blob;    // Category-specific execution data (max 100KB)
  org_id : opt nat64;           // Optional Official Org scope (org-002-2)
  is_test_data : opt bool;      // Staging-only test marker; omit in production
};

Vote ​

candid
type Vote = record {
  proposal_id : nat64;
  voter : principal;
  choice : VoteChoice;
  voted_at : nat64;               // Nanoseconds
  vote_pass_nft_id : opt nat64;   // ICRC-7 NFT token ID
  refinement_feedback : opt text; // Feedback for NeedsRefinement votes (max 500 chars)
};

VoteBreakdown ​

Aggregated counts + threshold state, readable at any time before finalization (returned by get_vote_breakdown).

candid
type VoteBreakdown = record {
  yes_votes : nat64;
  no_votes : nat64;
  abstain_votes : nat64;
  needs_refinement_votes : nat64;
  total_votes : nat64;
  quorum_threshold : nat64;
  approval_threshold : nat8;   // 0..=100
  approval_percentage : nat8;  // 0..=100, floor((yes*100)/total); 0 when total=0
  quorum_met : bool;
  approval_met : bool;
};

RatificationStatus ​

Returned by get_ratification_status (a record, not a variant).

candid
type RatificationStatus = record {
  ratified : vec RatificationRecord;  // { ratifier : principal; ratified_at : nat64 }
  pending : vec principal;
  fully_ratified : bool;
};

ProposalVersion ​

candid
type ProposalVersion = record {
  version_number : nat32;  // 1-indexed — version this entry captures
  content : text;          // description as it existed at this version
  updated_at : nat64;      // nanoseconds — when this version was superseded
  updated_by : principal;  // member principal that performed the update
};

Vote-Pass NFT (ICRC-7, Soul-Bound) ​

Vote-pass NFTs follow ICRC-7. They are non-transferable (icrc7_transfer always returns NonTransferable), single-use (one per member per proposal, minted on cast_vote), and burned at finalization (recorded in the burn-history audit trail).

candid
type TransferError = variant {
  NonTransferable;      // Vote-pass NFTs cannot be transferred
  NonExistingTokenId;
  Unauthorized;
  GenericError : record { error_code : nat64; message : text };
};

Proposal Management Methods ​

create_proposal ​

Create a new proposal (requires active membership). This is the live proposal-creation path — the legacy open_proposal entrypoint was removed (bl-1330).

candid
create_proposal : (CreateProposalArgs, principal) -> (variant { Ok : nat64; Err : text });
  • Args: (CreateProposalArgs, voter_principal) — the second argument is the forwarded member identity used for the membership check and proposer attribution (AUTH-003.6 forwarding model).
  • Returns: Ok(proposal_id) on success.
  • Validation errors (returned as Err text): Title cannot be empty · Title too long (max 200 characters) · Description cannot be empty · description exceeds maximum length of 10000 chars.

TypeScript (via generated IDL):

typescript
const result = await governanceActor.create_proposal(
  {
    category: { Treasury: null },
    title: 'Allocate funds for community event',
    description: 'Proposal to allocate 1000 DOM for Q1 community meetup…',
    voting_period_hours: 72n,
    execution_data: [],
    org_id: [],
    is_test_data: [],
  },
  voterPrincipal,
);

if ('Ok' in result) {
  console.log('Created proposal ID:', result.Ok);
}

In practice member clients do not call the canister directly — they POST to the oracle-bridge REST surface, which forwards the resolved voter_principal. See PLATFORM-004.3.

get_proposal (query) ​

candid
get_proposal : (nat64) -> (opt Proposal) query;

list_proposals (query) ​

candid
list_proposals : (opt ProposalFilter, opt Pagination) -> (vec Proposal) query;

type ProposalFilter = record {
  status : opt ProposalStatus;
  category : opt ProposalCategory;
  proposer : opt principal;
  org_scope : opt OrgScope;   // All | HelloWorld | Org : nat64 (org-002-3)
};

type Pagination = record { offset : nat64; limit : nat64 };

update_proposal ​

Replace the description of a proposal still in Draft status (PLATFORM-004.1). See Draft Proposal Version History for the full contract.

candid
update_proposal : (nat64, text, opt principal) -> (variant { Ok : nat32; Err : text });

Returns the post-increment version number (nat32) — the first successful update returns 2.

get_proposal_versions (query) ​

candid
get_proposal_versions : (nat64) -> (vec ProposalVersion) query;

Oldest-first history of previous descriptions. The current description lives on the Proposal itself and is not included.


Voting Methods ​

cast_vote ​

Cast a vote on an active proposal (requires active membership). 1M1V: each vote increments the tally by exactly 1 and mints one Soul-Bound vote-pass NFT for the voter.

candid
cast_vote : (nat64, VoteChoice, opt text, principal) -> (variant { Ok; Err : text });
  • Args: (proposal_id, choice, opt feedback, voter_principal).
    • opt feedback — optional refinement feedback (max 500 chars), used with NeedsRefinement.
    • voter_principal — forwarded member identity (AUTH-003.6), used for the membership check and 1M1V vote attribution.
  • Returns: Ok on success; Err if not a member, already voted, or voting not active.

Requirements:

  • Caller (forwarded principal) must be an active member.
  • Proposal must be in Active status.
  • Cannot vote twice on the same proposal.

Side effect: mints a vote-pass NFT for the voter (burned at finalization).

Legacy: the .did still exposes vote : (nat64, VoteChoice) -> () — a simplified, void-returning alias retained for compatibility. New integrations MUST use cast_vote, which forwards the member identity and returns a Result.

has_voted (query) ​

candid
has_voted : (nat64, principal) -> (bool) query;

get_vote (query) ​

candid
get_vote : (nat64, principal) -> (opt Vote) query;

get_vote_breakdown (query) ​

Aggregated counts + threshold state at any time (pre-finalization).

candid
get_vote_breakdown : (nat64) -> (opt VoteBreakdown) query;

get_refinement_feedback (query) ​

All NeedsRefinement feedback entries for a proposal. The voter principal is redacted (secret-ballot direction, bl-1173-2) — feedback stays community-visible but is not linked back to a member's vote choice.

candid
get_refinement_feedback : (nat64) -> (vec RefinementFeedback) query;

type RefinementFeedback = record { feedback : text; voted_at : nat64 };

Proposal Finalization & Execution ​

finalize ​

Finalize a proposal after its voting period ends. Tallies votes, sets Approved/Rejected, and burns all vote-pass NFTs for the proposal.

candid
finalize : (nat64) -> (nat64, nat64, nat64, nat64);
  • Returns: (yes_votes, no_votes, abstain_votes, needs_refinement_votes) — a 4-tuple (the needs_refinement_votes count was added in Story FOS-4.1.1).
  • Authorization (bl-1333): Layer-1 anonymous-reject + Layer-2 principals.is_authorized(caller, "governance:proposal:finalize"). Holders: dao-officer (manual) and governance-service (auto-finalize orchestrator). Denial traps; the tuple shape is unchanged.

TypeScript:

typescript
const [yes, no, abstain, needsRefinement] = await governanceActor.finalize(proposalId);
const total = yes + no + abstain + needsRefinement;

execute_proposal ​

Execute an approved proposal. Self-contained categories transition Approved → Executed (or Failed); orchestrated categories (Treasury Payout / Personnel StatusChange) transition Approved → Executing and emit an ExecutionIntentEvent — the terminal outcome is then set only by report_execution_result.

candid
execute_proposal : (nat64) -> (variant { Ok; Err : text });
  • Errors: Proposal not approved (Rejected/Draft/Active) · Proposal already executed (Executed/Failed) · Proposal execution already in progress (Executing).
  • Authorization (arch-004-3a.1): Layer-1 anonymous-reject + Layer-2 principals.is_authorized(caller, "governance:proposal:execute"). This is the manual execute/retry path; automatic dispatch reaches execution through an internal Rust call from finalize, not this method. Who may re-drive execution manually is an open owner decision (bl-1331).

report_execution_result ​

The off-chain governance-service reports an orchestrated effect's outcome, transitioning Executing → Executed (Ok) / Failed (Err) and appending an immutable ExecutionResultEvent. Matching replay is an idempotent no-op; conflicting replay is rejected.

candid
report_execution_result : (nat64, variant { Ok : ExecutionEvidence; Err : text }) -> (variant { Ok; Err : text });

type ExecutionEvidence = record { summary : text; external_ref : opt text };

Authorization: Layer-1 anonymous-reject + Layer-2 principals.is_authorized(caller, "governance:execution:report") — mandatory and fail-closed.

get_execution_intents / get_execution_results (query) ​

On-chain, immutable audit trail of orchestrated execution.

candid
get_execution_intents : (nat64) -> (vec ExecutionIntentEvent) query;
get_execution_results : (nat64) -> (vec ExecutionResultEvent) query;

ICRC-7 Vote-Pass NFT Methods ​

Vote-pass NFTs are Soul-Bound Tokens minted on cast_vote and burned at finalization. These methods are implemented and live.

candid
icrc7_name : () -> (text) query;                 // e.g. "Hello World DAO Vote Pass"
icrc7_symbol : () -> (text) query;               // e.g. "HWDVP"
icrc7_total_supply : () -> (nat64) query;        // active (not-yet-burned) vote passes
icrc7_balance_of : (principal) -> (nat64) query;
icrc7_owner_of : (nat64) -> (opt principal) query;
icrc7_token_metadata : (nat64) -> (opt vec record { text; Value }) query;
icrc7_tokens : (opt nat64, opt nat64) -> (vec nat64) query;       // (prev, take) pagination
icrc7_tokens_of : (principal) -> (vec nat64) query;
icrc7_transfer : (principal, principal, nat64, opt blob, opt nat64)
  -> (variant { Ok : nat64; Err : TransferError });               // ALWAYS returns NonTransferable

mint_vote_passes (legacy) ​

candid
mint_vote_passes : (nat64) -> ();

Legacy pre-mint entrypoint. In the current model a vote-pass is minted per voter on cast_vote, so pre-minting to all members is no longer part of the standard flow.


Audit Trail ​

get_burn_history / get_all_burn_history (query) ​

candid
get_burn_history : (nat64) -> (vec BurnRecord) query;
get_all_burn_history : (opt nat64, opt nat64) -> (vec BurnRecord) query;   // (offset, limit)

type BurnRecord = record {
  token_id : nat64;
  proposal_id : nat64;
  voter : principal;
  burned_at : nat64;
};

get_voter_stats (query) ​

candid
get_voter_stats : (principal) -> (VoterStats) query;

type VoterStats = record { proposals_created : nat64; votes_cast : nat64 };

Category Statistics ​

candid
get_proposal_count_by_category : () -> (vec record { ProposalCategory; nat64 }) query;
get_active_proposals_by_category : (ProposalCategory) -> (vec Proposal) query;

DOM Price Feed (BL-076.1) ​

Governance-set DOM/USD price, set by a passed price proposal.

candid
get_current_dom_price : () -> (opt DomPriceRecord) query;   // None if never set
get_dom_price_history : (opt nat64) -> (vec DomPriceRecord) query;  // newest first, default 10, max 50

type DomPriceRecord = record { usd_price : float64; set_at : nat64; proposal_id : nat64 };

CLT Board & Ratification (Immutable / Tier1Constitutional) ​

The Immutable and Tier1Constitutional categories enforce three gates before an outcome takes effect: a 90% supermajority, a 180-day voting window, and CLT Board ratification.

set_clt_board ​

Replace the CLT Board member list (controller-only; full replacement, not additive).

candid
set_clt_board : (vec principal) -> (variant { Ok; Err : text });

ratify_proposal ​

CLT Board ratification of a passed proposal (CLT Board members only).

candid
ratify_proposal : (nat64) -> (variant { Ok : text; Err : text });

get_clt_board (query) ​

candid
get_clt_board : () -> (vec principal) query;

get_ratification_status (query) ​

Returns the RatificationStatus record (ratified list, pending list, fully_ratified flag).

candid
get_ratification_status : (nat64) -> (RatificationStatus) query;

attest_clt_ratification ​

Attest CLT Board ratification for a Tier1Constitutional proposal (controller-only).

candid
attest_clt_ratification : (nat64) -> (variant { Ok; Err : text });

§14.5 Amended-Proposal Appeal Path (BL-450 — partial build) ​

File and disposition appeals against already-tallied proposals. Partial build: only the two coercion grounds (Duress, AccountTakeover) are live invalidation-eligible; the integrity grounds are defined but Tier-B counsel-gated, and MaterialMisrepresentation is intake-only (routes to §14.8, never invalidates).

candid
type FindingType = variant {
  Duress; AccountTakeover;                    // LIVE invalidation-eligible
  EvidenceMisrepresentation; ProcessIrregularity;  // DEFINED, counsel-gated (not live)
  MaterialMisrepresentation;                  // Intake-only
};

submit_amended_proposal : (nat64, FindingType, text, principal) -> (variant { Ok : nat64; Err : text });
list_amended_proposals : (nat64) -> (vec AmendedProposal) query;
get_amended_proposal : (nat64) -> (opt AmendedProposal) query;
get_invalidation : (nat64) -> (opt InvalidationRecord) query;   // Some ⇔ target is Invalid
mark_proposal_invalid : (nat64, nat64, FindingType) -> (variant { Ok; Err : text });

Live gating note: submit_amended_proposal and mark_proposal_invalid are fail-closed and no role currently holds the required permissions, so they refuse every caller today. This is the designed pre-rollout state, not an outage — the permissions are granted by the story that ships the member-facing appeal flow. Filing does not invalidate the target; disposition is a separate mark_proposal_invalid call.


Operational Decision Logging (BL-086, OA §4A.6) ​

candid
log_operational_decision : (LogOperationalDecisionArgs) -> (variant { Ok : nat64; Err : text });
get_operational_logs : (opt nat64, opt nat64) -> (vec OperationalLog) query;
get_operational_log : (nat64) -> (opt OperationalLog) query;

type LogOperationalDecisionArgs = record {
  decision : text;
  resources_committed : opt text;
  rsa_section : opt text;
};

Threshold Configuration (Controller Only) ​

Adjust default quorum/approval percentages per category. Only affects future proposals; existing proposals retain their original thresholds.

candid
set_default_quorum_percentage : (ProposalCategory, nat8) -> (variant { Ok; Err : text });
set_default_approval_percentage : (ProposalCategory, nat8) -> (variant { Ok; Err : text });

Health & State ​

candid
health : () -> (text) query;                        // returns "ok"
export_state : () -> (blob) query;                  // public — proposals/votes are public
import_state : (blob) -> (variant { Ok; Err : text });  // controller-only restore

Error Messages ​

ErrorCauseResolution
Not a member / Not an active memberForwarded caller is not an active memberComplete signup / reactivate
Already votedDuplicate vote attemptCannot vote twice
Voting not activeProposal not in Active statusWait for the voting window
Proposal not foundInvalid proposal IDCheck the proposal exists
Proposal not approvedExecuting a non-approved proposalProposal must pass first
Title cannot be empty / Title too long (max 200 characters)Invalid title on createFix title
Description cannot be empty / description exceeds maximum length of 10000 charsInvalid description on create/updateFix description
only the original proposer can update this proposalForwarded caller ≠ proposal.proposerOnly the proposer may update
proposal not in Draft statusUpdate after the draft window (time-aware)Draft edits close when voting starts

PLATFORM-004.1 — Draft Proposal Version History ​

Draft proposals carry an append-only edit history so proposers can refine content during the review window without losing previous revisions. The current content lives in description; previous revisions accumulate in versions as ProposalVersion entries.

Invariant: after N successful update_proposal calls, proposal.version_number == N + 1 and proposal.versions.len() == N. Entry versions[i] contains the content that was current at version_number = i + 1, which description has since replaced.

update_proposal — full contract ​

Signature: update_proposal : (nat64, text, opt principal) -> (variant { Ok : nat32; Err : text });

Parameters:

  • proposal_id: nat64 — target proposal.
  • new_content: text — replacement for description. Empty strings and content over 10,000 characters reject at the canister boundary.
  • opt caller: opt principal — under the oracle-bridge forwarding model (AUTH-003.6), this is the forwarded voter_principal, NOT ic_cdk::caller(). MUST equal proposal.proposer.

Returns: Ok(new_version_number) — the post-increment version, so the first successful update returns 2.

Err messageCause
proposal not foundUnknown proposal_id
only the original proposer can update this proposalForwarded caller does not match proposal.proposer
proposal not in Draft statusEffective status is not Draft (time-aware — a Draft that silently rolled into Active cannot be updated)
Description cannot be emptynew_content is the empty string
description exceeds maximum length of 10000 charsContent too long (BL-190 cap)
proposal has reached maximum revision count of 100 — cannot update furtherversions.len() >= MAX_VERSIONS_PER_PROPOSAL (BL-190)

Effective-Status Gate (important) ​

update_proposal checks effective status rather than the stored ProposalStatus. A proposal whose stored status is still Draft but whose voting_start_at has elapsed is treated as Active — the canister rejects the update with proposal not in Draft status rather than silently editing a proposal that voters might already be considering.


PLATFORM-004.3 — FOS Document → Proposal REST Surface ​

Two REST endpoints on oracle-bridge wrap the canister's create_proposal and update_proposal methods with the FounderyOS publish-flow wire contract. The FOS editor never calls the canister or talks to oracle-bridge directly — the FOS backend converts editor documents to the 10,000-char-capped Markdown the canister accepts, then forwards to oracle-bridge with the user's session cookie.

Gateway path (via PLATFORM-006 API gateway): /oracle/api/governance/proposals/from-document (prefix stripped at the edge). Direct access: https://staging-oracle.helloworlddao.com/api/governance/proposals/from-document.

Authentication ​

  • Browser → FounderyOS: cross-domain session established by identity-service PKCE SSO — POST /cross-domain/authorize + POST /token/exchange (ADR-024 / ADR-025 / ADR-026). No extra token required. ⛔ The former PLATFORM-003 endpoint /api/v1/auth/cross-domain-login was deleted 2026-06-25 (oracle-bridge 478b50a); see the retired cross-domain-auth reference. (bl-1286)
  • FOS backend → oracle-bridge: the FOS request's session cookie is forwarded verbatim. oracle-bridge's requireSessionAuth middleware resolves the voter principal via resolveVoterPrincipal(session).
  • oracle-bridge → governance canister: forwards the resolved principal as the voter_principal argument (AUTH-003.6 forwarding model). The canister rejects if caller != proposal.proposer on updates.

POST /api/governance/proposals/from-document — Create ​

Create a new proposal from a converted Markdown body. Rate limit: canisterRateLimit (ic-query budget).

Request body (zod-validated on oracle-bridge):

typescript
{
  category: "Operational" | "Treasury" | "Constitutional"
          | "Personnel" | "SoftwareDevelopment" | "Immutable" | "Tier1Constitutional";
  title:  string;              // 1..=200 chars
  content: string;             // 1..=10,000 chars (the converted Markdown)
  content_format?: "text";     // reserved for future rich-text
  votingPeriodHours: number;   // 24..=180*24 (180-day window allowed for Immutable)
  sector_tags?: Array<         // REQUIRED when category == "SoftwareDevelopment"
    "housing" | "food_water" | "education" | "economy"
    | "healthcare" | "environment" | "energy"
  >;
}

Response (201):

json
{
  "success": true,
  "proposalId": 42,
  "versionNumber": 1,
  "governanceUrl": "https://staging-governance.helloworlddao.com/proposals/42"
}

Errors (oracle-bridge surface):

StatuserrorCause
400<raw canister Err>Canister-side validation failure (unknown categories, invalid sector tags, voting period out of range, etc.)
400Document has no content after conversionConverter produced an empty body
401UnauthorizedSession cookie absent / expired
403NOT_ELIGIBLEAny governance eligibility rejection — the precise cause is carried in the additive reason field (see Eligibility rejections)
403NOT_MEMBERoracle-bridge pre-gate rejection before the canister is called
500Internal Server ErrorUncaught error; creation is idempotent at the proposal layer
502IC-transport errorsClassified via classifyIcError

PUT /api/governance/proposals/:id/from-document — Update ​

Replace an existing Draft proposal's content with a freshly converted Markdown body. Rate limit: canisterRateLimit.

typescript
{
  title?: string;     // 1..=200 — accepted for forward-compat, NOT forwarded to canister today
  content: string;    // 1..=10,000 — becomes the new description
  content_format?: "text";
}

The title is deliberately not forwarded to update_proposal today — the canister only versions description. Accepting the field keeps the wire contract stable for when title-update lands in a future sub-story.

Response (200): same { success, proposalId, versionNumber, governanceUrl } shape, with the incremented versionNumber.

Errors (mapped from canister Err strings via mapGovernanceUpdateErr):

StatuserrorCanister Err substringMeaning
400VALIDATION_ERRORdescription cannot be empty, exceeds maximum lengthContent empty or > 10,000 chars
400<raw>otherUnknown canister domain error (surfaces raw)
403NOT_AUTHORonly the original proposerCaller does not match proposal.proposer
403NOT_ELIGIBLEevery VotingEligibility sentence — see Eligibility rejectionsCaller failed the canister's eligibility gate; reason names which arm
404NOT_FOUNDproposal not foundUnknown proposal id
409NOT_IN_DRAFTproposal not in Draft statusProposal is Active / Approved / Rejected / Executed / Failed (time-aware)
409REVISION_CAPmaximum revision count100-revision cap reached
500Internal Server Error—Uncaught error
502IC-transport errors—classifyIcError classification

Eligibility rejections (NOT_ELIGIBLE) ​

Since bl-1171, every governance eligibility rejection on the oracle-bridge REST surface returns 403 with a machine-readable reason alongside member-safe copy. The governance canister returns Result<_, String>, so oracle-bridge classifies the sentence in one shared module (src/utils/vote-eligibility-error.ts) rather than in each route.

Applies to all six eligibility-bearing routes: POST /api/governance/proposals, POST /api/governance/proposals/:id/vote, PATCH /api/governance/proposals/:id, POST /api/governance/proposals/from-document, PUT /api/governance/proposals/:id/from-document, and the service-token path POST /api/governance/vote-cast/:id.

Response body:

json
{
  "success": false,
  "error": "NOT_ELIGIBLE",
  "reason": "VOTING_COOLDOWN",
  "message": "Your membership is still in its post-activation waiting period. You can vote in 5 days.",
  "eligibleAt": "2026-07-28T20:42:56.827Z",
  "eligibleInSeconds": 432000
}

error remains NOT_ELIGIBLE — unchanged, because governance-suite and dao-suite both branch on it. Everything else is additive.

FieldPresenceNotes
erroralwaysAlways the literal NOT_ELIGIBLE. Branch on this plus reason, never on message.
reasonalways (classified rejections)Stable machine code — see the table below. Absent on the residual membership catch-all.
messagealwaysMember-safe sentence. Contains no principal, no raw timestamp, no internal column name. Safe to render verbatim.
eligibleAtVOTING_COOLDOWN onlyISO-8601 UTC instant the member becomes eligible. Omitted when it exceeds 366 days or could not be decoded.
eligibleInSecondsVOTING_COOLDOWN onlyWhole seconds remaining, rounded up, clamped to 0. Present exactly when eligibleAt is.

reason values, one per VotingEligibility arm the canister can return:

reasonCanister Err
VOTING_COOLDOWNVoting cooldown: eligible after {ns} (ns since epoch)
NOT_ACTIVE_MEMBERNot an active member
MEMBERSHIP_EXPIREDYour membership has expired
MEMBERSHIP_REVOKEDYour membership has been revoked
MEMBERSHIP_INACTIVEYour membership is inactive — reactivate it to vote
MEMBERSHIP_RECORD_INCOMPLETEMembership state error: activated_at missing — contact support
NO_MEMBERSHIPNo membership found for principal
ORG_MEMBERSHIP_REQUIREDNot a member of org {id}

The raw eligible_at nanosecond integer is logged server-side and appears in no response body on any route.

Note on stability. These reason codes are derived by matching canister prose, which is inherently fragile — a Rust-side reword silently breaks the mapping. Each arm is pinned by a literal fixture in oracle-bridge's test suite so a reword fails CI by name. The durable fix is a typed Candid error variant, tracked as bl-1171-1.



Changelog ​

Version 3.0 (2026-08-25) — bl-1404 ​

  • Merged the former governance-api.md (comprehensive) and governance.md (Candid quick-ref) into this single canonical page, arbitrated method-by-method against the live governance.did.
  • Corrected stale vocabulary: removed the retired open_proposal : (text, text, nat64, nat64) -> (nat64) entrypoint (removed on-chain by bl-1330) in favour of create_proposal : (CreateProposalArgs, principal) -> (variant { Ok : nat64; Err : text }); documented cast_vote (with the opt feedback + forwarded voter_principal args and Result return) as the primary voting method, with the void-returning vote noted as a legacy alias.
  • Corrected signatures/types against the .did: finalize returns a 4-tuple (adds needs_refinement_votes); VoteChoice includes NeedsRefinement; the full ProposalCategory (7 variants), ProposalStatus (8 variants), Proposal, CreateProposalArgs, Vote, VoteBreakdown, and RatificationStatus (a record, previously mis-documented as a variant) now match live.
  • Removed the v1.0 "current implementation is stubbed / not enforced" framing and the speculative "future implementation" pseudo-code that no longer reflected the shipped canister; the ICRC-7 vote-pass methods are documented as live (they were previously listed as "to be implemented").
  • Preserved the accurate advanced sections: PLATFORM-004.1 draft version history, PLATFORM-004.3 FOS document → proposal REST surface, and the bl-1171 NOT_ELIGIBLE error contract. Added the execution-orchestration (ADR-034) and §14.5 amended-proposal surfaces from the live .did.

Version 2.2 (2026-07-29) ​

  • bl-1171: documented the NOT_ELIGIBLE error contract; oracle-bridge classifies every VotingEligibility arm into a stable reason code plus member-safe message, across all six eligibility-bearing routes.

Version 2.1 (2026-04-20) ​

  • PLATFORM-004.1: draft-only version history — ProposalVersion, update_proposal, get_proposal_versions.
  • PLATFORM-004.3: FOS document → proposal REST surface on oracle-bridge.

Version 2.0 (2026-04-06) ​

  • Added the Immutable proposal category and CLT Board ratification methods.

Version 1.0 (2025-11-15) ​

  • Initial API documentation.

Hello World DAO