Shares

Shares are publicly accessible pages — either a video share page with an optional call-to-action, or a contact form that collects contact information. Shares track views and type-specific metrics (ctaClicks for video, submissions for contact forms).

All endpoints require authentication via the X-API-Key header. See Authentication.

Share types

Shares use a discriminated union on the type field:

  • video — Displays a video with an optional CTA (call-to-action) button linking to a URL. Requires a mediaId referencing a video media item.
  • contact-form — Collects contact information via a public form. Automatically generates a QR code image stored as media. Can optionally auto-create contacts from submissions.

Create a share

POST /shares

The request body differs by share type.

Video share fields

FieldTypeRequiredNotes
typestringYesMust be "video".
titlestringYes
descriptionstringNo
activebooleanYesWhether the share page is live.
mediaIdstringYesReference to a video media item.
ctaEnabledbooleanYesWhether to show a CTA button.
ctaTitlestringConditionalRequired if ctaEnabled is true.
ctaUrlstringConditionalRequired if ctaEnabled is true.

Contact form share fields

FieldTypeRequiredNotes
typestringYesMust be "contact-form".
titlestringYes
descriptionstringNo
activebooleanYesWhether the share page is live.
autoCreateContactbooleanYesAutomatically create contacts from submissions.
existingContactUpdatestringNoWhat a submission does to a contact that already exists: "overwrite" (default) replaces its values with every answer given, "fill-empty" only sets the fields it has no value for.
contactInformationobjectYesSee structure below.
customFieldsarrayNoCustom contact fields shown on the public form. See structure below.
autoResponseobjectNoAutomatic text sent after a successful submission. See structure below.

Contact information structure

{
  "firstName": { "required": true, "visible": true },
  "lastName": { "required": true, "visible": true },
  "phone": { "required": false, "visible": true },
  "email": { "required": true, "visible": true },
  "organization": { "required": false, "visible": true }
}

Each field has required (boolean) and visible (boolean) properties controlling the form behavior. organization is optional; leaving it out hides the field.

Custom fields structure

[
  { "id": "6a82...", "required": true },
  { "id": "6a83...", "required": false }
]

Each entry references one of the organization's custom contact fields by id. Array order is display order; a field is visible on the public form by being present in the array. Rules enforced on save:

  • Every id must belong to a custom field of the organization.
  • Duplicate ids are rejected.
  • user-type custom fields are rejected — they reference org members and cannot be filled by an anonymous submitter.

Submitted values are validated per field type (option labels resolve to option ids for select/multi-select; dates normalize to UTC-midnight epoch milliseconds) and stored on the created or matched contact's fields.

Auto-response structure

{
  "enabled": true,
  "senderUserId": "632a...",
  "message": "Thanks for connecting! Save my contact card.",
  "contactCardMediaId": "676d..."
}

When enabled, submitters who opt in and provide a phone number receive an automatic text after their contact is created — at most once per contact per form. senderUserId must be an org member with a texting number; the text is sent from their number as a system message. contactCardMediaId is optional and must reference a vCard (text/vcard) media item of the organization, attached to the text as MMS so the recipient can save the sender's number. Opt-out suppression and the org's first-message disclosure are applied at send time. A disabled config ("enabled": false) is stored without these checks.

Example request (video)

curl -X POST https://<host>/api/shares \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "video",
    "title": "Welcome Video",
    "description": "Introduction to our program",
    "active": true,
    "mediaId": "676a...",
    "ctaEnabled": true,
    "ctaTitle": "Learn More",
    "ctaUrl": "https://example.com/info"
  }'

Example request (contact form)

curl -X POST https://<host>/api/shares \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "contact-form",
    "title": "Sign Up Form",
    "description": "Join our mailing list",
    "active": true,
    "autoCreateContact": true,
    "contactInformation": {
      "firstName": { "required": true, "visible": true },
      "lastName": { "required": true, "visible": true },
      "phone": { "required": false, "visible": true },
      "email": { "required": true, "visible": true }
    }
  }'

Response (video)

{
  "success": true,
  "data": {
    "_id": "677a...",
    "type": "video",
    "title": "Welcome Video",
    "description": "Introduction to our program",
    "active": true,
    "mediaId": "676a...",
    "views": 0,
    "cta": {
      "title": "Learn More",
      "url": "https://example.com/info"
    },
    "ctaClicks": 0,
    "createdAt": 1716000000000
  }
}

Response (contact form)

Contact form creation automatically generates a QR code image stored as media. The mediaId in the response points to this QR code.

