Form payload integration

Generate form telemetry and preserve your encoded application body.

Inspect the protected request

Form mode generates URL-encoded telemetry for supported material. Use it when Shape telemetry appears in the URL-encoded body. In DevTools, preserve the Network log and inspect Payload → Form Data as well as headers. Record the raw body, endpoint, content type, form ID/name and current cookies/CSRF state.

Form names can differ from header names; their separator and exact case vary. The illustrative family site-fielda/b/c/d/f/z has no separator before the suffix. Never derive form names by rewriting a header prefix. Supply complete ordered scripts and a profile; use measured browser values and session continuity when needed.

Both modes use public POST /v1/shape. Header mode remains the default.

The documented six-field form response applies to supported material. Sites may use other families; inspection alone does not establish compatibility. Unsupported or incomplete output returns an error. Generation success does not guarantee target acceptance.

Collect material and current state

Use prepared scripts directly, or follow Collect material when you need to download the full loader, payload and dependencies. Preserve observed execution order and complete URLs/query strings. Resolve HTML challenges or redirects first; Rayon never downloads missing scripts or submits the target request.

When browser values are needed, save a measured profile.json with the profile collector. If cookies apply, supply only the source page's current JavaScript-visible document.cookie as document_cookie. The full scoped HTTP cookie jar, including HttpOnly cookies, stays in your target client. Preserve browser identity and transport continuity when applicable; refresh CSRF and cookies when the application changes them.

Record the selected form's effective action, method and encoding, including clicked-submit formaction, formmethod or formenctype overrides. Include the clicked control's name=value when the browser sends it. Save the exact raw encoded application body as request-body.txt. Record observed ID/name and required application headers separately.

Prepare the form input

Add mode: "form" to your complete shape.json with scripts or a material reference, profile and any applicable JavaScript-visible cookies. This excerpt shows only the additional fields:

Form input excerpt — include your collected material and profile
{
  "mode": "form",
  "form": {
    "id": "account-form",
    "name": "account"
  },
  "request": {
    "url": "https://shop.example.com/account",
    "method": "POST",
    "content_type": "application/x-www-form-urlencoded",
    "body": "item=first&item=second&empty=&csrf=a%2Bb&name=caf%C3%A9"
  }
}

request.body is the application's exact encoded body before telemetry insertion. Empty string is valid; null is not. Preserve order, duplicate fields and empty values. Remove only stale telemetry tokens by their observed names; retain application fields, CSRF and cookies. Do not submit a stale complete telemetry family.

Optional form supplies the observed id and name when the material expects a named form. Each is a string of at most 256 characters without controls. Omit this object in header mode. The API envelope stays JSON; the target request must be POST with application/x-www-form-urlencoded, optionally with one UTF-8 charset parameter.

Form input permits at most 256 KiB UTF-8, 1,024 decoded pairs and 256 UTF-8 bytes per field name. Malformed percent escapes, invalid UTF-8 and NUL are rejected.

Call client.generate(shape) with the prepared input, as in Quickstart, and reuse that client across requests. Python and JavaScript SDKs support form mode. For an advanced REST fallback, use:

Generate from complete shape.json
curl --fail-with-body --max-time 45 'https://api.rayonapi.com/v1/shape' \
  -H "Authorization: Bearer $RAYON_API_KEY" \
  -H 'Content-Type: application/json' \
  --data-binary @shape.json

Before the synthetic submit, form mode allows eligible short initialization callbacks to settle, including chained zero-delay and animation-frame callbacks. This readiness phase stops when they settle and is bounded by 2 seconds and the execution deadline; long-running background work does not hold it open. Readiness is best effort, not full browser equivalence or a guarantee of target acceptance. A complete six-field family alone does not prove every browser signal was collected.

Compose the body once

Form response excerpt — synthetic values, not usable telemetry
{
  "form_fields": {
    "site-fielda": "example+value",
    "site-fieldb": "example+value",
    "site-fieldc": "example+value",
    "site-fieldd": "",
    "site-fieldf": "example+value",
    "site-fieldz": "example+value"
  },
  "form_body": "site-fielda=example%2Bvalue&site-fieldb=example%2Bvalue&site-fieldc=example%2Bvalue&site-fieldd=&site-fieldf=example%2Bvalue&site-fieldz=example%2Bvalue",
  "request_id": "00000000-0000-4000-8000-000000000001",
  "generation_ms": 12,
  "profile_used": {
    "user_agent": "<effective browser user agent>",
    "platform": "<effective platform>"
  },
  "material_ref": "<returned material reference>",
  "material_expires_at": "2026-10-14T12:05:00Z"
}

form_fields is an ordered object of raw exact-case names and values. form_body contains telemetry only, already URL-encoded once in observed order. Keep all six suffixes a/b/c/d/f/z, including an empty d value. Forms remain unsplit with no a0, even when a exceeds 7,900 characters. A form result has no headers field and returns no target cookies.

