Engagements

Docs API reference

Engagements

Live engagements: the sticky notes, questions and polaroid walls guests post to from their phones. Any system can post into one like a guest — a photo booth, a kiosk, an app — through the same moderation, and get a signed take-home link for each photo or video. Live polls (multiple choice, word clouds, ratings) are prepared and run under /engagements/{id}/polls and /polls/{poll_id}. The posting flow: POST /engagements/{id}/uploads, PUT the file to the upload_url, then POST /engagements/{id}/posts; poll GET /engagements/{id}/posts/{post_id} until share_ready before you show the take-home QR code.

List an engagement’s quizzes

GET /engagements/{id}/quizzes engagements:read

Its quizzes with their rounds, questions and right answers, and the games played (newest first).

Parameters

  • id string required path

    The engagement id (from GET /engagements).

Responses

  • 200 The quizzes and games.
  • 401 Missing or invalid API key.
  • 403 The key lacks the scope, or the role is read-only.
  • 404 Not found in this workspace.
  • 429 Too many requests for this key: wait details.retry_after_s seconds (also in Retry-After).
Response fields (200)
  • items object[]
    10 fields in items
    • id string
    • engagement_id string
    • title string
    • rounds object[]
      3 fields in rounds
      • id string
      • title string
      • questions object[]
        10 fields in questions
        • id string

          Short and stable; made for you when left out.

        • kind string required

          One of choice, true_false, number, order

        • text string required

          Up to 200 characters

        • image_asset_id string or null

          A picture round: an image from the media library.

        • options object[]
          2 fields in options
          • id string
          • label string

            Up to 80 characters

        • answer object
          5 fields in answer
          • option_id string
          • index integer

            On input: the right option by position, from 0.

          • value boolean
          • number number
          • tolerance number
        • unit string

          Up to 16 characters

        • points integer

          For a right answer at once (default 1000); right at the last moment scores half.

          0–5000

        • time_s integer

          Seconds to answer (default 20).

          5–120

        • partial boolean
    • settings object

      How the quiz is played.

      8 fields in settings
      • mode string

        One of solo, teams

      • captain boolean

        Teams: the captain’s answer counts (else the team’s first).

      • leaderboard string

        One of question, round, end

      • streak boolean

        A bonus for right answers in a row.

      • auto_advance boolean

        The game runs by itself: reveal, leaderboard, next.

      • read_ms integer

        0–15000

      • reveal_ms integer

        2000–30000

      • board_ms integer

        3000–60000

    • questions integer
    • problem string or null

      Why it cannot be played yet.

    • problems string[]

      A spreadsheet import: the rows that could not be read.

    • created_at integer
    • updated_at integer
  • games object[]
    6 fields in games
    • id string
    • quiz_id string
    • title string
    • phase string
    • started_at integer
    • ended_at integer or null
curl "https://www.wunderframe.com/api/v1/engagements/eng_Q2mK7vB3xYcZp4dW1/quizzes" \
  -H "Authorization: Bearer $WUNDERFRAME_API_KEY"
Response 200
{
  "items": [
    {
      "id": "qz_7vB3xYcZp4dW1Q2mK",
      "engagement_id": "eng_Q2mK7vB3xYcZp4dW1",
      "title": "Friday quiz night",
      "rounds": [
        {
          "id": "r1",
          "title": "Warm-up",
          "questions": [
            {
              "id": "cap",
              "kind": "choice",
              "text": "Capital of Switzerland?",
              "options": [
                {
                  "id": "o1",
                  "label": "Zurich"
                },
                {
                  "id": "o2",
                  "label": "Bern"
                },
                {
                  "id": "o3",
                  "label": "Geneva"
                }
              ],
              "answer": {
                "option_id": "o2"
              },
              "points": 1000,
              "time_s": 20
            }
          ]
        }
      ],
      "settings": {
        "mode": "teams",
        "captain": false,
        "leaderboard": "round",
        "streak": true,
        "auto_advance": false,
        "read_ms": 3000,
        "reveal_ms": 5000,
        "board_ms": 8000
      },
      "questions": 1,
      "problem": null,
      "created_at": 1791468000000,
      "updated_at": 1791468000000
    }
  ],
  "games": [
    {
      "id": "string",
      "quiz_id": "string",
      "title": "string",
      "phase": "string",
      "started_at": 0,
      "ended_at": null
    }
  ]
}
Error example
Response 401
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}

Create a quiz

POST /engagements/{id}/quizzes engagements:write

From rounds of questions, or from a spreadsheet (csv: rows it cannot read come back in problems, the rest is imported).

Parameters

  • id string required path

    The engagement id (from GET /engagements).

Request body required

  • title string

    Up to 80 characters

  • rounds object[]

    Up to 12 items

    3 fields in rounds
    • id string
    • title string
    • questions object[]
      10 fields in questions
      • id string

        Short and stable; made for you when left out.

      • kind string required

        One of choice, true_false, number, order

      • text string required

        Up to 200 characters

      • image_asset_id string or null

        A picture round: an image from the media library.

      • options object[]
        2 fields in options
        • id string
        • label string

          Up to 80 characters

      • answer object
        5 fields in answer
        • option_id string
        • index integer

          On input: the right option by position, from 0.

        • value boolean
        • number number
        • tolerance number
      • unit string

        Up to 16 characters

      • points integer

        For a right answer at once (default 1000); right at the last moment scores half.

        0–5000

      • time_s integer

        Seconds to answer (default 20).

        5–120

      • partial boolean
  • settings object

    How the quiz is played.

    8 fields in settings
    • mode string

      One of solo, teams

    • captain boolean

      Teams: the captain’s answer counts (else the team’s first).

    • leaderboard string

      One of question, round, end

    • streak boolean

      A bonus for right answers in a row.

    • auto_advance boolean

      The game runs by itself: reveal, leaderboard, next.

    • read_ms integer

      0–15000

    • reveal_ms integer

      2000–30000

    • board_ms integer

      3000–60000

  • csv string

    A spreadsheet instead of rounds.

Responses

  • 201 The quiz.
  • 400 The request is invalid — details.field names the field when there is one.
  • 401 Missing or invalid API key.
  • 403 The key lacks the scope, or the role is read-only.
  • 404 Not found in this workspace.
  • 429 Too many requests for this key: wait details.retry_after_s seconds (also in Retry-After).
Response fields (201)
  • id string
  • engagement_id string
  • title string
  • rounds object[]
    3 fields in rounds
    • id string
    • title string
    • questions object[]
      10 fields in questions
      • id string

        Short and stable; made for you when left out.

      • kind string required

        One of choice, true_false, number, order

      • text string required

        Up to 200 characters

      • image_asset_id string or null

        A picture round: an image from the media library.

      • options object[]
        2 fields in options
        • id string
        • label string

          Up to 80 characters

      • answer object
        5 fields in answer
        • option_id string
        • index integer

          On input: the right option by position, from 0.

        • value boolean
        • number number
        • tolerance number
      • unit string

        Up to 16 characters

      • points integer

        For a right answer at once (default 1000); right at the last moment scores half.

        0–5000

      • time_s integer

        Seconds to answer (default 20).

        5–120

      • partial boolean
  • settings object

    How the quiz is played.

    8 fields in settings
    • mode string

      One of solo, teams

    • captain boolean

      Teams: the captain’s answer counts (else the team’s first).

    • leaderboard string

      One of question, round, end

    • streak boolean

      A bonus for right answers in a row.

    • auto_advance boolean

      The game runs by itself: reveal, leaderboard, next.

    • read_ms integer

      0–15000

    • reveal_ms integer

      2000–30000

    • board_ms integer

      3000–60000

  • questions integer
  • problem string or null

    Why it cannot be played yet.

  • problems string[]

    A spreadsheet import: the rows that could not be read.

  • created_at integer
  • updated_at integer
