furrow-forms

Form backend an agent runs end to end: provision forms, snippets, spam, signed webhooks.

사용해야 할까요

품질 및 안전성

A
설명 품질
98%
스키마 완전성
93%
이름 품질
96%
오염 위험
100%
권한 일치
100%
프로토콜 준수
100%

도구 정의와 프로토콜 준수에 대한 자동 분석을 기반으로 합니다.

컨텍스트 비용

~16,128토큰 (도구 정의)
~4.3 KB일반적인 응답 크기
상당한 주의 영향 (128k 컨텍스트의 12.60%)

이는 서버의 도구가 모델의 컨텍스트에 로드될 때마다 소비되는 대략적인 토큰 수입니다. 수치가 높을수록 다른 작업에 사용할 수 있는 주의가 줄어듭니다.

설치

원클릭 설치

`claude_desktop_config.json` 파일에 다음을 추가하세요:

{
  "mcpServers": {
    "furrow-forms": {
      "command": "npx",
      "args": [
        "@furrowforms/mcp"
      ]
    }
  }
}

실행 가능한 패키지

npm@furrowforms/mcp1.0.1stdio

원격 엔드포인트

https://api.furrowforms.com/mcpstreamable-http

할 수 있는 일

도구 목록

도구 (27)

🟢 읽기 전용🟡 쓰기🔴 삭제⚪ 알 수 없음
🟢list_teams

Use to see workspaces this token can act on. A team-scoped frw_ token returns that team only. An operator token or an MCP OAuth login returns every team you belong to — pass team_id on list_clients, create_client, and bootstrap_site. Creating another workspace is dashboard-only after signing in and requires a yearly plan; free accounts stay on one team. Do not use this to list clients — call list_clients.

입력 스키마

{
  "type": "object",
  "properties": {},
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🟡update_team(name, team_id)

Use to rename a team. Operator tokens (all workspaces) must pass team_id from list_teams. Team-scoped tokens may omit team_id and cannot target another workspace. Do not use this to create another workspace — that is dashboard-only and requires yearly. For plan, usage, retention, and branding, call get_account / update_account. Do not use this to change clients or projects.

입력 스키마

{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 120,
      "description": "New team (workspace) display name."
    },
    "team_id": {
      "description": "Team id from list_teams. Required for operator tokens and OAuth logins; team-scoped frw_ tokens may omit it.",
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    }
  },
  "required": [
    "name"
  ],
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🟢get_account(team_id)

Use to read the team's plan, usage this month, limits, expansion_packs, retention_days, branding, and AI spam filter (ai.typesafe_key_set, ai.notify_suspected_spam). The TypeSafe key itself is never returned. Plans are capacity, not a feature ladder. Free: one workspace, 100 clean submissions/month pooled on the team, 3 active (non-archived) projects, 30-day submission retention, and email.footer_mode / branding.mode stay furrow. Yearly ($199/year): extra workspaces, 10,000 submissions/month, unlimited projects, 730-day retention, branding.mode furrow | off | agency. Expansion packs are $99/year each and add 5,000 submissions/month on a yearly account. MCP, webhooks, snippets, inheritance, Turnstile, and email templates stay available on free. Spam, honeypot, and rejected posts do not count. Use create_upgrade_link to hand a human a Stripe Checkout (or Customer Portal) URL. Pass product=expansion on a yearly team to add a pack. Operator tokens (all workspaces) must pass team_id from list_teams. Team-scoped tokens may omit team_id and cannot target another workspace. Do not use this to list projects — call list_projects.

입력 스키마