The result also contains request_id, generation_ms, profile_used, material_ref and material_expires_at. Prepend form_body, then add & and your original application body only when nonempty:

Compose with the exact application body supplied to generation
# result: FormShapeResult from Client.generate(shape) in SDK 1.1.4.
application_body = shape["request"]["body"]
wire_body = (
    result.form_body + ("&" + application_body if application_body else "")
).encode("utf-8")
# Send wire_body as data through your existing target_session.

SDK 1.1.4 is published for Python and JavaScript with typed FormShapeResultand optional header a0 support.

Do not URL-encode form_body again or pass the combined body to a form serializer. To rebuild, iterate form_fields with Object.entries or .items(), append application pairs in order and serialize the full list once. Do not also add form_body or collapse pairs into an object.

Optional direct REST continuation

Form mode is live on POST https://api.rayonapi.com/v1/shape. SDK 1.1.4+ also supports form mode. This Python example calls REST, then sends raw bytes through the open target_session from material collection. Run both steps in the same process so scripts, cookies, browser identity and sticky proxy/IP remain consistent.

Save current source-page document.cookie in document-cookie.txt without a trailing newline. Save observed required target headers in application-headers.json; keep auth/CSRF when required, and exclude the RayonAPI key. Replace the generic endpoint, origin allowlist and form selectors with approved values. Include clicked-submit data and remove only stale telemetry from request-body.txt before generation.

Generate via REST and send raw form bytes in the existing target session
"""Continue after the material-collection snippet, in the same process/session."""
import json
import os
from pathlib import Path
from urllib.parse import urlsplit
from urllib.request import Request, urlopen

# target_session, source_url, scripts and profile come from material collection.
request_url = "https://shop.example.com/account"  # Your exact observed action URL.
application_body = Path("request-body.txt").read_bytes().decode("utf-8")
document_cookie = Path("document-cookie.txt").read_bytes().decode("utf-8")
# Observed target headers, including current auth/CSRF when required; no Rayon key.
application_headers = json.loads(Path("application-headers.json").read_text("utf-8"))
observed_form = {"id": "account-form", "name": "account"}  # Omit absent attributes.
allowed_origins = {("https", "shop.example.com", 443)}
parsed = urlsplit(request_url)
port = parsed.port if parsed.port is not None else 443
if (not 1 <= port <= 65535 or parsed.username is not None or parsed.password is not None or parsed.fragment
        or (parsed.scheme, parsed.hostname, port) not in allowed_origins):
    raise ValueError("Use the exact approved HTTPS endpoint origin")
shape = {
    "source_url": source_url,
    "scripts": scripts,  # Full JavaScript sources, in observed execution order.
    "profile": profile,
    "document_cookie": document_cookie,  # JS-visible source-page cookies only.
    "mode": "form",
    "form": observed_form,
    "request": {
        "url": request_url,
        "method": "POST",
        "content_type": "application/x-www-form-urlencoded",
        "body": application_body,  # Application fields before fresh telemetry.
    },
}
api_request = Request(
    "https://api.rayonapi.com/v1/shape",
    data=json.dumps(shape, ensure_ascii=False).encode("utf-8"),
    headers={"Authorization": "Bearer " + os.environ["RAYON_API_KEY"],
             "Content-Type": "application/json"},
    method="POST",
)
# This is a separate REST API client, not the target's cookie/proxy session.
with urlopen(api_request, timeout=45) as api_response:
    result = json.load(api_response)
wire_body = (
    result["form_body"] + ("&" + application_body if application_body else "")
).encode("utf-8")
# The client owns cookie/connection headers; recompute headers tied to these bytes.
application_headers = {
    key: value for key, value in application_headers.items()
    if key.lower() not in {"host", "cookie", "content-length", "transfer-encoding",
                           "content-type", "user-agent"}
}
headers = {
    **application_headers,
    "Content-Type": shape["request"]["content_type"],
    "User-Agent": result["profile_used"]["user_agent"],
}
# A raw request keeps a page's Shape submit handler from overwriting these fields.
response = target_session.request(
    "POST", request_url, data=wire_body, headers=headers,
    allow_redirects=False, timeout=30,
)
print("Rayon request ID:", result["request_id"])
print("Target status:", response.status_code)
# Inspect response body, redirect and expected application outcome. 200 may challenge.

Download the REST example. A raw target request sends this telemetry once. If you use a browser, its Shape submit handler can replace or append fields afterward. Inspect the final outgoing raw body after handlers: confirm the API names/values survived unchanged and telemetry was not inserted twice.

Keep the same session

Send the composed bytes to the exact approved endpoint with POST and the form content type. Preserve the scoped cookie jar, CSRF token, authentication and required application headers when used, and preserve browser identity and proxy/IP/transport continuity when applicable. Keep your RayonAPI key in the API call only.

Generate fresh telemetry for each request. A material reference reuses scripts, not tokens or form state; supply the current profile, cookies, body and form identity. Inspect the target response body as well as status: generation success does not establish acceptance.

Continue with Collect material, Browser and session or API reference.