ResponseDock Docs
Menu

Registration Forms

A registration form is a questionnaire your platform creates and keeps in sync from its own server, to collect sign-ups for one of your events, programs or memberships. People fill it in, including file uploads, inside your product. Your backend gets back only a confirmation and the few registration fields it needs; every other answer and every uploaded document stays in ResponseDock.

ℹ️

Registration forms are managed server to server with a workspace secret key (sk_live_…), so they need a plan with API keys (Pro and above). Never send the secret key to the browser.

How It Works

  1. Your server creates the form for an event with PUT /integrations/registration-forms, and calls it again whenever the event's options change.
  2. The organizer can reword the form and add their own questions in the normal survey builder, for example in an admin embed.
  3. For each sign-up attempt, your server issues an unguessable attempt ID and shows the form in a respondent embed with that ID as externalUserId.
  4. When the visitor submits, your server confirms the attempt and receives the registration fields, either right away or when the signed completion callback arrives.

Managed Questions

A new form contains a small set of managed questions with stable IDs, followed by a required document upload the organizer can change or remove:

FieldQuestion IDNotes
firstNamecompeetee_first_nameShort text, required
lastNamecompeetee_last_nameShort text, required while present. The organizer may remove it
emailcompeetee_emailShort text, required
gendercompeetee_genderChoice with values male and female, required while present. The organizer may remove it
competitioncompeetee_competitionChoice built from your competitions (a dropdown above six options). Only present with two or more options; with one it is implied

Organizers may reword, reorder and surround the managed questions with anything else. Their type, required flag and option values are restored on every save and sync, and a deleted managed question is put back, except a removed last name or gender question, which stays removed. Labels can be set per form with labels (firstName, lastName, email, gender, genderMale, genderFemale, competition, documents).

Create or Sync the Form

PUT /api/v1/integrations/registration-forms
Authorization: Bearer sk_live_your_secret_key
Content-Type: application/json

{
  "workspaceId": "ws_xxxx",
  "surveyId": "optional: the form to sync",
  "subject": { "id": "event_123", "label": "Spring Open" },
  "title": "Spring Open registration",
  "description": "Optional",
  "language": "en",
  "competitions": [
    { "value": "c0", "label": "Men" },
    { "value": "c1", "label": "Women" }
  ],
  "callback": {
    "url": "https://app.yourplatform.com/api/responsedock/registration-completed",
    "secret": "at least 32 characters, generated and kept by your server"
  }
}

Without surveyId the call creates a published form, or returns the existing form for that subject.id, so a retried create never produces a second form. With surveyId it updates the managed questions in the published version and in any draft. The reply is 201 when a form was created and 200 otherwise:

{
  "created": true,
  "form": {
    "surveyId": "…",
    "slug": "spring-open-registration-1a2b3c4d",
    "title": "Spring Open registration",
    "status": "ONLINE",
    "acceptingResponses": true,
    "hasUnpublishedChanges": false,
    "missingFields": [],
    "competitions": [{ "value": "c0", "label": "Men" }, { "value": "c1", "label": "Women" }],
    "completedResponses": 0,
    "updatedAt": "2026-09-30T10:00:00.000Z"
  },
  "callbackRegistered": true
}

callbackRegistered is only present when you sent a callback; an unusable callback URL never fails the call, it answers false. GET /api/v1/integrations/registration-forms/:surveyId?workspaceId=ws_xxxx returns the same form object, so you can show organizers whether the form is ready. acceptingResponses is false while the form is not live, outside its schedule, or once the workspace has used its monthly responses; send visitors to it only while it is true. Competition labels may be up to 300 characters. A refused request answers 400 with code: "INVALID_REQUEST", the offending field and the reason in error.

Show the Form

Request a respondent embed with allowedSurveyIds: [surveyId] and externalUserId set to a fresh attempt ID. The form only accepts a signed identity: opening it without the embed's respondent token fails with RESPONDENT_IDENTITY_REQUIRED. Each attempt is its own respondent, so one person can register several participants.

Confirm the Registration

POST /api/v1/integrations/registration-forms/:surveyId/confirmations
Authorization: Bearer sk_live_your_secret_key
Content-Type: application/json

{ "workspaceId": "ws_xxxx", "externalUserId": "the attempt ID" }

// 200 OK
{
  "confirmation": {
    "token": "the completed response ID",
    "surveyId": "…",
    "completedAt": "2026-09-30T10:05:00.000Z",
    "externalUserId": "the attempt ID"
  },
  "registration": {
    "firstName": "Ada",
    "lastName": "Lovelace",
    "email": "ada@example.com",
    "gender": "female",
    "competition": { "value": "c1", "label": "Women" }
  },
  "missingFields": []
}

A 404 with code: "REGISTRATION_NOT_COMPLETED" means there is no completed response for that attempt yet. The lookup uses the attempt ID, never anything the browser reports, so you do not have to trust the iframe's postMessage. Store confirmation.token against the participant: it is the link back to the answers and documents, which organizers review in ResponseDock.

Completion Callback

A visitor may close the page right after submitting, before your page hears about it. With a callback, ResponseDock calls your server once for every completed registration:

POST https://app.yourplatform.com/api/responsedock/registration-completed
X-ResponseDock-Event: registration_form.response.completed
X-ResponseDock-Signature-V2: sha256=<HMAC-SHA256 of "<timestamp>.<raw body>", keyed with callback.secret>
X-ResponseDock-Timestamp: <milliseconds since the epoch>

{
  "event": "registration_form.response.completed",
  "data": {
    "workspaceId": "ws_xxxx",
    "surveyId": "…",
    "responseId": "the completed response ID",
    "externalUserId": "the attempt ID",
    "completedAt": "2026-09-30T10:05:00.000Z"
  }
}

Verify the signature and reject timestamps older than five minutes, then confirm the attempt as above; the callback itself carries no answers. Failed deliveries are retried with back-off up to five attempts, and the callback and a browser-triggered confirmation can both arrive for the same attempt, so make the handler idempotent. The callback works without the workspace webhook entitlement and is not listed under the workspace's webhooks.

import { createHmac, timingSafeEqual } from 'node:crypto';

function verifyResponseDockCallback(rawBody: string, headers: Record<string, string>, secret: string) {
  const timestamp = Number(headers['x-responsedock-timestamp']);
  if (!Number.isFinite(timestamp) || Math.abs(Date.now() - timestamp) > 5 * 60_000) return false;
  const expected = `sha256=${createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')}`;
  const received = headers['x-responsedock-signature-v2'] ?? '';
  return received.length === expected.length && timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}

Limits

  • All three endpoints are limited per calling server (600 requests per minute by default).
  • Creating a form counts toward the workspace's survey limit; a full workspace answers 403 with code: "PLAN_LIMIT_REACHED" and limit: "surveys".
  • Completed registrations count toward the workspace's monthly response quota like any other response.