TheirStory API Guide

Overview

TheirStory provides a web-based interviewing and storytelling platform that helps families and organizations collect and make accessible audiovisual oral histories. This guide details how to interact with the TheirStory HTTP API to authenticate, read user and organization data, and access stories, transcripts, and published media.

This document covers the read API. Write operations (creating projects, uploading recordings, editing stories) are not yet publicly documented; contact us if you need them.

Base URL

The API base URL is https://node.theirstory.io. All examples below use this base URL. The API is served over HTTPS only.

Authentication

The API uses JSON Web Tokens (JWTs). You obtain a token by calling POST /signin with email and password credentials, and you include the token on every subsequent request.

Authorization header format (important)

The Authorization header on this API does not use the Bearer scheme. Send the raw JWT as the header value, with no Bearer prefix. This differs from the convention described in RFC 6750 and from the default behavior of most API testing tools (Postman, Insomnia, curl examples copied from other APIs) and many client libraries.

Correct:

Authorization: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

Incorrect (will return 403):

Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

If you receive 403 Forbidden on every call despite having a valid token, this is the first thing to check. We plan to add support for the standard Bearer scheme; until then, configure your tooling accordingly.

Postman: Set the Authorization type to "API Key", with the key set to Authorization, the value set to the raw token (not "Bearer Token"), and "Add to" set to "Header".

curl:

curl -H "Authorization: $TS_TOKEN" \
  "https://node.theirstory.io/current_user?visitorId=..."

Token storage in browser clients

If you are calling this API from server-side code, store the token in your server's secret store (environment variables, a secrets manager, etc.) and never send it to the browser.

If you are calling this API directly from a browser, you have three options, all with tradeoffs:

Option XSS-readable? Survives page refresh? Notes
In-memory No No Safest, but the user must sign in again on every reload.
sessionStorage Yes No (cleared on tab close) Convenient; vulnerable to XSS.
localStorage Yes Yes Most convenient; vulnerable to XSS. This is what the sample code below uses.

The sample code below uses localStorage for simplicity. This is a convenience tradeoff, not a security recommendation. Any script running on your page — including third-party tags, dependencies, or extensions — can read localStorage and exfiltrate the token. For production browser integrations, the safest pattern is a backend-for-frontend (BFF): your server holds the TheirStory token, and your browser code authenticates against your own server with a session cookie. The browser never sees the TheirStory token.

We are tracking work to support HttpOnly cookie-based session auth, which would give browser clients a genuinely XSS-resistant option directly against this API.

Token lifetime and refresh

Tokens are JWTs signed by the API. Expiry is set on the token itself; decode the JWT payload to read the exp claim if you need to know when to re-authenticate. There is currently no dedicated refresh endpoint — to obtain a new token, call POST /signin again. Plan for this in any long-lived integration.

Conventions

Request and response format

Field naming

Field names are inconsistent across the API: some use snake_case (first_name, organizational_code), others use camelCase (maxProjectsAllowed, backgroundUrl). Treat field names as case-sensitive and refer to the per-endpoint documentation for the exact spelling. Normalizing field naming is on our backlog.

Internal fields

Several responses include fields prefixed with an underscore (_id) or named __v. These are storage-layer internals that we currently expose for backwards compatibility. Treat them as read-only and avoid relying on __v in your application logic; we may remove or stop populating it in the future.

Pagination

Only GET /stories currently supports pagination, via page and pageSize query parameters. Other list endpoints (/projects, /organization) return all records the caller has access to. If you expect a large number of records, please contact us before integrating so we can advise.

API endpoints

Sign in

Authenticates a user and returns user information along with an access token.

POST/signin

Request body

{
  "email": "user@example.com",
  "password": "yourpassword"
}

Response

A JSON object containing a JWT token and detailed information about the authenticated user.

Response fields

FieldTypeDescription
tokenstringJWT authentication token. Send as the raw Authorization header value on subsequent requests — see Authorization header format.
full_namestringUser's full name.
user_idstringUnique identifier for the user.
rolestringUser's role (e.g. admin, member).
emailstringUser's email address.
user_detailsobjectDetailed user profile. See below.
organization_infoobjectUser's primary organization. See below.
api_versionstringVersion of the API that served the response.
create_gallerybooleanWhether this user may create galleries.

user_details contains:

FieldTypeDescription
idnumberNumeric user ID.
emailstringUser's email.
first_namestringUser's first name.
last_namestringUser's last name.
photo_file_namestring | nullFilename of the user's photo, if any.
photo_content_typestring | nullMIME type of the user's photo, if any.
organizationsarrayOrganizations the user belongs to.