curl -X POST "https://www.wunderframe.com/api/v1/engagements/eng_Q2mK7vB3xYcZp4dW1/quizzes" \
  -H "Authorization: Bearer $WUNDERFRAME_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "title": "Friday quiz night",
  "settings": {
    "mode": "teams",
    "leaderboard": "round"
  },
  "rounds": [
    {
      "title": "Warm-up",
      "questions": [
        {
          "kind": "choice",
          "text": "Capital of Switzerland?",
          "options": [
            "Zurich",
            "Bern",
            "Geneva"
          ],
          "answer": {
            "index": 1
          }
        },
        {
          "kind": "number",
          "text": "Year of the Moon landing?",
          "answer": {
            "number": 1969,
            "tolerance": 2
          },
          "unit": "year"
        }
      ]
    }
  ]
}'
Response 201
{
  "id": "qz_7vB3xYcZp4dW1Q2mK",
  "engagement_id": "eng_Q2mK7vB3xYcZp4dW1",
  "title": "Friday quiz night",
  "rounds": [
    {
      "id": "r1",
      "title": "Warm-up",
      "questions": [
        {
          "id": "cap",
          "kind": "choice",
          "text": "Capital of Switzerland?",
          "options": [
            {
              "id": "o1",
              "label": "Zurich"
            },
            {
              "id": "o2",
              "label": "Bern"
            },
            {
              "id": "o3",
              "label": "Geneva"
            }
          ],
          "answer": {
            "option_id": "o2"
          },
          "points": 1000,
          "time_s": 20
        }
      ]
    }
  ],
  "settings": {
    "mode": "teams",
    "captain": false,
    "leaderboard": "round",
    "streak": true,
    "auto_advance": false,
    "read_ms": 3000,
    "reveal_ms": 5000,
    "board_ms": 8000
  },
  "questions": 1,
  "problem": null,
  "created_at": 1791468000000,
  "updated_at": 1791468000000
}
Error example
Response 400
{
  "error": {
    "code": "invalid",
    "message": "The request is invalid — `details.field` names the field when there is one."
  }
}

Get a quiz

GET /quizzes/{quiz_id} engagements:read

Parameters

  • quiz_id string required path

    The quiz id (from GET /engagements/{id}/quizzes).

Responses

  • 200 The quiz.
  • 401 Missing or invalid API key.
  • 403 The key lacks the scope, or the role is read-only.
  • 404 Not found in this workspace.
  • 429 Too many requests for this key: wait details.retry_after_s seconds (also in Retry-After).
Response fields (200)
  • id string
  • engagement_id string
  • title string
  • rounds object[]
    3 fields in rounds
    • id string
    • title string
    • questions object[]
      10 fields in questions
      • id string

        Short and stable; made for you when left out.

      • kind string required

        One of choice, true_false, number, order

      • text string required

        Up to 200 characters

      • image_asset_id string or null

        A picture round: an image from the media library.

      • options object[]
        2 fields in options
        • id string
        • label string

          Up to 80 characters

      • answer object
        5 fields in answer
        • option_id string
        • index integer

          On input: the right option by position, from 0.

        • value boolean
        • number number
        • tolerance number
      • unit string

        Up to 16 characters

      • points integer

        For a right answer at once (default 1000); right at the last moment scores half.

        0–5000

      • time_s integer

        Seconds to answer (default 20).

        5–120

      • partial boolean
  • settings object

    How the quiz is played.

    8 fields in settings
    • mode string

      One of solo, teams

    • captain boolean

      Teams: the captain’s answer counts (else the team’s first).

    • leaderboard string

      One of question, round, end

    • streak boolean

      A bonus for right answers in a row.

    • auto_advance boolean

      The game runs by itself: reveal, leaderboard, next.

    • read_ms integer

      0–15000

    • reveal_ms integer

      2000–30000

    • board_ms integer

      3000–60000

  • questions integer
  • problem string or null

    Why it cannot be played yet.

  • problems string[]

    A spreadsheet import: the rows that could not be read.

  • created_at integer
  • updated_at integer
curl "https://www.wunderframe.com/api/v1/quizzes/quiz_id" \
  -H "Authorization: Bearer $WUNDERFRAME_API_KEY"
Response 200
{
  "id": "qz_7vB3xYcZp4dW1Q2mK",
  "engagement_id": "eng_Q2mK7vB3xYcZp4dW1",
  "title": "Friday quiz night",
  "rounds": [
    {
      "id": "r1",
      "title": "Warm-up",
      "questions": [
        {
          "id": "cap",
          "kind": "choice",
          "text": "Capital of Switzerland?",
          "options": [
            {
              "id": "o1",
              "label": "Zurich"
            },
            {
              "id": "o2",
              "label": "Bern"
            },
            {
              "id": "o3",
              "label": "Geneva"
            }
          ],
          "answer": {
            "option_id": "o2"
          },
          "points": 1000,
          "time_s": 20
        }
      ]
    }
  ],
  "settings": {
    "mode": "teams",
    "captain": false,
    "leaderboard": "round",
    "streak": true,
    "auto_advance": false,
    "read_ms": 3000,
    "reveal_ms": 5000,
    "board_ms": 8000
  },
  "questions": 1,
  "problem": null,
  "created_at": 1791468000000,
  "updated_at": 1791468000000
}
Error example
Response 401
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}

Update a quiz

PATCH /quizzes/{quiz_id} engagements:write

Only the parts you send change (csv replaces the rounds). A game already running keeps the quiz as it was when it started.

Parameters

  • quiz_id string required path

    The quiz id (from GET /engagements/{id}/quizzes).

Request body required

  • title string

    Up to 80 characters

  • rounds object[]

    Up to 12 items

    3 fields in rounds
    • id string
    • title string
    • questions object[]
      10 fields in questions
      • id string

        Short and stable; made for you when left out.

      • kind string required

        One of choice, true_false, number, order

      • text string required

        Up to 200 characters

      • image_asset_id string or null

        A picture round: an image from the media library.

      • options object[]
        2 fields in options
        • id string
        • label string

          Up to 80 characters

      • answer object
        5 fields in answer
        • option_id string
        • index integer

          On input: the right option by position, from 0.

        • value boolean
        • number number
        • tolerance number
      • unit string

        Up to 16 characters

      • points integer

        For a right answer at once (default 1000); right at the last moment scores half.

        0–5000

      • time_s integer

        Seconds to answer (default 20).

        5–120

      • partial boolean
  • settings object

    How the quiz is played.

    8 fields in settings
    • mode string

      One of solo, teams

    • captain boolean

      Teams: the captain’s answer counts (else the team’s first).

    • leaderboard string

      One of question, round, end

    • streak boolean

      A bonus for right answers in a row.

    • auto_advance boolean

      The game runs by itself: reveal, leaderboard, next.

    • read_ms integer

      0–15000

    • reveal_ms integer

      2000–30000

    • board_ms integer

      3000–60000

  • csv string

    A spreadsheet instead of rounds.

Responses

  • 200 The quiz.
  • 400 The request is invalid — details.field names the field when there is one.
  • 401 Missing or invalid API key.
  • 403 The key lacks the scope, or the role is read-only.
  • 404 Not found in this workspace.
  • 429 Too many requests for this key: wait details.retry_after_s seconds (also in Retry-After).