{
  "type": "object",
  "properties": {
    "team_id": {
      "description": "Team id from list_teams. Required for operator tokens and OAuth logins; team-scoped frw_ tokens may omit it.",
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    }
  },
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🟡update_account(name, branding, ai, team_id)

Use to rename the workspace, set branding, or configure the AI spam filter. Pass ai.typesafe_api_key to save this team's TypeSafe key (null clears it; the key is write-only). ai.notify_suspected_spam (default true) emails notify addresses with a Potential spam label when Jev flags a submission; no webhook is sent either way. Heuristic spam stays silent. Free accounts cannot set branding.mode or email.footer_mode to anything but furrow — those writes are ignored. Yearly may set branding.mode to furrow, off, or agency (name + optional link + optional logo; Furrow keeps the layout, no raw HTML). Plans are capacity, not a feature ladder. Free: one workspace, 100 clean submissions/month pooled on the team, 3 active (non-archived) projects, 30-day submission retention, and email.footer_mode / branding.mode stay furrow. Yearly ($199/year): extra workspaces, 10,000 submissions/month, unlimited projects, 730-day retention, branding.mode furrow | off | agency. Expansion packs are $99/year each and add 5,000 submissions/month on a yearly account. MCP, webhooks, snippets, inheritance, Turnstile, and email templates stay available on free. Spam, honeypot, and rejected posts do not count. Use create_upgrade_link to hand a human a Stripe Checkout (or Customer Portal) URL. Pass product=expansion on a yearly team to add a pack. Operator tokens (all workspaces) must pass team_id from list_teams. Team-scoped tokens may omit team_id and cannot target another workspace. Do not use this to change the plan — call create_upgrade_link.

입력 스키마

{
  "type": "object",
  "properties": {
    "name": {
      "description": "New workspace display name.",
      "type": "string",
      "minLength": 1,
      "maxLength": 120
    },
    "branding": {
      "description": "Account-wide branding for notification emails and hosted pages.",
      "type": "object",
      "properties": {
        "mode": {
          "description": "furrow shows Furrow branding, off hides it, agency shows your own name/link/logo. Only yearly plans may change this.",
          "type": "string",
          "enum": [
            "furrow",
            "off",
            "agency"
          ]
        },
        "agency_name": {
          "description": "Agency name shown when mode is agency. Null clears it.",
          "anyOf": [
            {
              "type": "string",
              "minLength": 1,
              "maxLength": 120
            },
            {
              "type": "null"
            }
          ]
        },
        "agency_url": {
          "description": "Optional http(s) link for the agency name when mode is agency. Null clears it.",
          "anyOf": [
            {
              "type": "string",
              "format": "uri"
            },
            {
              "type": "null"
            }
          ]
        },
        "logo_url": {
          "description": "Optional http(s) logo image URL when mode is agency. Null clears it.",
          "anyOf": [
            {
              "type": "string",
              "format": "uri"
            },
            {
              "type": "null"
            }
          ]
        }
      }
    },
    "ai": {
      "description": "AI spam filter. Off until this team saves its own TypeSafe key. One Jev call per heuristics-clean submission.",
      "type": "object",
      "properties": {
        "typesafe_api_key": {
          "description": "TypeSafe API key for the AI spam filter. Send null or an empty string to remove it. The key is write-only and never returned.",
          "anyOf": [
            {
              "type": "string",
              "maxLength": 500
            },
            {
              "type": "null"
            }
          ]
        },
        "notify_suspected_spam": {
          "description": "When true (the default), submissions the AI flags as spam still email notify addresses with a Potential spam label and send no webhook. Set false to silence those emails.",
          "type": "boolean"
        }
      }
    },
    "team_id": {
      "description": "Team id from list_teams. Required for operator tokens and OAuth logins; team-scoped frw_ tokens may omit it.",
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    }
  },
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🟡create_upgrade_link(team_id, product)

Use when a free team is blocked on the 3-project, 100-submission, or one-workspace limit, or when a human should upgrade to yearly ($199/year) so you can keep adding projects, workspaces, and storing new submissions. Returns a Stripe Checkout URL to hand the human. If the team is already yearly, omit product to get a Stripe Customer Portal URL, or pass product=expansion for a $99/year pack that adds 5,000 submissions/month. Do not change plan in the database — Stripe webhooks do that. Plans are capacity, not a feature ladder. Free: one workspace, 100 clean submissions/month pooled on the team, 3 active (non-archived) projects, 30-day submission retention, and email.footer_mode / branding.mode stay furrow. Yearly ($199/year): extra workspaces, 10,000 submissions/month, unlimited projects, 730-day retention, branding.mode furrow | off | agency. Expansion packs are $99/year each and add 5,000 submissions/month on a yearly account. MCP, webhooks, snippets, inheritance, Turnstile, and email templates stay available on free. Spam, honeypot, and rejected posts do not count. Use create_upgrade_link to hand a human a Stripe Checkout (or Customer Portal) URL. Pass product=expansion on a yearly team to add a pack. Operator tokens (all workspaces) must pass team_id from list_teams. Team-scoped tokens may omit team_id and cannot target another workspace.

입력 스키마

{
  "type": "object",
  "properties": {
    "team_id": {
      "description": "Team id from list_teams. Required for operator tokens and OAuth logins; team-scoped frw_ tokens may omit it.",
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "product": {
      "description": "yearly starts the $199/year plan; expansion adds a $99/year 5,000-submission pack to a yearly team.",
      "type": "string",
      "enum": [
        "yearly",
        "expansion"
      ]
    }
  },
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🟢list_clients(limit, cursor, status, team_id)

Use to browse clients (companies). Operator tokens (all workspaces) must pass team_id from list_teams. Team-scoped tokens may omit team_id and cannot target another workspace. Do not use to list websites — call list_projects with client_id. Archived clients are hidden unless status=all or archived.

입력 스키마

{
  "type": "object",
  "properties": {
    "limit": {
      "description": "Page size, max 100.",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 100
    },
    "cursor": {
      "type": "string",
      "minLength": 1,
      "description": "Opaque cursor from the previous page's next_cursor."
    },
    "status": {
      "type": "string",
      "enum": [
        "active",
        "archived",
        "all"
      ],
      "description": "Filter by archive state. Defaults to active."
    },
    "team_id": {
      "description": "Team id from list_teams. Required for operator tokens and OAuth logins; team-scoped frw_ tokens may omit it.",
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    }
  },
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🟢get_client(client_id)

Use to inspect one client. Do not use to list that client's sites — call list_projects with client_id.

입력 스키마

{
  "type": "object",
  "properties": {
    "client_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
      "description": "Client id from list_clients or create_client."
    }
  },
  "required": [
    "client_id"
  ],
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🟡create_client(name, slug, strict, team_id)

Use to add a client (company) by slug. Idempotent on slug unless strict is true. Operator tokens (all workspaces) must pass team_id from list_teams. Team-scoped tokens may omit team_id and cannot target another workspace. Prefer bootstrap_site when also creating a website and forms. Do not put Turnstile or webhooks on a client — those live on the project.

입력 스키마

{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 120,
      "description": "Client (company) display name."
    },
    "slug": {
      "type": "string",
      "minLength": 1,
      "maxLength": 80,
      "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
      "description": "Lowercase hyphenated identifier, unique per team. Used for idempotent re-runs."
    },
    "strict": {
      "default": false,
      "description": "When true, fail if the slug already exists instead of returning the existing record.",
      "type": "boolean"
    },
    "team_id": {
      "description": "Team id from list_teams. Required for operator tokens and OAuth logins; team-scoped frw_ tokens may omit it.",
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    }
  },
  "required": [
    "name",
    "slug"
  ],
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🟡update_client(name, client_id)

Use to rename a client. Do not use this to change domains, Turnstile, or webhooks — call update_project.

입력 스키마

{
  "type": "object",
  "properties": {
    "name": {
      "description": "New client display name.",
      "type": "string",
      "minLength": 1,
      "maxLength": 120
    },
    "client_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
      "description": "Client id from list_clients or create_client."
    }
  },
  "required": [
    "client_id"
  ],
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🔴archive_client(client_id)

Use to archive a client. Do not use this as a hard delete. There is no unarchive tool in v1.

입력 스키마

{
  "type": "object",
  "properties": {
    "client_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
      "description": "Client id from list_clients or create_client."
    }
  },
  "required": [
    "client_id"
  ],
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🟢list_projects(limit, cursor, status, client_id, team_id)

Use to browse websites. Pass client_id to list one client's sites (team is inferred). Without client_id, Operator tokens (all workspaces) must pass team_id from list_teams. Team-scoped tokens may omit team_id and cannot target another workspace. Do not use to read one project's settings — call get_project. Archived projects are hidden unless status=all or archived.

입력 스키마

{
  "type": "object",
  "properties": {
    "limit": {
      "description": "Page size, max 100.",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 100
    },
    "cursor": {
      "type": "string",
      "minLength": 1,
      "description": "Opaque cursor from the previous page's next_cursor."
    },
    "status": {
      "type": "string",
      "enum": [
        "active",
        "archived",
        "all"
      ],
      "description": "Filter by archive state. Defaults to active."
    },
    "client_id": {
      "description": "Only projects under this client.",
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "team_id": {
      "description": "Team id from list_teams. Required for operator tokens and OAuth logins; team-scoped frw_ tokens may omit it.",
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    }
  },
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🟢get_project(project_id)

Use to inspect one website's settings, domains, webhook URL, email templates, upload policy, plan, usage this month, limits, retention_days, and branding. Returns stored values plus resolved.email. Secrets are never returned. Plans are capacity, not a feature ladder. Free: one workspace, 100 clean submissions/month pooled on the team, 3 active (non-archived) projects, 30-day submission retention, and email.footer_mode / branding.mode stay furrow. Yearly ($199/year): extra workspaces, 10,000 submissions/month, unlimited projects, 730-day retention, branding.mode furrow | off | agency. Expansion packs are $99/year each and add 5,000 submissions/month on a yearly account. MCP, webhooks, snippets, inheritance, Turnstile, and email templates stay available on free. Spam, honeypot, and rejected posts do not count. Use create_upgrade_link to hand a human a Stripe Checkout (or Customer Portal) URL. Pass product=expansion on a yearly team to add a pack. Do not use to list forms — call list_forms.

입력 스키마

{
  "type": "object",
  "properties": {
    "project_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
      "description": "Project (website) id from list_projects, create_project, or bootstrap_site."
    }
  },
  "required": [
    "project_id"
  ],
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🟢list_forms(limit, cursor, status, project_id, client_id, ...)

Use to list forms. Pass project_id for one website or client_id for one company (team is inferred). Otherwise Operator tokens (all workspaces) must pass team_id from list_teams. Team-scoped tokens may omit team_id and cannot target another workspace. Do not use to read resolved inheritance — call get_form.

입력 스키마

{
  "type": "object",
  "properties": {
    "limit": {
      "description": "Page size, max 100.",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 100
    },
    "cursor": {
      "type": "string",
      "minLength": 1,
      "description": "Opaque cursor from the previous page's next_cursor."
    },
    "status": {
      "type": "string",
      "enum": [
        "active",
        "archived",
        "all"
      ],
      "description": "Filter by archive state. Defaults to active."
    },
    "project_id": {
      "description": "Only forms in this project.",
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "client_id": {
      "description": "Only forms under this client's projects.",
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "team_id": {
      "description": "Team id from list_teams. Required for operator tokens and OAuth logins; team-scoped frw_ tokens may omit it.",
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    }
  },
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🟢get_form(form_id)

Use to read a form plus resolved and inherited settings, including resolved.email and resolved.uploads. files_url is the private inbox folder for this form under the project inbox (do not put it in a public snippet). Turnstile always inherits from the project (inherited.turnstile is true). Form webhook_url_override is nullable; null means inherit the project webhook. Forms may override email.subject and email.intro only. Do not use to generate embed code — call get_snippet.

입력 스키마

{
  "type": "object",
  "properties": {
    "form_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
      "description": "Form id from list_forms, create_form, or bootstrap_site."
    }
  },
  "required": [
    "form_id"
  ],
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🟢list_submissions(project_id, form_id, spam, since, limit, ...)

Use to inspect recent posts. Available on free and yearly. Pass project_id or form_id to infer the team; omit both and Operator tokens (all workspaces) must pass team_id from list_teams. Team-scoped tokens may omit team_id and cannot target another workspace. Free teams prune submissions after 30 days; yearly after 730 days. Do not use to read a single submission — call get_submission.

입력 스키마

{
  "type": "object",
  "properties": {
    "project_id": {
      "description": "Only submissions in this project.",
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "form_id": {
      "description": "Only submissions to this form.",
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "spam": {
      "default": "clean",
      "type": "string",
      "enum": [
        "clean",
        "spam",
        "unknown",
        "all"
      ],
      "description": "Which submissions to return. Defaults to clean."
    },
    "since": {
      "type": "string",
      "minLength": 1,
      "description": "ISO 8601 timestamp; only submissions created after this instant."
    },
    "limit": {
      "description": "Page size, max 100.",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 100
    },
    "cursor": {
      "type": "string",
      "minLength": 1,
      "description": "Opaque cursor from the previous page's next_cursor."
    },
    "team_id": {
      "description": "Team id from list_teams. Required for operator tokens and OAuth logins; team-scoped frw_ tokens may omit it.",
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    }
  },
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🟢get_submission(submission_id)

Use to read one stored submission by id, including file metadata and files_url (the form inbox). Do not use this for live submit tests — POST to /s/:publicKey instead. Do not expect signed bucket URLs — download from the inbox.

입력 스키마

{
  "type": "object",
  "properties": {
    "submission_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
      "description": "Submission id from list_submissions or a webhook payload."
    }
  },
  "required": [
    "submission_id"
  ],
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🟢get_snippet(form_id, framework)

Use after the form exists to get copy-paste html, astro, or next code that posts to /s/:publicKey. Do not invent a submit URL — this tool returns it. Snippets are single-value inputs; write checkbox groups and multi-selects yourself. Checkbox groups, select multiple, and other multi-value HTML fields must use a [] suffix on the name (name="services[]") or only the last checked value is stored. Put services[] on field_contract.name too. JSON/fetch should send a real array without brackets ({ "services": ["a", "b"] }) and must not use Object.fromEntries(formData) — use formData.getAll("services"). Single checkbox, select, or combobox: no brackets. File inputs use multipart POST (name="resume[]" for multiple). Enable uploads on the project first. Downloads are the project inbox at files_url, with the form slug as a folder — never put that URL in a public snippet. After dropping the snippet, add visitor-facing success UI: a thank-you page plus default_redirect_url for classic HTML POST, or an on-page message when using fetch/{ok:true}. Do not leave visitors looking at JSON. See furrow://docs/submit-success.

입력 스키마

{
  "type": "object",
  "properties": {
    "form_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
      "description": "Form id from list_forms, create_form, or bootstrap_site."
    },
    "framework": {
      "type": "string",
      "enum": [
        "html",
        "astro",
        "next"
      ],
      "description": "Snippet flavor: html (plain form), astro (component), or next (React client component)."
    }
  },
  "required": [
    "form_id",
    "framework"
  ],
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🟡create_project(name, slug, allowed_domains, turnstile_site_key, turnstile_secret, ...)

Use to add one website under an existing client_id. Idempotent on slug unless strict is true. Free accounts are limited to 3 active (non-archived) projects; yearly is unlimited. If create_project returns plan_limit, call create_upgrade_link and hand the human the Stripe URL. Prefer bootstrap_site when the client does not exist yet. Optional email sets notification templates (subject, from_name, intro, footer_mode, include_meta). Free accounts cannot set footer_mode off. Set uploads_enabled on the project to accept files. Email templates support {{project.name}}, {{project.slug}}, {{form.name}}, {{form.slug}}, {{count}} (1-based clean submissions on this form, including the current one), and any submission field such as {{name}} or {{email}}. Use a fallback with {{name | "New submission"}}. Missing fields render empty and never error. Do not pass HTML; Furrow renders one owned layout. Plans are capacity, not a feature ladder. Free: one workspace, 100 clean submissions/month pooled on the team, 3 active (non-archived) projects, 30-day submission retention, and email.footer_mode / branding.mode stay furrow. Yearly ($199/year): extra workspaces, 10,000 submissions/month, unlimited projects, 730-day retention, branding.mode furrow | off | agency. Expansion packs are $99/year each and add 5,000 submissions/month on a yearly account. MCP, webhooks, snippets, inheritance, Turnstile, and email templates stay available on free. Spam, honeypot, and rejected posts do not count. Use create_upgrade_link to hand a human a Stripe Checkout (or Customer Portal) URL. Pass product=expansion on a yearly team to add a pack. File inputs use multipart POST (name="resume[]" for multiple). Enable uploads on the project first. Downloads are the project inbox at files_url, with the form slug as a folder — never put that URL in a public snippet. Do not put Turnstile keys on a form.

입력 스키마

{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 120,
      "description": "Project (website) display name."
    },
    "slug": {
      "type": "string",
      "minLength": 1,
      "maxLength": 80,
      "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
      "description": "Lowercase hyphenated identifier, unique per client. Used for idempotent re-runs."
    },
    "allowed_domains": {
      "description": "Hostnames allowed to post to this project's forms (example.com, www.example.com). Empty accepts any origin.",
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 253
      }
    },
    "turnstile_site_key": {
      "description": "Cloudflare Turnstile site key for the frontend widget. Null removes it.",
      "anyOf": [
        {
          "type": "string",
          "minLength": 1
        },
        {
          "type": "null"
        }
      ]
    },
    "turnstile_secret": {
      "description": "Cloudflare Turnstile secret used to verify tokens server-side. Null removes it.",
      "anyOf": [
        {
          "type": "string",
          "minLength": 1
        },
        {
          "type": "null"
        }
      ]
    },
    "notify_emails": {
      "default": [],
      "description": "Addresses that receive each clean submission (max 10). Inherited by every form.",
      "maxItems": 10,
      "type": "array",
      "items": {
        "type": "string",
        "format": "email",
        "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
      }
    },
    "from_name": {
      "description": "Legacy From display name. Prefer email.from_name.",
      "anyOf": [
        {
          "type": "string",
          "minLength": 1,
          "maxLength": 120
        },
        {
          "type": "null"
        }
      ]
    },
    "email": {
      "description": "Notification email template for the project: subject, from_name, intro, footer_mode, include_meta.",
      "type": "object",
      "properties": {
        "subject": {
          "type": "string",
          "minLength": 1,
          "maxLength": 300,
          "description": "Notification email subject. Supports {{tokens}} such as {{form.name}} or {{name}}."
        },
        "from_name": {
          "type": "string",
          "minLength": 1,
          "maxLength": 120,
          "description": "Display name on the From line of notification emails."
        },
        "intro": {
          "anyOf": [
            {
              "type": "string",
              "maxLength": 4000
            },
            {
              "type": "null"
            }
          ],
          "description": "Plain-text paragraph above the submitted fields. Supports {{tokens}}. Null removes it."
        },
        "footer_mode": {
          "type": "string",
          "enum": [
            "furrow",
            "minimal",
            "off"
          ],
          "description": "furrow shows the Furrow footer, minimal shows a one-line footer, off hides it (yearly plans only)."
        },
        "include_meta": {
          "type": "boolean",
          "description": "Include submission metadata (IP, user agent, referrer, timestamp) below the fields."
        }
      }
    },
    "webhook_url": {
      "description": "Public https endpoint that receives a signed JSON POST per clean submission. Null disables it.",
      "anyOf": [
        {
          "type": "string",
          "format": "uri"
        },
        {
          "type": "null"
        }
      ]
    },
    "webhook_secret": {
      "description": "HMAC-SHA256 secret for the X-Furrow-Signature header. Omit to have one generated and returned.",
      "anyOf": [
        {
          "type": "string",
          "minLength": 1
        },
        {
          "type": "null"
        }
      ]
    },
    "honeypot_field": {
      "description": "Name of the hidden honeypot input the snippet emits. Defaults to _gotcha.",
      "type": "string",
      "minLength": 1,
      "maxLength": 80
    },
    "require_turnstile": {
      "description": "Reject submissions that lack a valid Turnstile token.",
      "type": "boolean"
    },
    "rate_limit_per_ip": {
      "description": "Max submissions per IP within rate_limit_window_s.",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 1000
    },
    "rate_limit_window_s": {
      "description": "Rate-limit window in seconds (max 86400).",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 86400
    },
    "default_redirect_url": {
      "description": "Where classic HTML posts redirect after success. Null returns JSON/inline success instead.",
      "anyOf": [
        {
          "type": "string",
          "format": "uri"
        },
        {
          "type": "null"
        }
      ]
    },
    "uploads_enabled": {
      "description": "Accept multipart file fields on this project's forms.",
      "type": "boolean"
    },
    "upload_allowed_types": {
      "description": "Allowed upload MIME types, e.g. application/pdf or image/*.",
      "minItems": 1,
      "maxItems": 20,
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 3,
        "maxLength": 80,
        "pattern": "^[a-z0-9.+-]+\\/[a-z0-9.+*-]+$"
      }
    },
    "upload_max_files": {
      "description": "Max files per submission (up to 20).",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 20
    },
    "upload_max_file_bytes": {
      "description": "Max bytes per file (up to 25 MB).",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 26214400
    },
    "upload_max_total_bytes": {
      "description": "Max total upload bytes per submission (up to 50 MB).",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 52428800
    },
    "strict": {
      "default": false,
      "description": "When true, fail if the slug already exists instead of returning the existing record.",
      "type": "boolean"
    },
    "client_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
      "description": "Client id from list_clients or create_client."
    }
  },
  "required": [
    "name",
    "slug",
    "client_id"
  ],
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🔴update_project(name, allowed_domains, turnstile_site_key, turnstile_secret, notify_emails, ...)

Use to patch website settings. Nested email sets notification templates: email.subject, email.from_name, email.intro, email.footer_mode (furrow | minimal | off), email.include_meta. Optional branding updates the team (mode furrow | off | agency on yearly only; ignored on free except furrow). Set uploads_enabled and upload_allowed_types here — forms inherit them. Email templates support {{project.name}}, {{project.slug}}, {{form.name}}, {{form.slug}}, {{count}} (1-based clean submissions on this form, including the current one), and any submission field such as {{name}} or {{email}}. Use a fallback with {{name | "New submission"}}. Missing fields render empty and never error. Do not pass HTML; Furrow renders one owned layout. Plans are capacity, not a feature ladder. Free: one workspace, 100 clean submissions/month pooled on the team, 3 active (non-archived) projects, 30-day submission retention, and email.footer_mode / branding.mode stay furrow. Yearly ($199/year): extra workspaces, 10,000 submissions/month, unlimited projects, 730-day retention, branding.mode furrow | off | agency. Expansion packs are $99/year each and add 5,000 submissions/month on a yearly account. MCP, webhooks, snippets, inheritance, Turnstile, and email templates stay available on free. Spam, honeypot, and rejected posts do not count. Use create_upgrade_link to hand a human a Stripe Checkout (or Customer Portal) URL. Pass product=expansion on a yearly team to add a pack. File inputs use multipart POST (name="resume[]" for multiple). Enable uploads on the project first. Downloads are the project inbox at files_url, with the form slug as a folder — never put that URL in a public snippet. Null Turnstile or webhook fields clear them. Do not use this to create forms. Updating project keys changes resolved settings on existing forms unless a form override is set.

입력 스키마

{
  "type": "object",
  "properties": {
    "name": {
      "description": "New project display name.",
      "type": "string",
      "minLength": 1,
      "maxLength": 120
    },
    "allowed_domains": {
      "description": "Replace the allowed hostnames. Empty array accepts any origin.",
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 253
      }
    },
    "turnstile_site_key": {
      "description": "Cloudflare Turnstile site key for the frontend widget. Null removes it.",
      "anyOf": [
        {
          "type": "string",
          "minLength": 1
        },
        {
          "type": "null"
        }
      ]
    },
    "turnstile_secret": {
      "description": "Cloudflare Turnstile secret used to verify tokens server-side. Null removes it.",
      "anyOf": [
        {
          "type": "string",
          "minLength": 1
        },
        {
          "type": "null"
        }
      ]
    },
    "notify_emails": {
      "description": "Replace the notification recipients (max 10).",
      "maxItems": 10,
      "type": "array",
      "items": {
        "type": "string",
        "format": "email",
        "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
      }
    },
    "from_name": {
      "description": "Legacy From display name. Prefer email.from_name.",
      "anyOf": [
        {
          "type": "string",
          "minLength": 1,
          "maxLength": 120
        },
        {
          "type": "null"
        }
      ]
    },
    "email": {
      "description": "Notification email template for the project: subject, from_name, intro, footer_mode, include_meta.",
      "type": "object",
      "properties": {
        "subject": {
          "type": "string",
          "minLength": 1,
          "maxLength": 300,
          "description": "Notification email subject. Supports {{tokens}} such as {{form.name}} or {{name}}."
        },
        "from_name": {
          "type": "string",
          "minLength": 1,
          "maxLength": 120,
          "description": "Display name on the From line of notification emails."
        },
        "intro": {
          "anyOf": [
            {
              "type": "string",
              "maxLength": 4000
            },
            {
              "type": "null"
            }
          ],
          "description": "Plain-text paragraph above the submitted fields. Supports {{tokens}}. Null removes it."
        },
        "footer_mode": {
          "type": "string",
          "enum": [
            "furrow",
            "minimal",
            "off"
          ],
          "description": "furrow shows the Furrow footer, minimal shows a one-line footer, off hides it (yearly plans only)."
        },
        "include_meta": {
          "type": "boolean",
          "description": "Include submission metadata (IP, user agent, referrer, timestamp) below the fields."
        }
      }
    },
    "webhook_url": {
      "description": "Public https endpoint that receives a signed JSON POST per clean submission. Null disables it.",
      "anyOf": [
        {
          "type": "string",
          "format": "uri"
        },
        {
          "type": "null"
        }
      ]
    },
    "webhook_secret": {
      "description": "HMAC-SHA256 secret for the X-Furrow-Signature header. Omit to have one generated and returned.",
      "anyOf": [
        {
          "type": "string",
          "minLength": 1
        },
        {
          "type": "null"
        }
      ]
    },
    "honeypot_field": {
      "description": "Name of the hidden honeypot input the snippet emits. Defaults to _gotcha.",
      "type": "string",
      "minLength": 1,
      "maxLength": 80
    },
    "require_turnstile": {
      "description": "Reject submissions that lack a valid Turnstile token.",
      "type": "boolean"
    },
    "rate_limit_per_ip": {
      "description": "Max submissions per IP within rate_limit_window_s.",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 1000
    },
    "rate_limit_window_s": {
      "description": "Rate-limit window in seconds (max 86400).",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 86400
    },
    "default_redirect_url": {
      "description": "Where classic HTML posts redirect after success. Null returns JSON/inline success instead.",
      "anyOf": [
        {
          "type": "string",
          "format": "uri"
        },
        {
          "type": "null"
        }
      ]
    },
    "branding": {
      "description": "Per-project branding override for notification emails and hosted pages.",
      "type": "object",
      "properties": {
        "mode": {
          "description": "furrow shows Furrow branding, off hides it, agency shows your own name/link/logo. Only yearly plans may change this.",
          "type": "string",
          "enum": [
            "furrow",
            "off",
            "agency"
          ]
        },
        "agency_name": {
          "description": "Agency name shown when mode is agency. Null clears it.",
          "anyOf": [
            {
              "type": "string",
              "minLength": 1,
              "maxLength": 120
            },
            {
              "type": "null"
            }
          ]
        },
        "agency_url": {
          "description": "Optional http(s) link for the agency name when mode is agency. Null clears it.",
          "anyOf": [
            {
              "type": "string",
              "format": "uri"
            },
            {
              "type": "null"
            }
          ]
        },
        "logo_url": {
          "description": "Optional http(s) logo image URL when mode is agency. Null clears it.",
          "anyOf": [
            {
              "type": "string",
              "format": "uri"
            },
            {
              "type": "null"
            }
          ]
        }
      }
    },
    "uploads_enabled": {
      "description": "Accept multipart file fields on this project's forms.",
      "type": "boolean"
    },
    "upload_allowed_types": {
      "description": "Allowed upload MIME types, e.g. application/pdf or image/*.",
      "minItems": 1,
      "maxItems": 20,
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 3,
        "maxLength": 80,
        "pattern": "^[a-z0-9.+-]+\\/[a-z0-9.+*-]+$"
      }
    },
    "upload_max_files": {
      "description": "Max files per submission (up to 20).",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 20
    },
    "upload_max_file_bytes": {
      "description": "Max bytes per file (up to 25 MB).",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 26214400
    },
    "upload_max_total_bytes": {
      "description": "Max total upload bytes per submission (up to 50 MB).",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 52428800
    },
    "project_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
      "description": "Project (website) id from list_projects, create_project, or bootstrap_site."
    }
  },
  "required": [
    "project_id"
  ],
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🔴rotate_project_inbox(project_id)

Use when a project's private inbox URL may have leaked. Issues a new inbox key; every previously shared inbox link stops working immediately. get_project returns the new files_url.

입력 스키마

{
  "type": "object",
  "properties": {
    "project_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
      "description": "Project (website) id from list_projects, create_project, or bootstrap_site."
    }
  },
  "required": [
    "project_id"
  ],
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🔴archive_project(project_id)

Use to archive a website. Archived projects reject public submit. Do not use this as a hard delete. There is no unarchive tool in v1.

입력 스키마

{
  "type": "object",
  "properties": {
    "project_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
      "description": "Project (website) id from list_projects, create_project, or bootstrap_site."
    }
  },
  "required": [
    "project_id"
  ],
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🟡create_form(name, slug, notify_emails_override, webhook_url_override, webhook_secret_override, ...)

Use to add a form under an existing project_id. Idempotent on slug unless strict is true. Overrides are nullable; omit them to inherit project notify emails, webhook, redirect, require_turnstile, and email templates. Pass email.subject / email.intro to override notification copy for this form only. Pass webhook_url_override to send this form to a different URL. Field contract labels (label) are used on notification emails. Checkbox groups, select multiple, and other multi-value HTML fields must use a [] suffix on the name (name="services[]") or only the last checked value is stored. Put services[] on field_contract.name too. JSON/fetch should send a real array without brackets ({ "services": ["a", "b"] }) and must not use Object.fromEntries(formData) — use formData.getAll("services"). Single checkbox, select, or combobox: no brackets. File inputs use multipart POST (name="resume[]" for multiple). Enable uploads on the project first. Downloads are the project inbox at files_url, with the form slug as a folder — never put that URL in a public snippet.

입력 스키마

{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 120,
      "description": "Form display name, e.g. Contact or Quote request."
    },
    "slug": {
      "type": "string",
      "minLength": 1,
      "maxLength": 80,
      "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
      "description": "Lowercase hyphenated identifier, unique per project. Used for idempotent re-runs."
    },
    "notify_emails_override": {
      "description": "Recipients for this form only. Null inherits the project's notify_emails.",
      "anyOf": [
        {
          "maxItems": 10,
          "type": "array",
          "items": {
            "type": "string",
            "format": "email",
            "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
          }
        },
        {
          "type": "null"
        }
      ]
    },
    "webhook_url_override": {
      "description": "Webhook endpoint for this form only. Null inherits the project webhook.",
      "anyOf": [
        {
          "type": "string",
          "format": "uri"
        },
        {
          "type": "null"
        }
      ]
    },
    "webhook_secret_override": {
      "description": "HMAC secret for this form's webhook. Null inherits the project secret.",
      "anyOf": [
        {
          "type": "string",
          "minLength": 1
        },
        {
          "type": "null"
        }
      ]
    },
    "redirect_url_override": {
      "description": "Success redirect for this form only. Null inherits default_redirect_url.",
      "anyOf": [
        {
          "type": "string",
          "format": "uri"
        },
        {
          "type": "null"
        }
      ]
    },
    "require_turnstile_override": {
      "description": "Force Turnstile on or off for this form. Null inherits the project setting.",
      "anyOf": [
        {
          "type": "boolean"
        },
        {
          "type": "null"
        }
      ]
    },
    "email": {
      "description": "Per-form subject and intro overrides for the notification email.",
      "type": "object",
      "properties": {
        "subject": {
          "description": "Per-form subject override. Supports {{tokens}}. Null falls back to the project subject.",
          "anyOf": [
            {
              "type": "string",
              "minLength": 1,
              "maxLength": 300
            },
            {
              "type": "null"
            }
          ]
        },
        "intro": {
          "description": "Per-form intro override. Supports {{tokens}}. Null falls back to the project intro.",
          "anyOf": [
            {
              "type": "string",
              "maxLength": 4000
            },
            {
              "type": "null"
            }
          ]
        }
      }
    },
    "field_contract": {
      "description": "Declared fields (name, type, label, report). Drives get_snippet and the email layout. Null clears it.",
      "anyOf": [
        {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "minLength": 1,
                "maxLength": 80,
                "description": "Posted field name. HTML checkbox groups / multi-select must end in [] (services[]) or only the last value is stored. JSON arrays omit the brackets."
              },
              "type": {
                "type": "string",
                "minLength": 1,
                "maxLength": 40,
                "description": "text, email, textarea, file, etc. type=file emits a file input in get_snippet."
              },
              "label": {
                "description": "Human label used in the snippet and notification email. Defaults to the field name.",
                "anyOf": [
                  {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "report": {
                "default": false,
                "description": "Include this field in dashboard summaries and reports.",
                "type": "boolean"
              }
            },
            "required": [
              "name",
              "type"
            ]
          }
        },
        {
          "type": "null"
        }
      ]
    },
    "strict": {
      "default": false,
      "description": "When true, fail if the slug already exists instead of returning the existing record.",
      "type": "boolean"
    },
    "project_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
      "description": "Project (website) id from list_projects, create_project, or bootstrap_site."
    }
  },
  "required": [
    "name",
    "slug",
    "project_id"
  ],
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🟡update_form(name, notify_emails_override, webhook_url_override, webhook_secret_override, redirect_url_override, ...)

Use to patch form overrides. Pass null on an override to inherit again. Set email.subject or email.intro to override project notification copy for this form only; from_name, footer_mode, and include_meta stay on the project. Email templates support {{project.name}}, {{project.slug}}, {{form.name}}, {{form.slug}}, {{count}} (1-based clean submissions on this form, including the current one), and any submission field such as {{name}} or {{email}}. Use a fallback with {{name | "New submission"}}. Missing fields render empty and never error. Do not pass HTML; Furrow renders one owned layout. Set webhook_url_override to override the project webhook for this form. Do not use this to change Turnstile keys — those live on the project. Checkbox groups, select multiple, and other multi-value HTML fields must use a [] suffix on the name (name="services[]") or only the last checked value is stored. Put services[] on field_contract.name too. JSON/fetch should send a real array without brackets ({ "services": ["a", "b"] }) and must not use Object.fromEntries(formData) — use formData.getAll("services"). Single checkbox, select, or combobox: no brackets. File inputs use multipart POST (name="resume[]" for multiple). Enable uploads on the project first. Downloads are the project inbox at files_url, with the form slug as a folder — never put that URL in a public snippet.

입력 스키마

{
  "type": "object",
  "properties": {
    "name": {
      "description": "New form display name.",
      "type": "string",
      "minLength": 1,
      "maxLength": 120
    },
    "notify_emails_override": {
      "description": "Recipients for this form only. Null inherits the project's notify_emails.",
      "anyOf": [
        {
          "maxItems": 10,
          "type": "array",
          "items": {
            "type": "string",
            "format": "email",
            "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
          }
        },
        {
          "type": "null"
        }
      ]
    },
    "webhook_url_override": {
      "description": "Webhook endpoint for this form only. Null inherits the project webhook.",
      "anyOf": [
        {
          "type": "string",
          "format": "uri"
        },
        {
          "type": "null"
        }
      ]
    },
    "webhook_secret_override": {
      "description": "HMAC secret for this form's webhook. Null inherits the project secret.",
      "anyOf": [
        {
          "type": "string",
          "minLength": 1
        },
        {
          "type": "null"
        }
      ]
    },
    "redirect_url_override": {
      "description": "Success redirect for this form only. Null inherits default_redirect_url.",
      "anyOf": [
        {
          "type": "string",
          "format": "uri"
        },
        {
          "type": "null"
        }
      ]
    },
    "require_turnstile_override": {
      "description": "Force Turnstile on or off for this form. Null inherits the project setting.",
      "anyOf": [
        {
          "type": "boolean"
        },
        {
          "type": "null"
        }
      ]
    },
    "email": {
      "description": "Per-form subject and intro overrides for the notification email.",
      "type": "object",
      "properties": {
        "subject": {
          "description": "Per-form subject override. Supports {{tokens}}. Null falls back to the project subject.",
          "anyOf": [
            {
              "type": "string",
              "minLength": 1,
              "maxLength": 300
            },
            {
              "type": "null"
            }
          ]
        },
        "intro": {
          "description": "Per-form intro override. Supports {{tokens}}. Null falls back to the project intro.",
          "anyOf": [
            {
              "type": "string",
              "maxLength": 4000
            },
            {
              "type": "null"
            }
          ]
        }
      }
    },
    "field_contract": {
      "description": "Declared fields (name, type, label, report). Drives get_snippet and the email layout. Null clears it.",
      "anyOf": [
        {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "minLength": 1,
                "maxLength": 80,
                "description": "Posted field name. HTML checkbox groups / multi-select must end in [] (services[]) or only the last value is stored. JSON arrays omit the brackets."
              },
              "type": {
                "type": "string",
                "minLength": 1,
                "maxLength": 40,
                "description": "text, email, textarea, file, etc. type=file emits a file input in get_snippet."
              },
              "label": {
                "description": "Human label used in the snippet and notification email. Defaults to the field name.",
                "anyOf": [
                  {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "report": {
                "default": false,
                "description": "Include this field in dashboard summaries and reports.",
                "type": "boolean"
              }
            },
            "required": [
              "name",
              "type"
            ]
          }
        },
        {
          "type": "null"
        }
      ]
    },
    "form_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
      "description": "Form id from list_forms, create_form, or bootstrap_site."
    }
  },
  "required": [
    "form_id"
  ],
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🔴archive_form(form_id)

Use to archive a form so public submit returns 404. Do not use this as a hard delete.

입력 스키마

{
  "type": "object",
  "properties": {
    "form_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
      "description": "Form id from list_forms, create_form, or bootstrap_site."
    }
  },
  "required": [
    "form_id"
  ],
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🟡upsert_project_webhook(webhook_url, webhook_secret, project_id)

Use to set or replace the project webhook URL. Available on free and yearly. A signing secret is generated once if the project has none. Forms inherit this unless webhook_url_override is set. Do not use this for a one-off delivery — call test_webhook.

입력 스키마

{
  "type": "object",
  "properties": {
    "webhook_url": {
      "anyOf": [
        {
          "type": "string",
          "format": "uri"
        },
        {
          "type": "null"
        }
      ],
      "description": "Public https endpoint that receives a signed JSON POST per clean submission. Null disables it."
    },
    "webhook_secret": {
      "description": "HMAC-SHA256 secret for X-Furrow-Signature. Omit to have one generated and returned.",
      "anyOf": [
        {
          "type": "string",
          "minLength": 1
        },
        {
          "type": "null"
        }
      ]
    },
    "project_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
      "description": "Project (website) id from list_projects, create_project, or bootstrap_site."
    }
  },
  "required": [
    "webhook_url",
    "project_id"
  ],
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🟡test_webhook(project_id)

Use to POST a signed webhook.test event to the project's webhook URL. Available on free and yearly. Do not use this to inspect past deliveries — that is REST GET /api/projects/:id/webhook-deliveries. Fails if no webhook URL and secret are set.

입력 스키마

{
  "type": "object",
  "properties": {
    "project_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
      "description": "Project (website) id from list_projects, create_project, or bootstrap_site."
    }
  },
  "required": [
    "project_id"
  ],
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
🟡bootstrap_site(name, slug, project_name, project_slug, allowed_domains, ...)

Use this to stand up a client, one website (project), and its forms in a single call. name/slug are the client; optional project_name/project_slug default to the same. Free accounts cannot create a fourth active project — call create_upgrade_link if bootstrap_site returns plan_limit. Operator tokens (all workspaces) must pass team_id from list_teams. Team-scoped tokens may omit team_id and cannot target another workspace. Idempotent on client, project, and form slugs. Site settings (domains, Turnstile, notify emails, webhook, email templates, uploads) live on the project. Email templates support {{project.name}}, {{project.slug}}, {{form.name}}, {{form.slug}}, {{count}} (1-based clean submissions on this form, including the current one), and any submission field such as {{name}} or {{email}}. Use a fallback with {{name | "New submission"}}. Missing fields render empty and never error. Do not pass HTML; Furrow renders one owned layout. Plans are capacity, not a feature ladder. Free: one workspace, 100 clean submissions/month pooled on the team, 3 active (non-archived) projects, 30-day submission retention, and email.footer_mode / branding.mode stay furrow. Yearly ($199/year): extra workspaces, 10,000 submissions/month, unlimited projects, 730-day retention, branding.mode furrow | off | agency. Expansion packs are $99/year each and add 5,000 submissions/month on a yearly account. MCP, webhooks, snippets, inheritance, Turnstile, and email templates stay available on free. Spam, honeypot, and rejected posts do not count. Use create_upgrade_link to hand a human a Stripe Checkout (or Customer Portal) URL. Pass product=expansion on a yearly team to add a pack. Checkbox groups, select multiple, and other multi-value HTML fields must use a [] suffix on the name (name="services[]") or only the last checked value is stored. Put services[] on field_contract.name too. JSON/fetch should send a real array without brackets ({ "services": ["a", "b"] }) and must not use Object.fromEntries(formData) — use formData.getAll("services"). Single checkbox, select, or combobox: no brackets. File inputs use multipart POST (name="resume[]" for multiple). Enable uploads on the project first. Downloads are the project inbox at files_url, with the form slug as a folder — never put that URL in a public snippet. Do not use when you only need to patch an existing form — call update_form.

입력 스키마

{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 120,
      "description": "Client (company) display name."
    },
    "slug": {
      "type": "string",
      "minLength": 1,
      "maxLength": 80,
      "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
      "description": "Lowercase hyphenated identifier, unique per team. Used for idempotent re-runs."
    },
    "project_name": {
      "description": "Website display name. Defaults to the client name.",
      "type": "string",
      "minLength": 1,
      "maxLength": 120
    },
    "project_slug": {
      "description": "Website slug. Defaults to the client slug.",
      "type": "string",
      "minLength": 1,
      "maxLength": 80,
      "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
    },
    "allowed_domains": {
      "description": "Hostnames allowed to post to this project's forms (example.com, www.example.com). Empty accepts any origin.",
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 253
      }
    },
    "turnstile_site_key": {
      "description": "Cloudflare Turnstile site key for the frontend widget. Null removes it.",
      "anyOf": [
        {
          "type": "string",
          "minLength": 1
        },
        {
          "type": "null"
        }
      ]
    },
    "turnstile_secret": {
      "description": "Cloudflare Turnstile secret used to verify tokens server-side. Null removes it.",
      "anyOf": [
        {
          "type": "string",
          "minLength": 1
        },
        {
          "type": "null"
        }
      ]
    },
    "notify_emails": {
      "default": [],
      "description": "Addresses that receive each clean submission (max 10). Inherited by every form.",
      "maxItems": 10,
      "type": "array",
      "items": {
        "type": "string",
        "format": "email",
        "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
      }
    },
    "from_name": {
      "description": "Legacy From display name. Prefer email.from_name.",
      "anyOf": [
        {
          "type": "string",
          "minLength": 1,
          "maxLength": 120
        },
        {
          "type": "null"
        }
      ]
    },
    "email": {
      "description": "Notification email template for the project: subject, from_name, intro, footer_mode, include_meta.",
      "type": "object",
      "properties": {
        "subject": {
          "type": "string",
          "minLength": 1,
          "maxLength": 300,
          "description": "Notification email subject. Supports {{tokens}} such as {{form.name}} or {{name}}."
        },
        "from_name": {
          "type": "string",
          "minLength": 1,
          "maxLength": 120,
          "description": "Display name on the From line of notification emails."
        },
        "intro": {
          "anyOf": [
            {
              "type": "string",
              "maxLength": 4000
            },
            {
              "type": "null"
            }
          ],
          "description": "Plain-text paragraph above the submitted fields. Supports {{tokens}}. Null removes it."
        },
        "footer_mode": {
          "type": "string",
          "enum": [
            "furrow",
            "minimal",
            "off"
          ],
          "description": "furrow shows the Furrow footer, minimal shows a one-line footer, off hides it (yearly plans only)."
        },
        "include_meta": {
          "type": "boolean",
          "description": "Include submission metadata (IP, user agent, referrer, timestamp) below the fields."
        }
      }
    },
    "webhook_url": {
      "description": "Public https endpoint that receives a signed JSON POST per clean submission. Null disables it.",
      "anyOf": [
        {
          "type": "string",
          "format": "uri"
        },
        {
          "type": "null"
        }
      ]
    },
    "webhook_secret": {
      "description": "HMAC-SHA256 secret for the X-Furrow-Signature header. Omit to have one generated and returned.",
      "anyOf": [
        {
          "type": "string",
          "minLength": 1
        },
        {
          "type": "null"
        }
      ]
    },
    "honeypot_field": {
      "description": "Name of the hidden honeypot input the snippet emits. Defaults to _gotcha.",
      "type": "string",
      "minLength": 1,
      "maxLength": 80
    },
    "require_turnstile": {
      "description": "Reject submissions that lack a valid Turnstile token.",
      "type": "boolean"
    },
    "rate_limit_per_ip": {
      "description": "Max submissions per IP within rate_limit_window_s.",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 1000
    },
    "rate_limit_window_s": {
      "description": "Rate-limit window in seconds (max 86400).",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 86400
    },
    "default_redirect_url": {
      "description": "Where classic HTML posts redirect after success. Null returns JSON/inline success instead.",
      "anyOf": [
        {
          "type": "string",
          "format": "uri"
        },
        {
          "type": "null"
        }
      ]
    },
    "uploads_enabled": {
      "description": "Accept multipart file fields on this project's forms.",
      "type": "boolean"
    },
    "upload_allowed_types": {
      "description": "Allowed upload MIME types, e.g. application/pdf or image/*.",
      "minItems": 1,
      "maxItems": 20,
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 3,
        "maxLength": 80,
        "pattern": "^[a-z0-9.+-]+\\/[a-z0-9.+*-]+$"
      }
    },
    "upload_max_files": {
      "description": "Max files per submission (up to 20).",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 20
    },
    "upload_max_file_bytes": {
      "description": "Max bytes per file (up to 25 MB).",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 26214400
    },
    "upload_max_total_bytes": {
      "description": "Max total upload bytes per submission (up to 50 MB).",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 52428800
    },
    "forms": {
      "default": [],
      "description": "Forms to create or update on the site, matched by slug. Re-running is safe.",
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120,
            "description": "Form display name, e.g. Contact or Quote request."
          },
          "slug": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80,
            "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
            "description": "Lowercase hyphenated identifier, unique per project. Used for idempotent re-runs."
          },
          "notify_emails_override": {
            "description": "Recipients for this form only. Null inherits the project's notify_emails.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "email",
                  "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "webhook_url_override": {
            "description": "Webhook endpoint for this form only. Null inherits the project webhook.",
            "anyOf": [
              {
                "type": "string",
                "format": "uri"
              },
              {
                "type": "null"
              }
            ]
          },
          "webhook_secret_override": {
            "description": "HMAC secret for this form's webhook. Null inherits the project secret.",
            "anyOf": [
              {
                "type": "string",
                "minLength": 1
              },
              {
                "type": "null"
              }
            ]
          },
          "redirect_url_override": {
            "description": "Success redirect for this form only. Null inherits default_redirect_url.",
            "anyOf": [
              {
                "type": "string",
                "format": "uri"
              },
              {
                "type": "null"
              }
            ]
          },
          "require_turnstile_override": {
            "description": "Force Turnstile on or off for this form. Null inherits the project setting.",
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ]
          },
          "email": {
            "description": "Per-form subject and intro overrides for the notification email.",
            "type": "object",
            "properties": {
              "subject": {
                "description": "Per-form subject override. Supports {{tokens}}. Null falls back to the project subject.",
                "anyOf": [
                  {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 300
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "intro": {
                "description": "Per-form intro override. Supports {{tokens}}. Null falls back to the project intro.",
                "anyOf": [
                  {
                    "type": "string",
                    "maxLength": 4000
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          },
          "field_contract": {
            "description": "Declared fields (name, type, label, report). Drives get_snippet and the email layout. Null clears it.",
            "anyOf": [
              {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 80,
                      "description": "Posted field name. HTML checkbox groups / multi-select must end in [] (services[]) or only the last value is stored. JSON arrays omit the brackets."
                    },
                    "type": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 40,
                      "description": "text, email, textarea, file, etc. type=file emits a file input in get_snippet."
                    },
                    "label": {
                      "description": "Human label used in the snippet and notification email. Defaults to the field name.",
                      "anyOf": [
                        {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 120
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "report": {
                      "default": false,
                      "description": "Include this field in dashboard summaries and reports.",
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "name",
                    "type"
                  ]
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "fields": {
            "description": "Shorthand for field_contract: the fields this form posts.",
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 80,
                  "description": "Posted field name. HTML checkbox groups / multi-select must end in [] (services[]) or only the last value is stored. JSON arrays omit the brackets."
                },
                "type": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 40,
                  "description": "text, email, textarea, file, etc. type=file emits a file input in get_snippet."
                },
                "label": {
                  "description": "Human label used in the snippet and notification email. Defaults to the field name.",
                  "anyOf": [
                    {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 120
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "report": {
                  "default": false,
                  "description": "Include this field in dashboard summaries and reports.",
                  "type": "boolean"
                }
              },
              "required": [
                "name",
                "type"
              ]
            }
          }
        },
        "required": [
          "name",
          "slug"
        ]
      }
    },
    "team_id": {
      "description": "Team id from list_teams. Required for operator tokens and OAuth logins; team-scoped frw_ tokens may omit it.",
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    }
  },
  "required": [
    "name",
    "slug"
  ],
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}

커뮤니티

이 서버 평가하기

증거

최근 관측

검증됨버전이 기록되지 않음도구 27개
검증됨버전이 기록되지 않음도구 27개