Each entry in organizations contains id, name, subdomain, custom_domain, description (HTML), and addresses[].

organization_info contains organization-level settings and quotas. See the Organization endpoint for the field list — the same shape is used here.

Example response

{
  "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.AbCdEfGh",
  "full_name": "John Doe",
  "user_id": "1234567890abcdef",
  "role": "admin",
  "email": "user@example.com",
  "user_details": {
    "id": 12345,
    "email": "user@example.com",
    "first_name": "John",
    "last_name": "Doe",
    "photo_file_name": null,
    "photo_content_type": null,
    "organizations": [
      {
        "id": 67,
        "name": "Example Organization",
        "subdomain": "example",
        "custom_domain": "",
        "description": "Example Organization is a platform that helps collect and preserve stories through interviews.",
        "addresses": [
          {
            "address_line_1": "123 Main Street",
            "address_line_2": "",
            "city": "Anytown",
            "state": "NY",
            "zipcode": "12345",
            "country": "US"
          }
        ]
      }
    ]
  },
  "organization_info": {
    "_id": "abcdef1234567890",
    "organization_name": "Example Admin",
    "organizational_code": "example_admin_123456",
    "permanent_org": {
      "export_allowed": true,
      "archive_nbr": "01ab-2345"
    },
    "settings": {
      "transcription_service": "cloudconvert",
      "filler_words": false,
      "vocabulary": [],
      "auto_chaptering": true,
      "summary_prompt": "Create a concise summary of this interview focusing on key topics discussed.",
      "chaptering_prompt": "Create an index for this interview with timecodes, titles, summaries, and keywords."
    },
    "branding": false,
    "activity": true,
    "org_manager_view_all": true,
    "export_allowed": true,
    "require_session_passwords": false,
    "local_recordings_allowed": true,
    "allow_dial_in": true,
    "local_recordings_polyfill": false,
    "groups": ["group123", "group456", "group789"],
    "credits": 1000000,
    "maxProjectsAllowed": 200,
    "projects": 150,
    "recording_hours": 320,
    "backgroundUrl": "uploads/projectbackgrounds/example-background-image.jpg",
    "created_at": "2023-01-15T12:30:45.678Z",
    "__v": 5
  },
  "api_version": "1.2.3",
  "create_gallery": true
}

Example code

const response = await fetch('https://node.theirstory.io/signin', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Accept': 'application/json'
  },
  body: JSON.stringify({ email, password })
});

if (!response.ok) {
  throw new Error('Login failed');
}

const data = await response.json();

// See "Token storage in browser clients" for tradeoffs.
// localStorage is convenient but XSS-readable.
localStorage.setItem('authToken', data.token);

Note on export_allowed vs aviary_export_allowed: Some organizations use aviary_export_allowed in place of export_allowed to indicate permission to export to Aviary specifically. If both are present, treat aviary_export_allowed as the more specific flag. Both fields are returned as-is and we are working to consolidate them.

Current user information

Retrieves information about the currently authenticated user.

GET/current_user?visitorId={visitorId}

Query parameters

ParameterRequiredDescription
visitorId yes The visitor ID associated with this session. The value is embedded in your JWT's visitor-id claim — decode the JWT payload to retrieve it, or persist the value your client generated at sign-in.

Example request

GET /current_user?visitorId=7fe32a19-84c5-4921-b517-29d7c8e54f3a
Authorization: <jwt>

Response

Same shape as the Sign in response, including a refreshed token. The user may belong to multiple organizations; user_details.organizations is an array.

Example response (abbreviated)

{
  "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "full_name": "Jane Smith",
  "user_id": "847629394863",
  "role": "admin",
  "email": "jane.smith@example.com",
  "user_details": {
    "id": 12345,
    "email": "jane.smith@example.com",
    "first_name": "Jane",
    "last_name": "Smith",
    "organizations": [
      { "id": 78, "name": "Community Archives Project", "subdomain": "communityarchives" },
      { "id": 92, "name": "Metro Historical Society",   "subdomain": "metrohistory" }
    ]
  },
  "organization_info": { "...": "see Sign in response" },
  "api_version": "1.2.3",
  "create_gallery": true
}

Projects

Retrieves projects the authenticated user has access to.

GET/projects

Response

An array of project objects.

Response fields