Response fields (200)
  • id string
  • engagement_id string
  • title string
  • rounds object[]
    3 fields in rounds
    • id string
    • title string
    • questions object[]
      10 fields in questions
      • id string

        Short and stable; made for you when left out.

      • kind string required

        One of choice, true_false, number, order

      • text string required

        Up to 200 characters

      • image_asset_id string or null

        A picture round: an image from the media library.

      • options object[]
        2 fields in options
        • id string
        • label string

          Up to 80 characters

      • answer object
        5 fields in answer
        • option_id string
        • index integer

          On input: the right option by position, from 0.

        • value boolean
        • number number
        • tolerance number
      • unit string

        Up to 16 characters

      • points integer

        For a right answer at once (default 1000); right at the last moment scores half.

        0–5000

      • time_s integer

        Seconds to answer (default 20).

        5–120

      • partial boolean
  • settings object

    How the quiz is played.

    8 fields in settings
    • mode string

      One of solo, teams

    • captain boolean

      Teams: the captain’s answer counts (else the team’s first).

    • leaderboard string

      One of question, round, end

    • streak boolean

      A bonus for right answers in a row.

    • auto_advance boolean

      The game runs by itself: reveal, leaderboard, next.

    • read_ms integer

      0–15000

    • reveal_ms integer

      2000–30000

    • board_ms integer

      3000–60000

  • questions integer
  • problem string or null

    Why it cannot be played yet.

  • problems string[]

    A spreadsheet import: the rows that could not be read.

  • created_at integer
  • updated_at integer
curl -X PATCH "https://www.wunderframe.com/api/v1/quizzes/quiz_id" \
  -H "Authorization: Bearer $WUNDERFRAME_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "title": "Friday quiz night — week 2",
  "settings": {
    "auto_advance": true
  }
}'
Response 200
{
  "id": "qz_7vB3xYcZp4dW1Q2mK",
  "engagement_id": "eng_Q2mK7vB3xYcZp4dW1",
  "title": "Friday quiz night",
  "rounds": [
    {
      "id": "r1",
      "title": "Warm-up",
      "questions": [
        {
          "id": "cap",
          "kind": "choice",
          "text": "Capital of Switzerland?",
          "options": [
            {
              "id": "o1",
              "label": "Zurich"
            },
            {
              "id": "o2",
              "label": "Bern"
            },
            {
              "id": "o3",
              "label": "Geneva"
            }
          ],
          "answer": {
            "option_id": "o2"
          },
          "points": 1000,
          "time_s": 20
        }
      ]
    }
  ],
  "settings": {
    "mode": "teams",
    "captain": false,
    "leaderboard": "round",
    "streak": true,
    "auto_advance": false,
    "read_ms": 3000,
    "reveal_ms": 5000,
    "board_ms": 8000
  },
  "questions": 1,
  "problem": null,
  "created_at": 1791468000000,
  "updated_at": 1791468000000
}
Error example
Response 400
{
  "error": {
    "code": "invalid",
    "message": "The request is invalid — `details.field` names the field when there is one."
  }
}

Delete a quiz

DELETE /quizzes/{quiz_id} engagements:write

Its games keep their results. Refused (409) while a game of it runs.

Parameters

  • quiz_id string required path

    The quiz id (from GET /engagements/{id}/quizzes).

Responses

  • 204 Deleted.
  • 401 Missing or invalid API key.
  • 403 The key lacks the scope, or the role is read-only.
  • 404 Not found in this workspace.
  • 409 Conflicts with the current state.
  • 429 Too many requests for this key: wait details.retry_after_s seconds (also in Retry-After).
curl -X DELETE "https://www.wunderframe.com/api/v1/quizzes/quiz_id" \
  -H "Authorization: Bearer $WUNDERFRAME_API_KEY"

204 — no body.

Error example
Response 401
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}

Start a game

POST /quizzes/{quiz_id}/games engagements:write

A lobby: guests join on the engagement’s phone page, the screens show the QR code and the names flowing in. A game running on the engagement ends first. Never start one nobody asked for.

Parameters

  • quiz_id string required path

    The quiz id (from GET /engagements/{id}/quizzes).

Responses

  • 201 The game.
  • 401 Missing or invalid API key.
  • 403 The key lacks the scope, or the role is read-only.
  • 404 Not found in this workspace.
  • 409 Conflicts with the current state.
  • 429 Too many requests for this key: wait details.retry_after_s seconds (also in Retry-After).
Response fields (201)
  • id string
  • quiz_id string
  • engagement_id string
  • title string
  • phase string

    One of lobby, question, locked, reveal, leaderboard, podium, ended

  • q integer

    The question, counted across rounds from 0; -1 in the lobby.

  • total integer
  • auto boolean
  • opens_at integer or null
  • locks_at integer or null
  • started_at integer
  • ended_at integer or null
curl -X POST "https://www.wunderframe.com/api/v1/quizzes/quiz_id/games" \
  -H "Authorization: Bearer $WUNDERFRAME_API_KEY"
Response 201
{
  "id": "qg_Zp4dW1Q2mK7vB3xYc",
  "quiz_id": "qz_7vB3xYcZp4dW1Q2mK",
  "engagement_id": "eng_Q2mK7vB3xYcZp4dW1",
  "title": "Friday quiz night",
  "phase": "question",
  "q": 0,
  "total": 12,
  "auto": false,
  "opens_at": 1791468003000,
  "locks_at": 1791468023000,
  "started_at": 1791467900000,
  "ended_at": null
}
Error example
Response 401
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}

Get a game’s results

GET /quiz-games/{game_id} engagements:read

The leaderboard and each question’s result (the right answer, counts, the fastest). ?format=csv answers the final leaderboard as CSV.

Parameters

  • game_id string required path

    The game id (from POST /quizzes/{quiz_id}/games).

  • format string query

    One of json, csv

Responses

  • 200 The results.
  • 401 Missing or invalid API key.
  • 403 The key lacks the scope, or the role is read-only.
  • 404 Not found in this workspace.
  • 429 Too many requests for this key: wait details.retry_after_s seconds (also in Retry-After).
Response fields (200)
  • game object
  • leaderboard object[]
    5 fields in leaderboard
    • name string
    • score integer
    • correct integer
    • rank integer
    • players integer

      Teams only.

  • questions object[]
curl "https://www.wunderframe.com/api/v1/quiz-games/game_id" \
  -H "Authorization: Bearer $WUNDERFRAME_API_KEY"
Response 200
{
  "game": {
    "id": "qg_Zp4dW1Q2mK7vB3xYc",
    "title": "Friday quiz night",
    "mode": "teams",
    "phase": "ended",
    "started_at": 1791467900000,
    "ended_at": 1791471500000,
    "players": 38,
    "teams": 9
  },
  "leaderboard": [
    {
      "id": "tm_1",
      "name": "The Quizzly Bears",
      "score": 8450,
      "correct": 10,
      "streak": 3,
      "rank": 1,
      "prev_rank": 2,
      "last": {
        "answered": true,
        "correct": true,
        "points": 910
      },
      "players": 4
    }
  ],
  "questions": [
    {
      "index": 0,
      "round": "Warm-up",
      "text": "Capital of Switzerland?",
      "kind": "choice",
      "question_id": "cap",
      "answer": {
        "option_id": "o2"
      },
      "counts": {
        "o1": 11,
        "o2": 25,
        "o3": 2
      },
      "answered": 38,
      "right": 25,
      "fastest": {
        "name": "Ada · The Quizzly Bears",
        "ms": 2140
      }
    }
  ]
}
Error example
Response 401
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}

Run a game

POST /quiz-games/{game_id}/{action} engagements:write

Like the Quizmaster: begin (the first question), lock (stop answers early), reveal (the right answer and the scores), leaderboard, next (the next question, or the podium after the last), podium, end, and auto_on / auto_off (the game runs by itself). A question locks by itself when its time is up. 409 when the move does not fit where the game is.

Parameters

  • game_id string required path

    The game id (from POST /quizzes/{quiz_id}/games).

  • action string required path

    One of begin, lock, reveal, leaderboard, next, podium, end, auto_on, auto_off

Responses

  • 200 The game as it is now.
  • 401 Missing or invalid API key.
  • 403 The key lacks the scope, or the role is read-only.
  • 404 Not found in this workspace.
  • 409 Conflicts with the current state.
  • 429 Too many requests for this key: wait details.retry_after_s seconds (also in Retry-After).