{
  "success": true,
  "data": {
    "_id": "677b...",
    "type": "contact-form",
    "title": "Sign Up Form",
    "description": "Join our mailing list",
    "active": true,
    "autoCreateContact": true,
    "contactInformation": {
      "firstName": { "required": true, "visible": true },
      "lastName": { "required": true, "visible": true },
      "phone": { "required": false, "visible": true },
      "email": { "required": true, "visible": true }
    },
    "mediaId": "677c...",
    "views": 0,
    "submissions": 0,
    "createdAt": 1716000000000
  }
}

List shares

GET /shares

Returns shares with their associated media populated.

Query parameters

ParameterTypeDefaultNotes
typestring—video or contact-form.
pagenumber00-indexed.
perPagenumber50Items per page.

Example request

curl "https://<host>/api/shares?type=video&perPage=25" \
  -H "X-API-Key: your-api-key"

Response

{
  "success": true,
  "data": [
    {
      "_id": "677a...",
      "type": "video",
      "title": "Welcome Video",
      "active": true,
      "views": 15,
      "cta": { "title": "Learn More", "url": "https://example.com/info" },
      "ctaClicks": 3,
      "media": {
        "_id": "676a...",
        "name": "welcome.mp4",
        "type": "video/mp4",
        "url": "https://storage.googleapis.com/..."
      },
      "createdAt": 1716000000000
    }
  ],
  "pagination": {
    "page": 0,
    "perPage": 25,
    "totalItems": 4,
    "totalPages": 1
  }
}

Get a share

GET /shares/:id

Returns a single share with its associated media populated.

Path parameters

ParameterTypeDescription
idstringShare ID.

Example request

curl https://<host>/api/shares/677a... \
  -H "X-API-Key: your-api-key"

Response

{
  "success": true,
  "data": {
    "_id": "677a...",
    "type": "video",
    "title": "Welcome Video",
    "active": true,
    "views": 15,
    "media": { "..." : "..." },
    "createdAt": 1716000000000
  }
}

Errors

StatusMessageCause
400share/invalid-idInvalid ObjectId format.

Update a share

PUT /shares/:id

All fields are optional. Accepted fields depend on the share type.

Path parameters

ParameterTypeDescription
idstringShare ID.

Common fields

FieldTypeRequiredNotes
titlestringNo
descriptionstringNo
activebooleanNo
disableAdsbooleanNo

Video-specific fields

FieldTypeRequiredNotes
ctaEnabledbooleanNo
ctaTitlestringNo
ctaUrlstringNo

Contact form-specific fields

FieldTypeRequiredNotes
autoCreateContactbooleanNo
existingContactUpdatestringNo"overwrite" or "fill-empty", as on create.
contactInformationobjectNoSame structure as create.
customFieldsarrayNoSame structure and rules as create. Replaces the whole array.
autoResponseobjectNoSame structure and rules as create. Replaces the whole object.

Example request

curl -X PUT https://<host>/api/shares/677a... \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{ "title": "Updated Welcome Video", "active": false }'

Response

{
  "success": true,
  "data": {
    "_id": "677a...",
    "title": "Updated Welcome Video",
    "active": false
  }
}

Delete a share

DELETE /shares/:id

Deletes a share using a transaction — removes the share document and unsets shareId from all related media documents.

Path parameters

ParameterTypeDescription
idstringShare ID.

Example request

curl -X DELETE https://<host>/api/shares/677a... \
  -H "X-API-Key: your-api-key"

Response

{
  "success": true,
  "message": "Success"
}

Errors

StatusMessageCause
400share/invalid-idInvalid ObjectId format.

Response envelope

All endpoints use the standard envelope:

{
  "error": false,
  "success": true,
  "message": "...",
  "code": 200,
  "data": "...",
  "pagination": {}
}

Error responses:

{
  "error": true,
  "success": false,
  "message": "...",
  "code": 400,
  "data": null
}

data is null for a general failure, such as a database error or an upstream timeout. When the request is rejected because of a specific field, data carries the field(s) at fault instead:

{
  "error": true,
  "success": false,
  "message": "phone: Phone parse failed: The string supplied did not seem to be a phone number (input: <masked>)",
  "code": 400,
  "data": {
    "issues": [
      {
        "path": "phone",
        "message": "Phone parse failed: The string supplied did not seem to be a phone number (input: <masked>)"
      }
    ]
  }
}

Each issue carries a path (the dotted path into the request body) and a message. A label is added when the path is not self-describing, which today means custom fields only, for example "path": "fields.68b0a1f39c2e4d0012ab77e5", "label": "Class Year". issues is an array, but treat it as holding at most one entry: validation stops at the first failure.

On this page