FieldTypeDescription
_idstringUnique identifier for the project.
namestringProject name.
descriptionstringProject description.
creatorstringUser ID of the project's creator.
backgroundUrlstringStorage path to the background image.
projectBackgroundUrlstringFully resolved URL to the background image.
displayTitlebooleanWhether to display the project title in the UI.
defaultProjectForOrganizationbooleanWhether this is the organization's default project.
organizationstringOrganization ID that owns the project.
organizationNamestringName of the owning organization.
view_only_codestringInvitation code for view-only access.
view_only_code_created_atstringWhen the view-only code was created (ISO 8601).
member_codestringInvitation code for member access.
member_code_created_atstringWhen the member code was created.
project_manager_codestringInvitation code for project manager access.
project_manager_code_created_atstringWhen the project manager code was created.
foldersarrayFolders in the project. See below.
storiesInProjectnumberTotal stories in the project.
isManagerbooleanWhether the authenticated user manages this project.
created_atstringWhen the project was created.
__vnumberInternal version (see Internal fields).

Each folder contains: _id, name, creator, project, defaultFolderForProject, created_at, __v, and an fu array of folder-user relationships. Each fu entry contains: _id, user, accepted, creator, folder, created_at, __v.

Invitation codes are sensitive. view_only_code, member_code, and project_manager_code grant access to the project. Treat them like passwords: don't log them, don't surface them in URLs you share publicly, and don't include them in browser-visible code unless the user is supposed to be sharing them.

Example response

[
  {
    "_id": "proj123456789",
    "name": "Community Archives",
    "description": "Archive of local community stories and interviews",
    "creator": "user987654321",
    "backgroundUrl": "uploads/projectbackgrounds/community-archives-bg.jpg",
    "displayTitle": true,
    "defaultProjectForOrganization": false,
    "organization": "org123456789",
    "organizationName": "Community Archives Organization",
    "view_only_code": "aBcD1234EfGh5i6",
    "view_only_code_created_at": "2024-01-15T14:26:58.682Z",
    "member_code": "jKlM7890nOpQ1r2",
    "member_code_created_at": "2024-01-15T14:26:58.987Z",
    "project_manager_code": "sTuV3456wXyZ7a8",
    "project_manager_code_created_at": "2024-10-15T14:30:21.837Z",
    "projectBackgroundUrl": "https://storage.example.com/uploads/projectbackgrounds/community-archives-bg.jpg?token=abc123",
    "folders": [
      {
        "_id": "fold123456789",
        "name": "Senior Interviews",
        "creator": "user987654321",
        "project": "proj123456789",
        "defaultFolderForProject": false,
        "created_at": "2024-01-20T12:57:05.461Z",
        "__v": 0,
        "fu": [
          {
            "_id": "foldu123456789",
            "user": "user987654321",
            "accepted": true,
            "creator": true,
            "folder": "fold123456789",
            "created_at": "2024-01-20T12:57:05.484Z",
            "__v": 0
          }
        ]
      }
    ],
    "storiesInProject": 32,
    "isManager": true,
    "created_at": "2024-01-15T14:26:14.086Z",
    "__v": 0
  }
]

Organization

Retrieves the organizations the authenticated user has access to.

GET/organization

Response

An object containing a single organizations array.

Response fields

Each organization in the array contains:

FieldTypeDescription
_idstringUnique identifier for the organization.
organization_namestringDisplay name.
organizational_codestringUnique slug-style code.
activitybooleanWhether the organization is active.
org_manager_view_allbooleanWhether org managers can view all projects.
export_allowedbooleanGeneral export permission.
aviary_export_allowedbooleanAviary-specific export permission (when present).
require_session_passwordsbooleanWhether recording sessions require passwords.
local_recordings_allowedbooleanWhether local recordings are permitted.
local_recordings_polyfillbooleanWhether the local-recording polyfill is enabled.
allow_dial_inbooleanWhether dial-in recording is enabled.
brandingbooleanWhether custom branding is enabled.
groupsarrayIDs of groups in the organization.
permanent_orgobjectPermanence details (export_allowed, archive_nbr).
settingsobjectTranscription/AI settings; see below.
usersarrayUsers in the organization; see below.
creditsnumberAvailable credits.
maxProjectsAllowednumberProject quota.
projectsnumberCurrent project count.
recording_hoursnumberRecording-hour quota.
backgroundUrlstringStorage path to the organization's background image.
created_atstringCreation timestamp.
__vnumberInternal version.

settings contains:

FieldTypeDescription
transcription_servicestringTranscription provider (e.g. cloudconvert).
filler_wordsbooleanWhether filler words are kept in transcripts.
vocabularyarrayCustom vocabulary terms for transcription.
auto_chapteringbooleanWhether automatic chaptering is enabled.
summary_promptstringLLM prompt used for summary generation.
chaptering_promptstringLLM prompt used for chapter generation.

Each user in users contains: _id, is_verified, role, stories, full_name, email, organization, signed_up, __v, create_gallery, and featureFlags (an object with enableTwilioVideo, localRecordingMimeType, disableAutomaticResolutionSwitching, autoChaptering, _id).

Example response (abbreviated)

{
  "organizations": [
    {
      "_id": "org123456789",
      "organization_name": "Community Archives Project",
      "organizational_code": "cap_admin_2023",
      "activity": true,
      "org_manager_view_all": true,
      "aviary_export_allowed": true,
      "require_session_passwords": false,
      "groups": ["group1234", "group5678", "group9012"],
      "permanent_org": { "export_allowed": true, "archive_nbr": "07kp-2341" },
      "settings": {
        "transcription_service": "cloudconvert",
        "filler_words": false,
        "vocabulary": ["oral history", "archives", "cultural heritage"],
        "auto_chaptering": true,
        "summary_prompt": "Create a concise summary...",
        "chaptering_prompt": "Divide this interview into logical chapters..."
      },
      "users": [
        {
          "_id": "user123456",
          "is_verified": false,
          "role": "admin",
          "full_name": "Emma Johnson",
          "email": "emma.johnson@example.com",
          "organization": "org123456789",
          "signed_up": "2023-05-12T15:32:47.821Z",
          "create_gallery": true,
          "featureFlags": {
            "enableTwilioVideo": true,
            "localRecordingMimeType": "video/webm;codecs=vp8,opus",
            "disableAutomaticResolutionSwitching": false,
            "autoChaptering": true,
            "_id": "flag123456"
          },
          "__v": 0
        }
      ],
      "credits": 1750000,
      "maxProjectsAllowed": 300,
      "projects": 187,
      "recording_hours": 428,
      "backgroundUrl": "uploads/projectbackgrounds/community-archives-bg.jpg",
      "branding": true,
      "created_at": "2023-04-15T14:27:42.519Z",
      "__v": 5
    }
  ]
}

Stories

Retrieves stories (recordings) accessible to the authenticated user, with pagination.

GET/stories?page={page}&pageSize={pageSize}

Query parameters

ParameterRequiredDefaultDescription
pageno1Page number (1-indexed).
pageSizeno15Items per page.

Response fields

FieldTypeDescription
pagenumberCurrent page number.
totalnumberTotal stories matching the request.
itemsarrayStory objects on this page; see below.

Each story object contains at minimum: _id, title, record_date (ISO 8601), duration (seconds), author, indexes (chapter metadata), and asyncOperations (processing history). The full per-story shape matches the story field in the transcript JSON response — use that as the canonical reference.

Example response

{
  "page": 1,
  "total": 234,
  "items": [
    {
      "_id": "def456ghi789jkl012",
      "title": "Example Recording",
      "record_date": "2025-01-27T16:44:51.289Z",
      "duration": 107,
      "author": { "id": 12345, "first_name": "John", "last_name": "Doe" },
      "indexes": [],
      "asyncOperations": []
    }
  ]
}

Get story transcript JSON

Retrieves a story's transcript in JSON format, including word-level timing and story metadata.

GET/transcripts/{storyId}
Path parameters
ParameterDescription
storyIdThe unique identifier of the story.
Response fields
FieldTypeDescription
transcriptobjectTranscript content and metadata; see below.
storyobjectStory metadata.
videoURLstringTime-limited URL to the video recording.
participantsarrayParticipants in the recording.
tagsarrayTags applied to the recording.

transcript contains:

FieldTypeDescription
_idstringTranscript identifier.
statusstringcompleted, processing, failed.
storyIdstringStory this transcript belongs to.
wordsarrayWord-level entries: { start, end, text } in seconds.
paragraphsarrayParagraph-level entries: { start, end, speaker }.
created_atstringWhen the transcript was created.
updated_atstringWhen the transcript was last updated.

videoURL is short-lived and signed. Don't cache or share it. Re-request the transcript to get a fresh URL.