Response fields (200)
  • id string
  • quiz_id string
  • engagement_id string
  • title string
  • phase string

    One of lobby, question, locked, reveal, leaderboard, podium, ended

  • q integer

    The question, counted across rounds from 0; -1 in the lobby.

  • total integer
  • auto boolean
  • opens_at integer or null
  • locks_at integer or null
  • started_at integer
  • ended_at integer or null
curl -X POST "https://www.wunderframe.com/api/v1/quiz-games/game_id/action" \
  -H "Authorization: Bearer $WUNDERFRAME_API_KEY"
Response 200
{
  "id": "qg_Zp4dW1Q2mK7vB3xYc",
  "quiz_id": "qz_7vB3xYcZp4dW1Q2mK",
  "engagement_id": "eng_Q2mK7vB3xYcZp4dW1",
  "title": "Friday quiz night",
  "phase": "question",
  "q": 0,
  "total": 12,
  "auto": false,
  "opens_at": 1791468003000,
  "locks_at": 1791468023000,
  "started_at": 1791467900000,
  "ended_at": null
}
Error example
Response 401
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}

List polls

GET /engagements/{id}/polls engagements:read

The engagement’s live polls, in the order they were prepared, each with where its run stands.

Parameters

  • id string required path

    The engagement id (from GET /engagements).

Responses

  • 200 The polls.
  • 401 Missing or invalid API key.
  • 403 The key lacks the scope, or the role is read-only.
  • 404 Not found in this workspace.
  • 429 Too many requests for this key: wait details.retry_after_s seconds (also in Retry-After).
Response fields (200)
  • items object[]
    20 fields in items
    • id string required
    • engagement_id string required
    • order integer
    • type string required

      One of choice, word_cloud, rating

    • question string required

      Up to 200 characters

    • options object[]

      Multiple choice: 2–10 answers. The id never changes when the label does.

      2 fields in options
      • id string
      • label string

        Up to 80 characters

    • max_choices integer

      1 = one answer; more = up to that many.

    • scale object or null

      A rating: 1–5 or 1–10 as numbers, stars or faces (stars and faces are 1–5).

      4 fields in scale
      • min integer
      • max integer

        One of 5, 10

      • style string

        One of numbers, stars, emoji

      • labels string[] or null
    • words_per_guest integer

      1–3

    • results string

      The room sees the results as they come in, or once revealed.

      One of live, on_reveal

    • phone_results string

      One of after_vote, on_reveal, never

    • number_format string

      One of percent, count, both

    • allow_change boolean
    • state string required

      One of draft, live, closed

    • revealed boolean
    • run integer

      Bumped by reset: earlier votes stop counting.

    • launched_at integer or null
    • closed_at integer or null
    • created_at integer
    • updated_at integer
curl "https://www.wunderframe.com/api/v1/engagements/eng_Q2mK7vB3xYcZp4dW1/polls" \
  -H "Authorization: Bearer $WUNDERFRAME_API_KEY"
Response 200
{
  "items": [
    {
      "id": "pol_8Hk2mQ4xZ1",
      "engagement_id": "eng_Q2mK7vB3xYcZp4dW1",
      "order": 0,
      "type": "choice",
      "question": "Which session should run again tomorrow?",
      "options": [
        {
          "id": "a1b2",
          "label": "The keynote"
        },
        {
          "id": "c3d4",
          "label": "The workshop"
        }
      ],
      "max_choices": 1,
      "scale": null,
      "words_per_guest": 1,
      "results": "on_reveal",
      "phone_results": "after_vote",
      "number_format": "percent",
      "allow_change": true,
      "state": "live",
      "revealed": false,
      "run": 1,
      "launched_at": 1790150400000,
      "closed_at": null,
      "created_at": 1790140000000,
      "updated_at": 1790150400000
    }
  ]
}
Error example
Response 401
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}

Create a poll

POST /engagements/{id}/polls engagements:write

A new poll (a draft) at the end of the engagement’s list. Multiple choice takes 2–10 options (strings, or { id, label } to keep an id); a rating a scale; a word cloud words_per_guest (1–3). The engagement needs polls switched on to run it.

Parameters

  • id string required path

    The engagement id (from GET /engagements).

Request body required

  • type string

    One of choice, word_cloud, rating

  • question string

    Up to 200 characters

  • options string or object[]
  • max_choices integer
  • scale object
  • words_per_guest integer
  • results string

    One of live, on_reveal

  • phone_results string

    One of after_vote, on_reveal, never

  • number_format string

    One of percent, count, both

  • allow_change boolean

Responses

  • 201 The new poll.
  • 400 The request is invalid — details.field names the field when there is one.
  • 401 Missing or invalid API key.
  • 403 The key lacks the scope, or the role is read-only.
  • 404 Not found in this workspace.
  • 409 Conflicts with the current state.
  • 429 Too many requests for this key: wait details.retry_after_s seconds (also in Retry-After).
Response fields (201)
  • id string required
  • engagement_id string required
  • order integer
  • type string required

    One of choice, word_cloud, rating

  • question string required

    Up to 200 characters

  • options object[]

    Multiple choice: 2–10 answers. The id never changes when the label does.

    2 fields in options
    • id string
    • label string

      Up to 80 characters

  • max_choices integer

    1 = one answer; more = up to that many.

  • scale object or null

    A rating: 1–5 or 1–10 as numbers, stars or faces (stars and faces are 1–5).

    4 fields in scale
    • min integer
    • max integer

      One of 5, 10

    • style string

      One of numbers, stars, emoji

    • labels string[] or null
  • words_per_guest integer

    1–3

  • results string

    The room sees the results as they come in, or once revealed.

    One of live, on_reveal

  • phone_results string

    One of after_vote, on_reveal, never

  • number_format string

    One of percent, count, both

  • allow_change boolean
  • state string required

    One of draft, live, closed

  • revealed boolean
  • run integer

    Bumped by reset: earlier votes stop counting.

  • launched_at integer or null
  • closed_at integer or null
  • created_at integer
  • updated_at integer
curl -X POST "https://www.wunderframe.com/api/v1/engagements/eng_Q2mK7vB3xYcZp4dW1/polls" \
  -H "Authorization: Bearer $WUNDERFRAME_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "type": "choice",
  "question": "Which session should run again tomorrow?",
  "options": [
    "The keynote",
    "The workshop"
  ],
  "results": "on_reveal"
}'
Response 201
{
  "id": "pol_8Hk2mQ4xZ1",
  "engagement_id": "eng_Q2mK7vB3xYcZp4dW1",
  "order": 0,
  "type": "choice",
  "question": "Which session should run again tomorrow?",
  "options": [
    {
      "id": "a1b2",
      "label": "The keynote"
    },
    {
      "id": "c3d4",
      "label": "The workshop"
    }
  ],
  "max_choices": 1,
  "scale": null,
  "words_per_guest": 1,
  "results": "on_reveal",
  "phone_results": "after_vote",
  "number_format": "percent",
  "allow_change": true,
  "state": "live",
  "revealed": false,
  "run": 1,
  "launched_at": 1790150400000,
  "closed_at": null,
  "created_at": 1790140000000,
  "updated_at": 1790150400000
}
Error example
Response 400
{
  "error": {
    "code": "invalid",
    "message": "The request is invalid — `details.field` names the field when there is one."
  }
}

Change a poll

PATCH /polls/{poll_id} engagements:write

Change its setup; what you leave out stays. A poll that has run keeps its type, its answers’ ids, its scale and word count until it is reset (409); its words may change.

Parameters

  • poll_id string required path

    The poll id (from GET /engagements/{id}/polls).

Request body required

  • type string

    One of choice, word_cloud, rating

  • question string

    Up to 200 characters

  • options string or object[]
  • max_choices integer
  • scale object
  • words_per_guest integer
  • results string

    One of live, on_reveal

  • phone_results string

    One of after_vote, on_reveal, never

  • number_format string

    One of percent, count, both

  • allow_change boolean

