---
title: "Form completed"
method: POST
path: "form.completed"
tags: ["Webhooks"]
---

# Form completed

`POST form.completed` (webhook)

Sent to your endpoint when a respondent completes a form.

## Payload

- WebhookEvent: Outbound webhook payload. Standard-Webhooks-shaped envelope - the structure is stable and evolves additively within a `version`.
  - `type` string, required
  - `version` string, required
  - `timestamp` string, date-time, required
  - `data` object, required
    - `form_id` string, uuid, required
    - `form_name` string, required
    - `share_id` string, required
    - `form_tags` string[], required
    - `response_id` string, uuid, required
    - `submitted_at` string, date-time, required
    - `verified` boolean, required: Whether the submission's back-office processing finished without a terminal failure. `false` means it was released after a persistent processing error - its answers are still delivered so nothing is lost, but may be incomplete (e.g. a missing transcript or moderation verdict), so treat them as unverified.
    - `session` object, required
      - `device` string
      - `browser` string
      - `country` string
      - `city` string
      - `time_spent_seconds` integer, nullable
    - `responses` union[], required: The submission's answers, one item per answered node.
      - union
        - WebhookResponseChoice
          - `node_id` string, uuid, required
          - `prompt` string, required
          - `node_type` 'choice' | 'button', required: The kind of node this answer came from (`choice`, `button`, `open`, `details`, ...). Discriminates the value fields below. Unknown or future types always include `node_id`, `prompt`, and `node_type`, so they can be handled generically.
          - `label` string: The chosen option's text.
          - `option_id` string: The chosen option's id.
        - WebhookResponseOpen
          - `node_id` string, uuid, required
          - `prompt` string, required
          - `node_type` 'open', required: The kind of node this answer came from (`choice`, `button`, `open`, `details`, ...). Discriminates the value fields below. Unknown or future types always include `node_id`, `prompt`, and `node_type`, so they can be handled generically.
          - `text` string: The respondent's typed answer.
          - `media` object: Present for audio/video answers. `url` is a direct, time-limited signed link to the recording (an MP3 copy for audio answers when available) - valid for about 48 hours and supporting HTTP range requests, so it can be fetched or streamed straight into another tool - and `download_url` is the same link forced to download (Content-Disposition: attachment). After they expire, re-fetch the submission from the authenticated API (using the stable `response_id`) for fresh links.
            - `type` string: `audio` or `video`.
            - `content_type` string: MIME type of the recording.
            - `filename` string: Suggested download filename.
            - `url` string, uri: Signed link to fetch or stream the file inline.
            - `download_url` string, uri: Same link forced to download.
            - `expires_at` string, date-time: When the links stop working.
          - `transcription` object: Present when the audio/video answer was transcribed.
            - `text` string: The full transcript.
            - `language` string: BCP-47 language code, when detected.
            - `segments` object[]: Timestamped transcript segments (start/end in seconds) for captions or seeking. Present when word-level timing is available.
              - …
        - WebhookResponseDetails
          - `node_id` string, uuid, required
          - `prompt` string, required
          - `node_type` 'details', required: The kind of node this answer came from (`choice`, `button`, `open`, `details`, ...). Discriminates the value fields below. Unknown or future types always include `node_id`, `prompt`, and `node_type`, so they can be handled generically.
          - `fields` object[]: The contact details the respondent filled in. Each entry's `type` is the field kind (e.g. `email`, `first_name`), not the node type. Only filled fields appear.
            - `type` string
            - `value` string
          - `consent` object[]: Consent and opt-in agreements the respondent was shown, present only when the node has them. `accepted` records their choice and `label` is the exact wording they agreed to, snapshotted at submission - editing the agreement text later does not rewrite past responses.
            - `name` string: The consent item's field key.
            - `accepted` boolean: Whether the respondent accepted this agreement.
            - `type` string: `consent` (required) or `opt_in` (optional).
            - `label` string: The exact agreement wording, snapshotted at submission.
            - `reached_end` boolean: Whether the respondent scrolled the attached document to the end. Present only for document-gated agreements.
            - `document_version` string: The exact version of the attached agreement document the respondent saw. Present only for document-gated agreements.
            - `accepted_at` string: ISO timestamp of when the respondent accepted the document-gated agreement. Present only for document-gated agreements.
            - `document` object: The exact agreement file the respondent accepted, stamped when they submitted. Present only for document-gated agreements whose file still matched the version they saw.
              - …
        - WebhookResponseDraw
          - `node_id` string, uuid, required
          - `prompt` string, required
          - `node_type` 'draw', required: The kind of node this answer came from (`choice`, `button`, `open`, `details`, ...). Discriminates the value fields below. Unknown or future types always include `node_id`, `prompt`, and `node_type`, so they can be handled generically.
          - `media` object: The respondent's drawing, exported as a PNG image. `url` is a direct, time-limited signed link (~48h, HTTP range) and `download_url` is the same link forced to download. Re-fetch from the authenticated API after they expire.
            - `type` string: Always `image`.
            - `content_type` string: Image MIME type.
            - `filename` string: Suggested download filename.
            - `url` string, uri: Signed link to fetch the image inline.
            - `download_url` string, uri: Same link forced to download.
            - `expires_at` string, date-time: When the links stop working.
          - `game` object: Present only when the node has a configured target shape - the respondent's score against it.
            - `score` integer, nullable: 0-100 match score against the target outline.
            - `avg_deviation` number, nullable: Average normalized deviation from the target outline (lower is better).
            - `tier` string: The configured tier label the score falls into.
        - WebhookResponseFileUpload
          - `node_id` string, uuid, required
          - `prompt` string, required
          - `node_type` 'file_upload', required: The kind of node this answer came from (`choice`, `button`, `open`, `details`, ...). Discriminates the value fields below. Unknown or future types always include `node_id`, `prompt`, and `node_type`, so they can be handled generically.
          - `files` object[]: The uploaded files. Each has a direct, time-limited signed `url` (~48h, HTTP range) and a `download_url` (same link forced to download). Re-fetch from the authenticated API after they expire.
            - `id` string, uuid
            - `filename` string
            - `content_type` string
            - `size` integer, nullable
            - `uploaded_at` string, date-time
            - `url` string, uri
            - `download_url` string, uri
            - `expires_at` string, date-time
        - WebhookResponsePayment
          - `node_id` string, uuid, required
          - `prompt` string, required
          - `node_type` 'payment', required: The kind of node this answer came from (`choice`, `button`, `open`, `details`, ...). Discriminates the value fields below. Unknown or future types always include `node_id`, `prompt`, and `node_type`, so they can be handled generically.
          - `provider` string: Payment provider that processed the charge (currently always `stripe`).
          - `status` string: Payment status: `succeeded`, `partially_refunded`, or `refunded` - reflects Stripe at the time the event fired. A refund after delivery does not re-fire the webhook - see the Stripe integration docs.
          - `amount` integer, nullable: Amount charged, in the currency's smallest unit (e.g. 5000 = $50.00).
          - `amount_refunded` integer, nullable: Amount refunded so far, in the currency's smallest unit. Only present once a refund (full or partial) has been issued.
          - `currency` string: Three-letter ISO currency code, lowercase.
          - `payment_intent_id` string: Stripe PaymentIntent ID (`pi_...`).
          - `charge_id` string: Stripe Charge ID (`ch_...`) for reconciliation against your Stripe dashboard.
          - `receipt_url` string: Stripe-hosted receipt page for this charge.
          - `receipt_number` string: Stripe receipt number - present once Stripe emails a receipt.
          - `customer` object: Billing details the respondent entered at checkout. Only non-null parts are present.
            - `name` string
            - `email` string
            - `phone` string
            - `address` object: Billing address. Only non-null parts are present.
              - …
        - WebhookResponseFileDownload
          - `node_id` string, uuid, required
          - `prompt` string, required
          - `node_type` 'file_download', required: The kind of node this answer came from (`choice`, `button`, `open`, `details`, ...). Discriminates the value fields below. Unknown or future types always include `node_id`, `prompt`, and `node_type`, so they can be handled generically.
          - `files` object[]: The files the respondent downloaded. Each has a direct, time-limited signed `url` (~48h, HTTP range) and a `download_url` (same link forced to download). Re-fetch from the authenticated API after they expire.
            - `filename` string
            - `content_type` string
            - `downloaded_at` string, date-time
            - `url` string, uri
            - `download_url` string, uri
            - `expires_at` string, date-time
        - WebhookResponseCamera
          - `node_id` string, uuid, required
          - `prompt` string, required
          - `node_type` 'camera', required: The kind of node this answer came from (`choice`, `button`, `open`, `details`, ...). Discriminates the value fields below. Unknown or future types always include `node_id`, `prompt`, and `node_type`, so they can be handled generically.
          - `files` object[]: The captured photos and videos in capture order (one item unless the node allows several), mirroring file_upload's shape. Each has a direct, time-limited signed `url` (~48h, HTTP range) and a `download_url` (same link forced to download). Re-fetch from the authenticated API after they expire.
            - `id` string, uuid
            - `filename` string
            - `content_type` string
            - `size` integer, nullable
            - `uploaded_at` string, date-time
            - `url` string, uri
            - `download_url` string, uri
            - `expires_at` string, date-time

## Acknowledgement `2XX`

Acknowledged. Any 2xx response stops retries.

---

[API](https://skmtc.dev/clipform/apis/clipform-api.md) · [All operations](https://skmtc.dev/clipform/apis/clipform-api/llms.txt) · [OpenAPI document](https://skmtc.dev/clipform/apis/clipform-api/revisions/490de83af482?raw)
