Skip to main content

Intents: Human Approval for Irreversible Actions

An intent is a proposed action that waits for a person. Your application says what it wants to do; a human with permission on the resource reviews it on Wefunder and approves or rejects it; only then does it run. The API uses intents for the actions that cannot be undone.

Why​

Agents hallucinate and automations have bugs. In a regulated financial context a single wrong call can close a raise or remove an investor's access, and there is no undo. Intents separate who proposes from who approves: software proposes, a person approves, and the proposer can never approve its own proposal.

Which actions need an intent​

Action nameResourceParamsWhat it does
syndicates.remove_memberthe syndicate{ "member_id" }Remove a member from the syndicate
syndicates.publishthe syndicatenoneMake the syndicate public
syndicates.delete_draftthe syndicatenoneDelete an unpublished syndicate
comments.createCompany{ "target_type", "target", "body", "disclosure_key"? }Post a question on a company's Ask tab (target_type: "company", target: co_...) or answer one (target_type: "comment", target: cmt_...) as the requesting user. Scope write:comments. Only the requester can approve; 20 per user per day

The syndicates.* actions require the write:syndicates scope; comments.create requires write:comments and is rolling out per account, so a token whose user is not yet enabled gets 403 feature_disabled. Everything else on the Syndicate API (inviting, approving, promoting, demoting, updating members, editing settings) is reversible and executes immediately.

Proposing goes through POST /intents. Trying to bypass the system, for example DELETE /syndicates/{id}/members/{member_id}, returns 422 with type: "use_intents".

Closing a deal moved to wefunder.com in 2026-10-15

syndicates.close_deal and syndicates.finalize_deal are not actions on Wefunder-Version: 2026-10-15 or later: POST /intents answers 400 bad_request ("Unknown action") with details.removed_in_version: "2026-10-15". A syndicate's managers close and finalize deals on wefunder.com. Applications pinned to 2026-10-01 can still propose both, with { "fundraise_id" } params, and still have the dedicated close and finalize deal endpoints that mint them. Intents minted earlier keep their action names in GET /intents.

Propose​

curl -X POST "https://api.wefunder.com/intents" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"action_name": "syndicates.remove_member",
"resource_type": "Club",
"resource_id": "syn_aB3xQ9k2vF8mNp1zT5wY7Qc4",
"params": { "member_id": "mem_7Hk2mP9qR4sT1uV6wX8yZ3aB" },
"idempotency_key": "remove-mem_7Hk2mP9qR4sT1uV6wX8yZ3aB-2026-09",
"requested_by_agent": "Claude via MCP"
}'
ParamRequiredDescription
action_nameyesOne of the actions above
resource_typeyesThe literal string Club for every syndicates.* action (the API's name for a syndicate); Company for comments.create
resource_idyesThe syndicate's syn_... id, or the company's co_... id
paramsper actionA JSON object; see the table
idempotency_keynoNames this one operation; see Idempotency
requested_by_agentnoShown to the reviewer as who asked

Response 201:

{
"data": {
"id": "int_aB3xQ9k2vF8mNp1zT5wY7Qc4",
"type": "intent",
"attributes": {
"action": "syndicates.remove_member",
"status": "pending",
"resource_type": "Club",
"resource_id": "syn_aB3xQ9k2vF8mNp1zT5wY7Qc4",
"impact_summary": "Remove Jane Smith from Acme Syndicate. She will lose access to the syndicate's deals.",
"review_url": "https://wefunder.com/intents/a1b2c3d4-e5f6-7890-abcd-ef1234567890/review",
"requested_by_agent": "Claude via MCP",
"expires_at": "2026-03-06T14:30:00Z",
"approved_at": null,
"executed_at": null,
"rejection_reason": null,
"execution_result": null,
"created_at": "2026-03-05T14:30:00Z",
"updated_at": "2026-03-05T14:30:00Z"
}
}
}

Errors you may see: 400 bad_request for an unknown action, wrong resource_type, or non-object params; 403 insufficient_scope when the token lacks the action's scope; 403 feature_disabled when the action is not yet enabled for this account; 403 forbidden when a resource-bound token targets a different syndicate or the user may not propose here; 409 idempotency_conflict when the key was used for a different operation; 422 validation_error when the handler refuses the proposal, with the reason in message; 429 rate_limit_exceeded after 10 new pending intents in an hour (20 comments.create proposals per user per day).

Preview​

POST /intents/preview runs every check POST /intents runs and returns what the reviewer would read, without minting anything. Send the same body without idempotency_key. It is admitted by any proposing scope or by read:explore, and the action's own scope is not required on the token, so an agent can let the user shape a proposal before the write permission is granted. The caller must still be allowed to propose on the resource (for syndicate actions, an edit-tier principal), and for a syndicate-owned token the application's Installation scope ceiling still applies: an install granted only read:syndicates cannot preview a write:syndicates action (403 insufficient_scope).

{
"data": {
"type": "intent_preview",
"attributes": {
"action": "comments.create",
"resource_type": "Company",
"resource_id": "co_aB3xQ9k2vF8mNp1zT5wY7Qc4",
"params": { "target_type": "company", "target": "co_aB3xQ9k2vF8mNp1zT5wY7Qc4", "body": "What is your runway?" },
"impact_summary": "Post a question on Acme's Ask tab as Jane Smith.",
"scope": "write:comments"
}
}
}

A refused preview is 422 validation_error with the handler's reason, the same response POST /intents would give.

Review​

review_url is a page on Wefunder. The reviewer must be logged in and hold permission on the resource; the handler re-checks that permission when the page loads and again on approve, so a role change after the proposal blocks approval. The page shows the proposing application and agent, the impact summary, the exact parameters, the expiry, and Approve / Reject.

Outcome​

StatusMeaning
pendingWaiting for a reviewer
approvedApproved; execution about to start
executingRunning
executedDone. execution_result is populated
rejectedA reviewer declined. rejection_reason may be set
expiredNobody acted before expires_at
failedApproved, but execution raised

Poll GET /intents/{id} for the outcome, or list with filters:

GET /intents?status=pending&resource_type=Club&resource_id=syn_aB3xQ9k2vF8mNp1zT5wY7Qc4

You see only intents your own application proposed for this user. The list is paginated by timestamp cursor like every other list.

Expiry​

Pending intents expire 24 hours after proposal (deleting a draft syndicate expires after 12). A proposal made today should not become executable next week when the facts may have changed. Propose again if it expires.

Guarantees​

  • The proposal is sealed. The action, resource, requesting user, and params are hashed at proposal time and re-verified before execution; a tampered row does not run.
  • Permission is re-checked at approval, not assumed from the proposal.
  • Scoped visibility. An application sees only the intents it proposed for the authorizing user.
  • Replay is safe. A repeated proposal with the same idempotency_key returns the existing pending intent instead of a second one.

For agents​

When a tool wraps an intent, return the summary and the link and stop:

I've proposed removing Jane Smith from Acme Syndicate. This won't happen until you approve it.

Review here: https://wefunder.com/intents/…/review
Expires: tomorrow at 2:30 PM.

There is no execute endpoint for the agent to call. See how intents work in practice in the syndicate quickstart.

Was this page helpful?