Responses

  • 200 The poll.
  • 400 The request is invalid — details.field names the field when there is one.
  • 401 Missing or invalid API key.
  • 403 The key lacks the scope, or the role is read-only.
  • 404 Not found in this workspace.
  • 409 Conflicts with the current state.
  • 429 Too many requests for this key: wait details.retry_after_s seconds (also in Retry-After).
Response fields (200)
  • id string required
  • engagement_id string required
  • order integer
  • type string required

    One of choice, word_cloud, rating

  • question string required

    Up to 200 characters

  • options object[]

    Multiple choice: 2–10 answers. The id never changes when the label does.

    2 fields in options
    • id string
    • label string

      Up to 80 characters

  • max_choices integer

    1 = one answer; more = up to that many.

  • scale object or null

    A rating: 1–5 or 1–10 as numbers, stars or faces (stars and faces are 1–5).

    4 fields in scale
    • min integer
    • max integer

      One of 5, 10

    • style string

      One of numbers, stars, emoji

    • labels string[] or null
  • words_per_guest integer

    1–3

  • results string

    The room sees the results as they come in, or once revealed.

    One of live, on_reveal

  • phone_results string

    One of after_vote, on_reveal, never

  • number_format string

    One of percent, count, both

  • allow_change boolean
  • state string required

    One of draft, live, closed

  • revealed boolean
  • run integer

    Bumped by reset: earlier votes stop counting.

  • launched_at integer or null
  • closed_at integer or null
  • created_at integer
  • updated_at integer
curl -X PATCH "https://www.wunderframe.com/api/v1/polls/poll_id" \
  -H "Authorization: Bearer $WUNDERFRAME_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "question": "Which session should run again on Friday?",
  "results": "live"
}'
Response 200
{
  "id": "pol_8Hk2mQ4xZ1",
  "engagement_id": "eng_Q2mK7vB3xYcZp4dW1",
  "order": 0,
  "type": "choice",
  "question": "Which session should run again tomorrow?",
  "options": [
    {
      "id": "a1b2",
      "label": "The keynote"
    },
    {
      "id": "c3d4",
      "label": "The workshop"
    }
  ],
  "max_choices": 1,
  "scale": null,
  "words_per_guest": 1,
  "results": "on_reveal",
  "phone_results": "after_vote",
  "number_format": "percent",
  "allow_change": true,
  "state": "live",
  "revealed": false,
  "run": 1,
  "launched_at": 1790150400000,
  "closed_at": null,
  "created_at": 1790140000000,
  "updated_at": 1790150400000
}
Error example
Response 400
{
  "error": {
    "code": "invalid",
    "message": "The request is invalid — `details.field` names the field when there is one."
  }
}

Delete a poll

DELETE /polls/{poll_id} engagements:write

The poll and every vote on it. The phones and screens let it go.

Parameters

  • poll_id string required path

    The poll id (from GET /engagements/{id}/polls).

Responses

  • 204 Deleted.
  • 401 Missing or invalid API key.
  • 403 The key lacks the scope, or the role is read-only.
  • 404 Not found in this workspace.
  • 429 Too many requests for this key: wait details.retry_after_s seconds (also in Retry-After).
curl -X DELETE "https://www.wunderframe.com/api/v1/polls/poll_id" \
  -H "Authorization: Bearer $WUNDERFRAME_API_KEY"

204 — no body.

Error example
Response 401
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}

Run a poll

POST /polls/{poll_id}/{action} engagements:write

launch puts it on the phones and screens (closing the engagement’s live poll first); close stops the voting; reveal and hide show or hide the results of a poll whose results is on_reveal; reset starts a new run from zero votes.

Parameters

  • poll_id string required path

    The poll id (from GET /engagements/{id}/polls).

  • action string required path

    One of launch, close, reveal, hide, reset

Responses

  • 200 The poll after the move.
  • 401 Missing or invalid API key.
  • 403 The key lacks the scope, or the role is read-only.
  • 404 Not found in this workspace.
  • 409 Conflicts with the current state.
  • 429 Too many requests for this key: wait details.retry_after_s seconds (also in Retry-After).
Response fields (200)
  • id string required
  • engagement_id string required
  • order integer
  • type string required

    One of choice, word_cloud, rating

  • question string required

    Up to 200 characters

  • options object[]

    Multiple choice: 2–10 answers. The id never changes when the label does.

    2 fields in options
    • id string
    • label string

      Up to 80 characters

  • max_choices integer

    1 = one answer; more = up to that many.

  • scale object or null

    A rating: 1–5 or 1–10 as numbers, stars or faces (stars and faces are 1–5).

    4 fields in scale
    • min integer
    • max integer

      One of 5, 10

    • style string

      One of numbers, stars, emoji

    • labels string[] or null
  • words_per_guest integer

    1–3

  • results string

    The room sees the results as they come in, or once revealed.

    One of live, on_reveal

  • phone_results string

    One of after_vote, on_reveal, never

  • number_format string

    One of percent, count, both

  • allow_change boolean
  • state string required

    One of draft, live, closed

  • revealed boolean
  • run integer

    Bumped by reset: earlier votes stop counting.

  • launched_at integer or null
  • closed_at integer or null
  • created_at integer
  • updated_at integer
curl -X POST "https://www.wunderframe.com/api/v1/polls/poll_id/action" \
  -H "Authorization: Bearer $WUNDERFRAME_API_KEY"
Response 200
{
  "id": "pol_8Hk2mQ4xZ1",
  "engagement_id": "eng_Q2mK7vB3xYcZp4dW1",
  "order": 0,
  "type": "choice",
  "question": "Which session should run again tomorrow?",
  "options": [
    {
      "id": "a1b2",
      "label": "The keynote"
    },
    {
      "id": "c3d4",
      "label": "The workshop"
    }
  ],
  "max_choices": 1,
  "scale": null,
  "words_per_guest": 1,
  "results": "on_reveal",
  "phone_results": "after_vote",
  "number_format": "percent",
  "allow_change": true,
  "state": "live",
  "revealed": false,
  "run": 1,
  "launched_at": 1790150400000,
  "closed_at": null,
  "created_at": 1790140000000,
  "updated_at": 1790150400000
}
Error example
Response 401
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}

Get a poll’s results

GET /polls/{poll_id}/results engagements:read

The current run’s tally — also while it is hidden from the room. ?format=csv answers a CSV file. Counts only: no participant ever appears.

Parameters

  • poll_id string required path

    The poll id (from GET /engagements/{id}/polls).

  • format string query

    One of json, csv

Responses

  • 200 The poll and its tally (null before its first launch).
  • 401 Missing or invalid API key.
  • 403 The key lacks the scope, or the role is read-only.
  • 404 Not found in this workspace.
  • 429 Too many requests for this key: wait details.retry_after_s seconds (also in Retry-After).
Response fields (200)
  • poll object
    20 fields in poll
    • id string required
    • engagement_id string required
    • order integer
    • type string required

      One of choice, word_cloud, rating

    • question string required

      Up to 200 characters

    • options object[]

      Multiple choice: 2–10 answers. The id never changes when the label does.

      2 fields in options
      • id string
      • label string

        Up to 80 characters

    • max_choices integer

      1 = one answer; more = up to that many.

    • scale object or null

      A rating: 1–5 or 1–10 as numbers, stars or faces (stars and faces are 1–5).

      4 fields in scale
      • min integer
      • max integer

        One of 5, 10

      • style string

        One of numbers, stars, emoji

      • labels string[] or null
    • words_per_guest integer

      1–3

    • results string

      The room sees the results as they come in, or once revealed.

      One of live, on_reveal

    • phone_results string

      One of after_vote, on_reveal, never

    • number_format string

      One of percent, count, both

    • allow_change boolean
    • state string required

      One of draft, live, closed

    • revealed boolean
    • run integer

      Bumped by reset: earlier votes stop counting.

    • launched_at integer or null
    • closed_at integer or null
    • created_at integer
    • updated_at integer
  • tally object or any or null