Example response (abbreviated)
{
  "transcript": {
    "_id": "abc123def456ghi789",
    "status": "completed",
    "storyId": "def456ghi789jkl012",
    "words": [
      { "start": 1.12, "end": 1.44, "text": "Super" },
      { "start": 1.44, "end": 1.80, "text": "quickly." }
    ],
    "paragraphs": [
      { "start": 0, "end": 105.6, "speaker": "SPEAKER_S1" }
    ],
    "created_at": "2025-01-27T16:46:48.850Z",
    "updated_at": "2025-01-27T16:46:48.850Z"
  },
  "story": {
    "_id": "def456ghi789jkl012",
    "title": "Example Recording",
    "record_date": "2025-01-27T16:44:51.289Z",
    "duration": 107
  },
  "videoURL": "https://example.s3.amazonaws.com/uploads/example-id/transcoded.mp4?token=example",
  "participants": [],
  "tags": []
}

Get story transcript HTML

Retrieves a story's transcript as HTML with timing attributes, suitable for use with Hyperaudio Lite and similar interactive-transcript libraries.

GET/stories/{storyId}/html

Note: this endpoint is under /stories/... while the JSON variant above is under /transcripts/.... This is a historical inconsistency we plan to resolve; both will continue to work.

Response

An HTML document. Each word is wrapped in a <span> with timing data attributes:

AttributeDescription
data-mStart time of the word in milliseconds.
data-dDuration of the word in milliseconds.
class="speaker"Marks a speaker label.
Example response
<article>
  <section>
    <p>
      <span data-m="0" data-d="0" class="speaker">SPEAKER_S1: </span>
      <span data-m="1120" data-d="320">Super </span>
      <span data-m="1440" data-d="360">quickly. </span>
      <span data-m="1800" data-d="360">Then </span>
      ...
    </p>
  </section>
</article>

Groups

This endpoint is documented as available but its response shape is not yet finalized. Contact us if you need to integrate against it; we will update this section once the response is stable.

GET/groups

Publishing

The publishing endpoints create non-expiring media assets on our Mux-backed CDN, useful when you need stable URLs to embed in external systems.

Publish

POST/stories/{storyId}/publish_media
Request body
{ "format": "video" }

format accepts "video" or "audio".

Response
{ "url": "https://stream.mux.com/{uid}/highest.mp4" }

For audio:

{ "url": "https://stream.mux.com/{uid}/audio.m4a" }

Assets may take up to a minute to be created. The URL is returned immediately but may 404 or stream a placeholder until processing completes. There is currently no status endpoint or webhook; if you need to verify availability, poll the returned URL with a HEAD request until it succeeds. We recommend a poll interval of 5–10 seconds and a total timeout of 2 minutes.

Unpublish

POST/stories/{storyId}/unpublish_media
Request body
{ "format": "video" }
Response
{
  "message": "video deleted",
  "url": "https://stream.mux.com/{uid}/highest.mp4"
}

Example code

const response = await fetch(
  `https://node.theirstory.io/stories/${storyId}/publish_media`,
  {
    method: 'POST',
    headers: {
      'Authorization': token,             // raw JWT, no "Bearer" prefix
      'Content-Type': 'application/json',
      'Accept': 'application/json'
    },
    body: JSON.stringify({ format: 'video' })
  }
);

if (!response.ok) {
  throw new Error(`Publish failed: ${response.status}`);
}

const { url } = await response.json();

Error handling

The API uses standard HTTP status codes. Error responses are JSON objects of the shape:

{
  "error": "short_error_code",
  "message": "Human-readable description of what went wrong."
}
StatusMeaningCommon causes
400Bad RequestMalformed JSON, missing required parameters.
401UnauthorizedMissing, expired, or invalid token.
403ForbiddenValid token but insufficient permissions. Also returned if the Authorization header includes Bearer — see Authorization header format.
404Not FoundResource does not exist or the caller cannot see it.
429Too Many RequestsRate limit exceeded. See Rate limits.
500Internal Server ErrorUnexpected error. Retry with backoff and contact support if it persists.

Rate limits

The API enforces per-token rate limits. Exceeding the limit returns 429 Too Many Requests. We do not currently publish specific limits; design your integration to back off on 429 and retry after a delay of at least 1 second, doubling on repeated failures. Contact us if your integration needs sustained high throughput.

Security considerations

Versioning

The API URL is not versioned. Responses include an api_version field that reflects the server's deployed version (a semantic version string). Breaking changes are communicated through release notes; subscribe to repository updates or contact support to be notified.

Backwards-compatible changes (new fields, new endpoints) may be added without notice. Treat your response parsing accordingly — ignore fields you don't recognize rather than failing.

Support