{
  "openapi": "3.1.0",
  "info": {
    "title": "FormParcel form address",
    "version": "1",
    "summary": "The public address a website form posts leads to.",
    "description": "Post a lead from the visitor's browser: a plain HTML form, or fetch on the page. The page loads the bot check script, which adds a proof to each submission.\n\nA post from a server, a webhook or curl carries no proof: it gets the same 200 and the lead is held in the owner's Spam folder. Test with \"Send a test lead\" in the dashboard.\n\nA 200 means Accepted (stored), not Delivered. Never resend a lead whose answer said \"pending\".\n\nDocs: https://formparcel.live/docs/reference/endpoint"
  },
  "externalDocs": {
    "description": "FormParcel docs",
    "url": "https://formparcel.live/docs"
  },
  "servers": [
    {
      "url": "https://f.formparcel.live"
    }
  ],
  "paths": {
    "/f/{formId}": {
      "parameters": [
        {
          "name": "formId",
          "in": "path",
          "required": true,
          "description": "The form's ID from the dashboard. Random, at least 128 bits. Public by design: it is in the page's HTML.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "operationId": "sendLead",
        "summary": "Send a lead",
        "description": "No API key. A browser form post (Accept: text/html, Sec-Fetch-Mode navigate) gets a page or a 303; everything else gets JSON.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "description": "Your form's fields, by name. Text only: at most 50 fields, names up to 64 characters, values up to 4096 characters, 16 KB in all. Files, and fields named like passwords, one-time codes or card details, are refused.",
                "maxProperties": 50,
                "propertyNames": {
                  "maxLength": 64
                },
                "additionalProperties": {
                  "type": "string",
                  "maxLength": 4096
                },
                "properties": {
                  "_fp_proof": {
                    "type": "string",
                    "description": "The bot-check proof from /f/formparcel.js (FormParcel.proof(address) for fetch). Without a valid one the lead is held in the Spam folder, and the answer is the same 200."
                  },
                  "_gotcha": {
                    "type": "string",
                    "description": "The spam trap. Keep it empty. When filled, a script gets 204 and nothing is stored."
                  },
                  "_next": {
                    "type": "string",
                    "format": "uri",
                    "description": "A thank-you page on the site's own domain. A browser form post is redirected there (303). Ignored for JSON callers."
                  },
                  "cf-turnstile-response": {
                    "type": "string",
                    "description": "The Cloudflare Turnstile token, when the site has Turnstile on."
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "description": "Your form's fields, by name. Text only: at most 50 fields, names up to 64 characters, values up to 4096 characters, 16 KB in all. Files, and fields named like passwords, one-time codes or card details, are refused.",
                "maxProperties": 50,
                "propertyNames": {
                  "maxLength": 64
                },
                "additionalProperties": {
                  "type": "string",
                  "maxLength": 4096
                },
                "properties": {
                  "_fp_proof": {
                    "type": "string",
                    "description": "The bot-check proof from /f/formparcel.js (FormParcel.proof(address) for fetch). Without a valid one the lead is held in the Spam folder, and the answer is the same 200."
                  },
                  "_gotcha": {
                    "type": "string",
                    "description": "The spam trap. Keep it empty. When filled, a script gets 204 and nothing is stored."
                  },
                  "_next": {
                    "type": "string",
                    "format": "uri",
                    "description": "A thank-you page on the site's own domain. A browser form post is redirected there (303). Ignored for JSON callers."
                  },
                  "cf-turnstile-response": {
                    "type": "string",
                    "description": "The Cloudflare Turnstile token, when the site has Turnstile on."
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Your form's fields, by name. Text only: at most 50 fields, names up to 64 characters, values up to 4096 characters, 16 KB in all. Files, and fields named like passwords, one-time codes or card details, are refused.",
                "maxProperties": 50,
                "propertyNames": {
                  "maxLength": 64
                },
                "additionalProperties": {
                  "type": "string",
                  "maxLength": 4096
                },
                "properties": {
                  "_fp_proof": {
                    "type": "string",
                    "description": "The bot-check proof from /f/formparcel.js (FormParcel.proof(address) for fetch). Without a valid one the lead is held in the Spam folder, and the answer is the same 200."
                  },
                  "_gotcha": {
                    "type": "string",
                    "description": "The spam trap. Keep it empty. When filled, a script gets 204 and nothing is stored."
                  },
                  "_next": {
                    "type": "string",
                    "format": "uri",
                    "description": "A thank-you page on the site's own domain. A browser form post is redirected there (303). Ignored for JSON callers."
                  },
                  "cf-turnstile-response": {
                    "type": "string",
                    "description": "The Cloudflare Turnstile token, when the site has Turnstile on."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted: the lead is stored and has a receipt. Delivery is separate. A post without the bot check gets this too, and waits in the Spam folder. A browser form post gets a short page headed \"Your message was sent\".",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Accepted"
                }
              },
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "204": {
            "description": "The spam trap was filled (a script post). Nothing is stored. A proof sent twice is not dropped: it is held in the Spam folder and answered 200."
          },
          "303": {
            "description": "A browser form post with a trusted _next: the lead is accepted and the visitor is sent to that page.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "400": {
            "description": "Refused. Nothing is stored and there is no receipt. A browser form post gets a short page instead of JSON.\n- INVALID_JSON: The JSON cannot be parsed\n- INVALID_BODY: The body is malformed, or the JSON is not a key-value object\n- DUPLICATE_FIELD: A top-level JSON key appears twice\n- TOO_MANY_FIELDS: More than 50 fields\n- FIELD_NAME_TOO_LONG: A field name is over 64 characters\n- FIELD_VALUE_TOO_LONG: A field value is over 4,096 characters\n- PAYLOAD_TOO_DEEP: JSON nested deeper than 4 levels\n- NON_FINITE_NUMBER: A number that is not finite, such as 1e400\n- FILE_UPLOAD_NOT_SUPPORTED: A multipart body contains a file\n- EMPTY_SUBMISSION: No field holds readable content\n- No code (Form not ready): The form has no enabled destination yet\nEvery refusal: https://formparcel.live/docs/reference/responses#codes",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Refusal"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "INVALID_JSON",
                            "INVALID_BODY",
                            "DUPLICATE_FIELD",
                            "TOO_MANY_FIELDS",
                            "FIELD_NAME_TOO_LONG",
                            "FIELD_VALUE_TOO_LONG",
                            "PAYLOAD_TOO_DEEP",
                            "NON_FINITE_NUMBER",
                            "FILE_UPLOAD_NOT_SUPPORTED",
                            "EMPTY_SUBMISSION"
                          ]
                        }
                      }
                    }
                  ]
                }
              },
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Nothing is stored and there is no receipt. A browser form post gets a short page instead of JSON.\n- No code (Form paused): The form is paused\n- No code (Origin not allowed): The page is not on the form's allowed origins\n- SPAM_CHECK_FAILED: Turnstile is on and the token did not pass\nEvery refusal: https://formparcel.live/docs/reference/responses#codes",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Refusal"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "SPAM_CHECK_FAILED"
                          ]
                        }
                      }
                    }
                  ]
                }
              },
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Refused. Nothing is stored and there is no receipt. A browser form post gets a short page instead of JSON.\n- No code (Form not found): No form has this ID\nEvery refusal: https://formparcel.live/docs/reference/responses#codes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              },
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "413": {
            "description": "Refused. Nothing is stored and there is no receipt. A browser form post gets a short page instead of JSON.\n- PAYLOAD_TOO_LARGE: The body is over 16 KB\nEvery refusal: https://formparcel.live/docs/reference/responses#codes",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Refusal"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "PAYLOAD_TOO_LARGE"
                          ]
                        }
                      }
                    }
                  ]
                }
              },
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "415": {
            "description": "Refused. Nothing is stored and there is no receipt. A browser form post gets a short page instead of JSON.\n- UNSUPPORTED_MEDIA_TYPE: Not form-encoded, multipart or JSON\n- UNSUPPORTED_CHARSET: A JSON or multipart body that is not UTF-8\nEvery refusal: https://formparcel.live/docs/reference/responses#codes",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Refusal"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "UNSUPPORTED_MEDIA_TYPE",
                            "UNSUPPORTED_CHARSET"
                          ]
                        }
                      }
                    }
                  ]
                }
              },
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "422": {
            "description": "Refused. Nothing is stored and there is no receipt. A browser form post gets a short page instead of JSON.\n- PROHIBITED_FIELD: A field named like a password, one-time code or card detail\nEvery refusal: https://formparcel.live/docs/reference/responses#codes",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Refusal"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "PROHIBITED_FIELD"
                          ]
                        }
                      }
                    }
                  ]
                }
              },
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Refused. Nothing is stored and there is no receipt. A browser form post gets a short page instead of JSON.\n- CLIENT_RATE_LIMITED: Too many requests from one client, across all forms\n- FORM_RATE_LIMITED: Too many requests from one client to this form\n- CLIENT_HOURLY_LIMIT: One client's hourly limit in the workspace\n- FORM_HOURLY_LIMIT: The form's hourly cap on accepted leads\n- WORKSPACE_DAILY_LIMIT: The workspace's daily cap, on a busy form\n- SPAM_FOLDER_FULL: The submission would be held as spam, and the workspace's Spam folder is full for the month. Nothing is stored and there is no receipt.\n- PLAN_LIMIT_REACHED: The workspace has used three times its monthly leads. Nothing is stored and there is no receipt.\nEvery refusal: https://formparcel.live/docs/reference/responses#codes",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before one more try (not on PLAN_LIMIT_REACHED or SPAM_FOLDER_FULL).",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Refusal"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "CLIENT_RATE_LIMITED",
                            "FORM_RATE_LIMITED",
                            "CLIENT_HOURLY_LIMIT",
                            "FORM_HOURLY_LIMIT",
                            "WORKSPACE_DAILY_LIMIT",
                            "SPAM_FOLDER_FULL",
                            "PLAN_LIMIT_REACHED"
                          ]
                        }
                      }
                    }
                  ]
                }
              },
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Refused. Nothing is stored and there is no receipt. A browser form post gets a short page instead of JSON.\n- No code (Lead not saved): The lead could not be stored\nEvery refusal: https://formparcel.live/docs/reference/responses#codes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refusal"
                }
              },
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "503": {
            "description": "Refused. Nothing is stored and there is no receipt. A browser form post gets a short page instead of JSON.\n- SERVICE_UNAVAILABLE: The form lookup failed. Sends Retry-After: 5.\nEvery refusal: https://formparcel.live/docs/reference/responses#codes",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before one more try (not on PLAN_LIMIT_REACHED or SPAM_FOLDER_FULL).",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Refusal"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "SERVICE_UNAVAILABLE"
                          ]
                        }
                      }
                    }
                  ]
                }
              },
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "options": {
        "operationId": "preflight",
        "summary": "CORS preflight",
        "security": [],
        "description": "Allows POST, OPTIONS and the headers Content-Type, Accept, Authorization. Max age 86400 seconds.",
        "responses": {
          "204": {
            "description": "Preflight answered."
          }
        }
      },
      "get": {
        "operationId": "formAddressInfo",
        "summary": "Not a way to send",
        "security": [],
        "responses": {
          "405": {
            "description": "Method not allowed. Allow: POST, OPTIONS. A browser gets a short page that says this is a form address."
          }
        }
      }
    },
    "/f/formparcel.js": {
      "get": {
        "operationId": "botCheckScript",
        "summary": "The bot check script",
        "description": "Load it once on each page that has the form. It adds the proof to every form whose action is a form address on this host, and exposes FormParcel.proof(address) for fetch. No cookies, no widget.",
        "security": [],
        "responses": {
          "200": {
            "description": "The script.",
            "content": {
              "text/javascript": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/f/{formId}/challenge": {
      "parameters": [
        {
          "name": "formId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "operationId": "botCheckChallenge",
        "summary": "A bot-check challenge, for the script only",
        "description": "The script asks for it. Its shape is internal and may change: do not call it yourself.",
        "security": [],
        "responses": {
          "200": {
            "description": "A signed challenge."
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Accepted": {
        "type": "object",
        "required": [
          "accepted",
          "deliveryStatus",
          "submissionId"
        ],
        "properties": {
          "accepted": {
            "const": true
          },
          "deliveryStatus": {
            "enum": [
              "delivered",
              "pending"
            ],
            "description": "How far delivery got before the answer. Both are success."
          },
          "submissionId": {
            "type": "string",
            "pattern": "^sub_",
            "description": "The receipt ID. Also in column A of the Sheet row."
          }
        }
      },
      "Refusal": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "const": false
          },
          "error": {
            "type": "string",
            "description": "For people; it can change. Branch on the status, then on code."
          },
          "code": {
            "type": "string"
          }
        }
      }
    }
  },
  "x-service-unavailable-retry-seconds": 5
}