curl "https://www.wunderframe.com/api/v1/polls/poll_id/results" \
  -H "Authorization: Bearer $WUNDERFRAME_API_KEY"
Response 200
{
  "poll": {
    "id": "pol_8Hk2mQ4xZ1",
    "engagement_id": "eng_Q2mK7vB3xYcZp4dW1",
    "order": 0,
    "type": "choice",
    "question": "Which session should run again tomorrow?",
    "options": [
      {
        "id": "a1b2",
        "label": "The keynote"
      },
      {
        "id": "c3d4",
        "label": "The workshop"
      }
    ],
    "max_choices": 1,
    "scale": null,
    "words_per_guest": 1,
    "results": "on_reveal",
    "phone_results": "after_vote",
    "number_format": "percent",
    "allow_change": true,
    "state": "live",
    "revealed": false,
    "run": 1,
    "launched_at": 1790150400000,
    "closed_at": null,
    "created_at": 1790140000000,
    "updated_at": 1790150400000
  },
  "tally": {
    "poll_id": "pol_8Hk2mQ4xZ1",
    "run": 1,
    "voters": 312,
    "options": [
      {
        "id": "a1b2",
        "count": 201,
        "pct": 64
      },
      {
        "id": "c3d4",
        "count": 111,
        "pct": 36
      }
    ],
    "at": 1790150460000
  }
}
Error example
Response 401
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}

List engagements

GET /engagements engagements:read

The workspace’s live engagements, newest first, with their state, what they take and their rooms.

Parameters

  • limit integer query

    Page size, 1–200.

    1–200. Default 50

  • cursor string query

    The next_cursor of the previous page.

Responses

  • 200 A page of engagements.
  • 401 Missing or invalid API key.
  • 403 The key lacks the scope, or the role is read-only.
  • 429 Too many requests for this key: wait details.retry_after_s seconds (also in Retry-After).
Response fields (200)
  • items object[] required
    15 fields in items
    • id string required
    • name string required
    • key string required

      The public key in the guests’ link, /e/<key>.

    • url string (uri) required

      The phone page guests open.

    • state string required

      open takes posts; paused takes them but holds them off the live walls until it opens again; closed refuses them (423).

      One of open, paused, closed

    • open boolean required

      Posts are taken now (open or paused).

    • moderation string required

      open: straight to the walls. approval: every post waits for the operator. auto_check: clean posts go through, flagged ones wait.

      One of open, approval, auto_check

    • accepts object required

      What a post may be now, after the workspace’s rules (videos can be switched off for the whole workspace).

      4 fields in accepts
      • note boolean
      • question boolean
      • photo boolean
      • video boolean
    • rooms object[] required

      Where posts can come from. Send a room’s key as room on a post so walls set to that room show it.

      3 fields in rooms
      • key string

        6 characters; never changes when the room is renamed.

      • label string
      • url string (uri)
    • consent object required

      When required, show the text to the person in their language (texts.de / texts.en / texts.fr; text is the German) and send consent: true with their post. The workspace default wording exists in all three; an own text is the same in each.

      3 fields in consent
      • required boolean
      • text string
      • texts object
        3 fields in texts
        • de string
        • en string
        • fr string
    • limits object required

      The file sizes, video length and text lengths this engagement takes.

      9 fields in limits
      • photo_max_bytes integer
      • video_max_bytes integer
      • video_max_s integer
      • note_max_chars integer
      • question_max_chars integer
      • photo_caption_max_chars integer
      • video_caption_max_chars integer
      • name_max_chars integer
      • client_label_max_chars integer
    • scope object

      What it belongs to: the workspace, a show or a session.

      2 fields in scope
      • kind string

        One of workspace, room, show, session

      • id string
    • created_at integer or null

      Epoch milliseconds.

    • availability object or null

      When guests can take part: why (always, window = its opening hours, range = its dates, session, on_screen, manual, override, shot_setting), until when it stays as it is, when it opens next, and one sentence in German, English and French ("Guests can take part now · until 22:00").

      5 fields in availability
      • reason string
      • until integer or null

        Epoch milliseconds.

      • next_open integer or null

        Epoch milliseconds.

      • sentence object
        3 fields in sentence
        • de string
        • en string
        • fr string
      • time_zone string or null
    • retention_days integer

      Posts and photos are deleted this many days after they are posted.

  • next_cursor string or null required

    Pass as cursor for the next page; null on the last page.

curl "https://www.wunderframe.com/api/v1/engagements?limit=50" \
  -H "Authorization: Bearer $WUNDERFRAME_API_KEY"
Response 200
{
  "items": [
    {
      "id": "eng_Q2mK7vB3xYcZp4dW1",
      "name": "Gala night",
      "key": "a6ptt2gre6vu",
      "url": "https://www.wunderframe.com/e/a6ptt2gre6vu",
      "state": "open",
      "open": true,
      "moderation": "approval",
      "accepts": {
        "note": true,
        "question": false,
        "photo": true,
        "video": true
      },
      "rooms": [
        {
          "key": "up82k8",
          "label": "Lobby",
          "url": "https://www.wunderframe.com/e/a6ptt2gre6vu/up82k8"
        }
      ],
      "consent": {
        "required": true,
        "text": "Ich bin einverstanden, dass mein Beitrag und mein Foto auf den Bildschirmen gezeigt und bis zum Ende der Aufbewahrungsfrist gespeichert werden.",
        "texts": {
          "de": "Ich bin einverstanden, dass mein Beitrag und mein Foto auf den Bildschirmen gezeigt und bis zum Ende der Aufbewahrungsfrist gespeichert werden.",
          "en": "I agree that my post and photo are shown on the screens and kept until the retention period ends.",
          "fr": "J’accepte que ma publication et ma photo soient affichées sur les écrans et conservées jusqu’à la fin de la durée de conservation."
        }
      },
      "limits": {
        "photo_max_bytes": 8388608,
        "video_max_bytes": 62914560,
        "video_max_s": 15,
        "note_max_chars": 280,
        "question_max_chars": 280,
        "photo_caption_max_chars": 60,
        "video_caption_max_chars": 60,
        "name_max_chars": 40,
        "client_label_max_chars": 40
      },
      "scope": {
        "kind": "workspace",
        "id": "ws_acme"
      },
      "created_at": 1790150400000
    }
  ],
  "next_cursor": null
}
Error example
Response 401
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}

Get an engagement

GET /engagements/{id} engagements:read

Whether it takes posts now (state), what kinds, its rooms, whether consent is asked and the limits.

Parameters

  • id string required path

    The engagement id (from GET /engagements).

Responses

  • 200 The engagement.
  • 401 Missing or invalid API key.
  • 403 The key lacks the scope, or the role is read-only.
  • 404 Not found in this workspace.
  • 429 Too many requests for this key: wait details.retry_after_s seconds (also in Retry-After).
Response fields (200)
  • id string required
  • name string required
  • key string required

    The public key in the guests’ link, /e/<key>.

  • url string (uri) required

    The phone page guests open.

  • state string required

    open takes posts; paused takes them but holds them off the live walls until it opens again; closed refuses them (423).

    One of open, paused, closed

  • open boolean required

    Posts are taken now (open or paused).

  • moderation string required

    open: straight to the walls. approval: every post waits for the operator. auto_check: clean posts go through, flagged ones wait.

    One of open, approval, auto_check

  • accepts object required

    What a post may be now, after the workspace’s rules (videos can be switched off for the whole workspace).

    4 fields in accepts
    • note boolean
    • question boolean
    • photo boolean
    • video boolean
  • rooms object[] required

    Where posts can come from. Send a room’s key as room on a post so walls set to that room show it.

    3 fields in rooms
    • key string

      6 characters; never changes when the room is renamed.

    • label string
    • url string (uri)
  • consent object required

    When required, show the text to the person in their language (texts.de / texts.en / texts.fr; text is the German) and send consent: true with their post. The workspace default wording exists in all three; an own text is the same in each.

    3 fields in consent
    • required boolean
    • text string
    • texts object
      3 fields in texts
      • de string
      • en string
      • fr string
  • limits object required

    The file sizes, video length and text lengths this engagement takes.

    9 fields in limits
    • photo_max_bytes integer
    • video_max_bytes integer
    • video_max_s integer
    • note_max_chars integer
    • question_max_chars integer
    • photo_caption_max_chars integer
    • video_caption_max_chars integer
    • name_max_chars integer
    • client_label_max_chars integer
  • scope object

    What it belongs to: the workspace, a show or a session.

    2 fields in scope
    • kind string

      One of workspace, room, show, session

    • id string
  • created_at integer or null

    Epoch milliseconds.

  • availability object or null

    When guests can take part: why (always, window = its opening hours, range = its dates, session, on_screen, manual, override, shot_setting), until when it stays as it is, when it opens next, and one sentence in German, English and French ("Guests can take part now · until 22:00").

    5 fields in availability
    • reason string
    • until integer or null

      Epoch milliseconds.

    • next_open integer or null

      Epoch milliseconds.

    • sentence object
      3 fields in sentence
      • de string
      • en string
      • fr string
    • time_zone string or null
  • retention_days integer

    Posts and photos are deleted this many days after they are posted.

curl "https://www.wunderframe.com/api/v1/engagements/eng_Q2mK7vB3xYcZp4dW1" \
  -H "Authorization: Bearer $WUNDERFRAME_API_KEY"
Response 200
{
  "id": "eng_Q2mK7vB3xYcZp4dW1",
  "name": "Gala night",
  "key": "a6ptt2gre6vu",
  "url": "https://www.wunderframe.com/e/a6ptt2gre6vu",
  "state": "open",
  "open": true,
  "moderation": "approval",
  "accepts": {
    "note": true,
    "question": false,
    "photo": true,
    "video": true
  },
  "rooms": [
    {
      "key": "up82k8",
      "label": "Lobby",
      "url": "https://www.wunderframe.com/e/a6ptt2gre6vu/up82k8"
    }
  ],
  "consent": {
    "required": true,
    "text": "Ich bin einverstanden, dass mein Beitrag und mein Foto auf den Bildschirmen gezeigt und bis zum Ende der Aufbewahrungsfrist gespeichert werden.",
    "texts": {
      "de": "Ich bin einverstanden, dass mein Beitrag und mein Foto auf den Bildschirmen gezeigt und bis zum Ende der Aufbewahrungsfrist gespeichert werden.",
      "en": "I agree that my post and photo are shown on the screens and kept until the retention period ends.",
      "fr": "J’accepte que ma publication et ma photo soient affichées sur les écrans et conservées jusqu’à la fin de la durée de conservation."
    }
  },
  "limits": {
    "photo_max_bytes": 8388608,
    "video_max_bytes": 62914560,
    "video_max_s": 15,
    "note_max_chars": 280,
    "question_max_chars": 280,
    "photo_caption_max_chars": 60,
    "video_caption_max_chars": 60,
    "name_max_chars": 40,
    "client_label_max_chars": 40
  },
  "scope": {
    "kind": "workspace",
    "id": "ws_acme"
  },
  "created_at": 1790150400000
}
Error example
Response 401
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}

Get an upload URL

POST /engagements/{id}/uploads engagements:write

Step 1 of a photo or video post: one signed URL for one file. PUT the file’s bytes to upload_url with headers, then name upload_id in POST /engagements/{id}/posts. The file is checked when you post: it must really be the type you named and no larger than bytes.

Parameters

  • id string required path

    The engagement id (from GET /engagements).

Request body required

  • kind string required

    One of photo, video

  • mime string required

    A photo: image/jpeg, image/png or image/webp. A video: video/mp4, video/quicktime or video/webm.

  • bytes integer required

    The file’s exact size. A photo up to 8 MB, a video up to 60 MB.

    At least 1

Responses

  • 201 Where to send the file.
  • 400 The request is invalid — details.field names the field when there is one.
  • 401 Missing or invalid API key.
  • 403 The key lacks the scope, or the role is read-only.
  • 404 Not found in this workspace.
  • 413 Too large.
  • 415 Unsupported media type.
  • 423 Closed: the engagement takes no posts right now.
  • 429 Too many requests for this key: wait details.retry_after_s seconds (also in Retry-After).
  • 503 A service this call needs is not available right now.
Response fields (201)
  • upload_id string required

    Name it in the post. Good for one post, within an hour.

  • upload_url string (uri) required

    Send the file’s bytes here, at once (the URL is short-lived and single-use).

  • method string required

    Usually PUT.

  • headers map of string required

    Send these headers with the upload.

curl -X POST "https://www.wunderframe.com/api/v1/engagements/eng_Q2mK7vB3xYcZp4dW1/uploads" \
  -H "Authorization: Bearer $WUNDERFRAME_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "kind": "photo",
  "mime": "image/jpeg",
  "bytes": 1843200
}'
Response 201
{
  "upload_id": "u3f9a0c1d2e4b5a6978c0",
  "upload_url": "https://www.wunderframe.com/_storage/upload/…",
  "method": "PUT",
  "headers": {
    "Content-Type": "image/jpeg"
  }
}
Error example
Response 400
{
  "error": {
    "code": "invalid",
    "message": "The request is invalid — `details.field` names the field when there is one."
  }
}

Post like a guest

POST /engagements/{id}/posts engagements:write

A photo, video, note or question, through the same moderation as a guest’s post. It reaches the walls at once when moderation is open (a video once it is made small), or waits for the operator. While the engagement is paused, posts are taken but held off the live walls; while it is closed, 423. Send an Idempotency-Key header to retry safely: the same key and body answer the same post for 24 hours (200, Idempotent-Replayed: true).

Parameters

  • id string required path

    The engagement id (from GET /engagements).

  • Idempotency-Key string header

    Any unique string (1–200 printable characters), e.g. a UUID per shot. A retry with the same key and body returns the first post; another body is a 409.

    Up to 200 characters

Request body required

  • kind string required

    One of photo, video, note, question

  • upload_id string

    A photo or video: the upload_id its upload returned, after the PUT.

  • caption string

    A photo’s or video’s caption, or the words of a note or question (text is the same field).

  • text string

    The same as caption.

  • name string

    The person’s name, shown with the post. Leave it out for anonymous.

    Up to 40 characters

  • room string

    A room key (or label) of the engagement. An unknown room is refused.

  • client_label string

    Your own label for where it came from, e.g. Booth 1. Stored on the post.

    Up to 40 characters

  • consent boolean

    Required (true) when the engagement’s consent.required: the person agreed to the consent text.

Responses

  • 200 A retry under the same Idempotency-Key: the first post, as it is now.
  • 201 The post, with its take-home link.
  • 400 The request is invalid — details.field names the field when there is one.
  • 401 Missing or invalid API key.
  • 403 The key lacks the scope, or the role is read-only.
  • 404 Not found in this workspace.
  • 409 Conflicts with the current state.
  • 413 Too large.
  • 415 Unsupported media type.
  • 423 Closed: the engagement takes no posts right now.
  • 429 Too many requests for this key: wait details.retry_after_s seconds (also in Retry-After).
  • 503 A service this call needs is not available right now.
Response fields (200)
  • post_id string required
  • engagement_id string required
  • kind string required

    One of photo, video, note, question

  • status string required

    pending waits for the operator; approved, featured and answered are on the walls; rejected and hidden are not.

    One of pending, approved, featured, answered, rejected, hidden

  • video_status string or null

    A video is made small on the server first; it reaches the walls once ready. null for other kinds.

    One of processing, ready, failed, null

  • room string or null

    The room key.

  • client_label string or null
  • share_ready boolean required

    The take-home copy is made: the take-home page shows it at once. On the free plan it carries the Wunderframe watermark.

  • take_home_url string (uri) or null required

    A signed link to a page where the person sees their photo or video and can share or save it. Show it as a QR code. For one post, for 24 hours after it was made. null for notes and questions, and for a post that was rejected or taken down.

  • take_home_expires_at integer or null

    When the link stops working (epoch milliseconds).

  • source string

    Where it came from: a guest’s phone, an API client, or a stage set to Photo booth.

    One of phone, api, booth

  • crop object or null

    The face-aware crop box every screen frames the photo by: 0..1 of the original, x and y the top-left corner. null until faces were looked for.

    4 fields in crop
    • x number
    • y number
    • w number
    • h number
  • crop_source string or null

    One of faces, booth, center, manual, null

  • keepsake_ready boolean

    The branded keepsake polaroid is made; the take-home page shows and shares it.

  • keepsake_url string (uri) or null

    Where the person gets the keepsake: the take-home page.

  • created_at integer required

    Epoch milliseconds.

Response fields (201)
  • post_id string required
  • engagement_id string required
  • kind string required

    One of photo, video, note, question

  • status string required

    pending waits for the operator; approved, featured and answered are on the walls; rejected and hidden are not.

    One of pending, approved, featured, answered, rejected, hidden

  • video_status string or null

    A video is made small on the server first; it reaches the walls once ready. null for other kinds.

    One of processing, ready, failed, null

  • room string or null

    The room key.

  • client_label string or null
  • share_ready boolean required

    The take-home copy is made: the take-home page shows it at once. On the free plan it carries the Wunderframe watermark.

  • take_home_url string (uri) or null required

    A signed link to a page where the person sees their photo or video and can share or save it. Show it as a QR code. For one post, for 24 hours after it was made. null for notes and questions, and for a post that was rejected or taken down.

  • take_home_expires_at integer or null

    When the link stops working (epoch milliseconds).

  • source string

    Where it came from: a guest’s phone, an API client, or a stage set to Photo booth.

    One of phone, api, booth

  • crop object or null

    The face-aware crop box every screen frames the photo by: 0..1 of the original, x and y the top-left corner. null until faces were looked for.

    4 fields in crop
    • x number
    • y number
    • w number
    • h number
  • crop_source string or null

    One of faces, booth, center, manual, null

  • keepsake_ready boolean

    The branded keepsake polaroid is made; the take-home page shows and shares it.

  • keepsake_url string (uri) or null

    Where the person gets the keepsake: the take-home page.

  • created_at integer required

    Epoch milliseconds.

curl -X POST "https://www.wunderframe.com/api/v1/engagements/eng_Q2mK7vB3xYcZp4dW1/posts" \
  -H "Authorization: Bearer $WUNDERFRAME_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "kind": "photo",
  "upload_id": "u3f9a0c1d2e4b5a6978c0",
  "caption": "Gala night 2026",
  "room": "up82k8",
  "client_label": "Booth 1",
  "consent": true
}'
Response 200
{
  "post_id": "pst_7vB3xYcZp4dW1qR8",
  "engagement_id": "eng_Q2mK7vB3xYcZp4dW1",
  "kind": "photo",
  "status": "pending",
  "video_status": null,
  "room": "up82k8",
  "client_label": "Booth 1",
  "share_ready": false,
  "take_home_url": "https://www.wunderframe.com/e/a6ptt2gre6vu/p/pst_7vB3xYcZp4dW1qR8?t=sp1x2c.hK3…",
  "take_home_expires_at": 1790236800000,
  "source": "api",
  "crop": {
    "x": 0.12,
    "y": 0,
    "w": 0.75,
    "h": 1
  },
  "crop_source": "faces",
  "keepsake_ready": false,
  "keepsake_url": "https://www.wunderframe.com/e/a6ptt2gre6vu/p/pst_7vB3xYcZp4dW1qR8?t=sp1x2c.hK3…",
  "created_at": 1790150400000
}
Error example
Response 400
{
  "error": {
    "code": "invalid",
    "message": "The request is invalid — `details.field` names the field when there is one."
  }
}

Get a post’s status

GET /engagements/{id}/posts/{post_id} engagements:write

Poll this (every 2–3 seconds) until share_ready is true, then show take_home_url as a QR code. Only posts made with the same API key are found.

Parameters

  • id string required path

    The engagement id (from GET /engagements).

  • post_id string required path

    The post id POST /engagements/{id}/posts returned.

Responses

  • 200 The post as it is now.
  • 401 Missing or invalid API key.
  • 403 The key lacks the scope, or the role is read-only.
  • 404 Not found in this workspace.
  • 429 Too many requests for this key: wait details.retry_after_s seconds (also in Retry-After).
Response fields (200)
  • post_id string required
  • engagement_id string required
  • kind string required

    One of photo, video, note, question

  • status string required

    pending waits for the operator; approved, featured and answered are on the walls; rejected and hidden are not.

    One of pending, approved, featured, answered, rejected, hidden

  • video_status string or null

    A video is made small on the server first; it reaches the walls once ready. null for other kinds.

    One of processing, ready, failed, null

  • room string or null

    The room key.

  • client_label string or null
  • share_ready boolean required

    The take-home copy is made: the take-home page shows it at once. On the free plan it carries the Wunderframe watermark.

  • take_home_url string (uri) or null required

    A signed link to a page where the person sees their photo or video and can share or save it. Show it as a QR code. For one post, for 24 hours after it was made. null for notes and questions, and for a post that was rejected or taken down.

  • take_home_expires_at integer or null

    When the link stops working (epoch milliseconds).

  • source string

    Where it came from: a guest’s phone, an API client, or a stage set to Photo booth.

    One of phone, api, booth

  • crop object or null

    The face-aware crop box every screen frames the photo by: 0..1 of the original, x and y the top-left corner. null until faces were looked for.

    4 fields in crop
    • x number
    • y number
    • w number
    • h number
  • crop_source string or null

    One of faces, booth, center, manual, null

  • keepsake_ready boolean

    The branded keepsake polaroid is made; the take-home page shows and shares it.

  • keepsake_url string (uri) or null

    Where the person gets the keepsake: the take-home page.

  • created_at integer required

    Epoch milliseconds.

curl "https://www.wunderframe.com/api/v1/engagements/eng_Q2mK7vB3xYcZp4dW1/posts/pst_7vB3xYcZp4dW1qR8" \
  -H "Authorization: Bearer $WUNDERFRAME_API_KEY"
Response 200
{
  "post_id": "pst_7vB3xYcZp4dW1qR8",
  "engagement_id": "eng_Q2mK7vB3xYcZp4dW1",
  "kind": "photo",
  "status": "pending",
  "video_status": null,
  "room": "up82k8",
  "client_label": "Booth 1",
  "share_ready": false,
  "take_home_url": "https://www.wunderframe.com/e/a6ptt2gre6vu/p/pst_7vB3xYcZp4dW1qR8?t=sp1x2c.hK3…",
  "take_home_expires_at": 1790236800000,
  "source": "api",
  "crop": {
    "x": 0.12,
    "y": 0,
    "w": 0.75,
    "h": 1
  },
  "crop_source": "faces",
  "keepsake_ready": false,
  "keepsake_url": "https://www.wunderframe.com/e/a6ptt2gre6vu/p/pst_7vB3xYcZp4dW1qR8?t=sp1x2c.hK3…",
  "created_at": 1790150400000
}
Error example
Response 401
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  }
}