Greetings and welcome to the documentation of the tl;dv API! This API is designed to seamlessly integrate tl;dv functionalities into your applications, making it easier for your users to access transcripts, notes, or import your meetings. Let's explore the details together.
Current API Version: v1alpha1 (v1 Alpha 1).
You are pioneering the tl;dv API, and we're excited to have you on board! Embark with us on the alpha phase adventure! Expect upcoming changes as we sculpt this API masterpiece, evolving towards the stable v1 release. Your invaluable feedback during this period will be our guiding star. Anticipate a slew of updates, and maybe a surprise or two – who said versioning can't be thrilling?
In the tl;dv API universe, we're currently all about that production data. So there is no alternate realities or sandbox just yet. We're working on it, though, so stay tuned!
Production API Endpoint: https://pasta.tldv.io (brace yourselves, this endpoint may choose to change its pasta shape, but we will keep you updated)
To unlock the tl;dv Public API's treasure trove, you need the key – THE API key - that's it. It's the golden ticket to seamless integration.
Once you have your API key, you can include it in the header of all your API requests.
x-api-key: YOUR_API_KEY
Replace YOUR_API_KEY with the actual key. If you're using a client library, it should be as simple as setting the header value.
All API requests must be made over HTTPS. Requests made over plain HTTP will be rejected.
Each request must include a valid API key. Requests without a valid key will return an authentication error.
The tl;dv Public API follows the same underlying permission model as the web application, with additional plan-based constraints applied to automation and export capabilities.
Seeing a meeting in the web app does not automatically guarantee programmatic (API) access.
Meeting ownership is determined by the calendar invite organizer or the meeting organizer (if the meeting was created spontaneously)
If you rely on API access, make sure:
To summarize:
| Meeting Organizer Plan | UI Access | API Access |
|---|---|---|
| Free | ✅ Yes (if shared) | ❌ No |
| Pro / Business | ✅ Yes | ✅ Yes |
| Enterprise | ✅ Yes | ✅ Yes |
Webhooks allow you to receive real-time notifications from tl;dv when key events occur in your workspace.
| Trigger | Fires when … |
|---|---|
| MeetingReady | A meeting has finished processing and is ready to be used in your applications. |
| TranscriptReady | A meeting transcript has been generated and is available. |
Webhooks can be configured at different levels, depending on your needs:
You can enable one or both triggers ("MeetingReady", "TranscriptReady") at the level that makes the most sense for your setup. This gives you control over how broadly you want setup your webhooks.
When creating a subscription you can ask tl;dv to embed extra data in the payload through the include option (in the app: the Include meeting transcript checkbox). Sections are opt-in and only offered on the triggers that support them:
| Section | Trigger | Adds |
|---|---|---|
transcript |
MeetingReady | data.transcript, the same object the TranscriptReady webhook sends (id, meetingId and the data sentences). It is null when the meeting has no completed transcript, and the key is absent when the section is not enabled. |
Embedding the transcript makes the payload considerably larger (roughly 1–2 KB per minute of speech, so a few hundred KB for a long meeting). Make sure your endpoint accepts bodies of that size and answers with a 2xx quickly; tl;dv waits at most 10 seconds for a response before retrying.
When something goes wrong, the API returns a JSON body that describes what happened. The shape of that body depends on the type of error: validation errors (bad or invalid request data) use one format, and all other errors use a simpler format. Here’s what to expect.
Returned when the request is malformed or fails validation (e.g. invalid parameters, missing required fields, wrong types). Use this to fix the request and retry.
| Field | Type | Description |
|---|---|---|
message |
string | A short summary of the problem (e.g. Invalid query params, check 'errors' property for more info.). |
errors |
array | List of validation issues. Each item has property (the field that failed) and constraints (key-value pairs describing what went wrong). |
Example (invalid filter on GET /meetings — e.g. meetingType must be internal or external):
{
"message": "Invalid query params, check 'errors' property for more info.",
"errors": [
{
"property": "meetingType",
"constraints": { "isEnum": "meetingType must be one of the following values: internal, external" }
}
]
}
Used for authentication, authorization, not-found, and server errors. Same structure for all of these status codes.
| Field | Type | Description |
|---|---|---|
name |
string | Error code or type (e.g. UnauthorizedError, NotFoundError). |
message |
string | Human-readable description of the error. |
Example:
{
"name": "NotFoundError",
"message": "Meeting not found"
}
Check the HTTP status code and the name / message fields to decide whether to retry, fix the request, or contact support.
Need assistance, have a brilliant idea, or just want to say hello? Our support team is ready and waiting at support@tldv.io. Alternatively, drop by via the Intercom Widget.
Have fun! 🎉
Meetings (or recordings) related endpoints. With tl;dv you can manage your meetings. So far you can only import new ones, but more functionalities are coming soon.
Import a meeting, recording, or other media from a URL. The URL must be publicly accessible, and the media must be in a supported format.
MeetingImportControllerBody
| name required | string The name of the meeting/recording imported |
| url required | string <url> The url of the meeting/recording imported. Important: the url must be publicly accessible and the media must be in a supported format. Supported formats are: .mp3, .mp4, .wav, .m4a, .mkv, .mov, .avi, .wma, .flac. |
| happenedAt | string\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d.... The date of the meeting/recording. If not provided, the current date will be used |
| dryRun | boolean For testing purposes: whether to run the import as a dry run. If true, the import will not be persisted to the database, and nothing will be run. |
| participants | Array of strings An array of emails of the invited participants to the meeting/recording |
| phoneNumber | string <= 64 characters A phone number associated with the meeting/recording, e.g. the other party of a phone call. It is stored and returned verbatim on the meeting (GET /meetings/{id}) and in the MeetingReady webhook.Max 64 characters. |
object <= 20 properties Flat key/value object with your own identifiers (record ids, call ids, ...) so you can correlate the meeting when it comes back on GET /meetings/{id} and in the MeetingReady webhook. Values are stored and returned verbatim. Limits: at most 20 keys; keys must match ^[A-Za-z0-9_-]{1,64}$; values must be strings of at most 256 characters, numbers or booleans. Nested objects and arrays are not accepted. |
{- "name": "string",
- "url": "string",
- "happenedAt": "string",
- "dryRun": true,
- "participants": [
- "string"
], - "phoneNumber": "+1 555 010 0100",
- "metadata": {
- "crmRecordId": "rec_123",
- "callId": 42,
- "inbound": true
}
}{- "success": true,
- "jobId": "string",
- "message": "string"
}| query | string The query to search for |
| page | number > 0 The page number to return. Default is 1 |
| limit | number ( 0 .. 100 ] The number of results to return per page. Default is 50. Max is 100. The total number of results cannot exceed 10,000. If this limit is exceeded, refine the date range or period. |
string or string The date to search from | |
string or string The date to search to | |
| onlyParticipated | boolean Whether to only return meetings the user participated in. Default is false |
| meetingType | string Enum: "internal" "external" Filters meetings by type: internal (all participants from the same organization) or external (includes at least one participant from another organization). Defaults to both if not provided |
const axios = require('axios'); let config = { method: 'get', maxBodyLength: Infinity, url: 'https://pasta.tldv.io/v1alpha1/meetings?<query-params>', headers: { 'Content-Type': 'application/json', 'x-api-key': '••••••' } }; axios.request(config) .then((response) => { console.log(JSON.stringify(response.data)); }) .catch((error) => { console.log(error); });
{- "page": 0,
- "pages": 0,
- "total": 0,
- "pageSize": 0,
- "results": [
- {
- "id": "string",
- "name": "string",
- "happenedAt": "string",
- "url": "string",
- "duration": 0,
- "organizer": {
- "name": "string",
- "email": "string"
}, - "invitees": [
- {
- "name": "string",
- "email": "string"
}
], - "template": "string",
- "extraProperties": {
- "conferenceId": "string"
}, - "phoneNumber": "+1 555 010 0100",
- "metadata": {
- "crmRecordId": "rec_123",
- "callId": "call_456"
}
}
]
}Get a meeting by its id. The meeting is returned in a human readable format
| meetingId required | string[^\/#\?]+? |
const axios = require('axios'); let config = { method: 'get', maxBodyLength: Infinity, url: 'https://pasta.tldv.io/v1alpha1/meetings/653663ac7c8dbd00130f11d9', headers: { 'Content-Type': 'application/json', 'x-api-key': '••••••' } }; axios.request(config) .then((response) => { console.log(JSON.stringify(response.data)); }) .catch((error) => { console.log(error); });
{- "id": "string",
- "name": "string",
- "happenedAt": "string",
- "url": "string",
- "duration": 0,
- "organizer": {
- "name": "string",
- "email": "string"
}, - "invitees": [
- {
- "name": "string",
- "email": "string"
}
], - "template": "string",
- "extraProperties": {
- "conferenceId": "string"
}, - "phoneNumber": "+1 555 010 0100",
- "metadata": {
- "crmRecordId": "rec_123",
- "callId": "call_456"
}
}Returns an HTTP redirect to a signed, expiring download URL for the meeting recording.
Redirect behavior:
Location header to obtain the signed URL.Signed URL TTL: The signed URL expires 6 hours after issuance.
| meetingId required | string[^\/#\?]+? |
const axios = require('axios'); let config = { method: 'get', maxBodyLength: Infinity, url: 'https://pasta.tldv.io/v1alpha1/meetings/653663ac7c8dbd00130f11d9/download', headers: { 'Content-Type': 'application/json', 'x-api-key': '••••••' } }; axios.request(config) .then((response) => { console.log(JSON.stringify(response.data)); }) .catch((error) => { console.log(error); });
Beta. Recording uploads are in beta and are enabled on request. To get access, contact support@tldv.io. Until your user is enabled, these endpoints answer
404.
Start uploading a recording from your own system. Declare the exact file size and the meeting details, and receive the part plan with a signed upload URL per part.
Uploading a recording is a three-step process:
POST /meetings/upload): declare the exact file size and the meeting details. The response is a
plan: the file is split into numbered parts, each with the byte range to send and a signed URL to send it to.PUT the bytes of its byte range to its URL, with no extra headers. The
server answers 200 when the part is stored. Parts can be sent in parallel. A body of the wrong length is
rejected with 403.POST /meetings/upload/{uploadId}/complete): Once you have uploaded all parts, POST to this endpoint to complete the upload. The answer carries the import job id.To resume after an interruption or once a URL has expired, fetch the upload status
(GET /meetings/upload/{uploadId}): it tells which parts are already stored and returns fresh URLs for the rest.
An upload must be completed within the window given by expiresAt; after it, initiate a new one.
Limits: files up to 2 GiB (2,147,483,648 bytes). The daily import quota and the public-API access rules apply as for URL imports.
InitiateUploadBody
| name required | string <= 500 characters The name of the meeting/recording that will be created from the upload |
| sizeBytes required | integer >= 1 Exact size of the file in bytes. The part plan and the signed upload URLs are derived from it, so it must match the file byte for byte: a part whose body does not have the planned length is rejected by the storage. Maximum 2 GiB (2,147,483,648 bytes); larger files are rejected with |
| fileName | string <= 255 characters Original file name, kept for reference only |
| contentType | string <= 100 characters MIME type of the file when known. Supported families: video/, audio/, application/octet-stream. The media is validated again during processing regardless of this value. |
| happenedAt | string\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d.... The date of the meeting/recording. If not provided, the current date will be used |
| participants | Array of strings <= 100 items An array of emails of the invited participants to the meeting/recording |
| phoneNumber | string <= 64 characters A phone number associated with the meeting/recording, e.g. the other party of a phone call. It is stored and returned verbatim on the meeting (GET /meetings/{id}) and in the MeetingReady webhook. Max 64 characters. |
object <= 20 properties Flat key/value object with your own identifiers (record ids, call ids, ...) so you can correlate the meeting when it comes back on GET /meetings/{id} and in the MeetingReady webhook. Values are stored and returned verbatim. Limits: at most 20 keys; keys must match ^[A-Za-z0-9_-]{1,64}$; values must be strings of at most 256 characters, numbers or booleans. Nested objects and arrays are not accepted. |
{- "name": "string",
- "sizeBytes": 734003200,
- "fileName": "call-2026-09-16.mp4",
- "contentType": "video/mp4",
- "happenedAt": "string",
- "participants": [
- "string"
], - "phoneNumber": "+1 555 010 0100",
- "metadata": {
- "crmRecordId": "rec_123",
- "callId": 42,
- "inbound": true
}
}{- "uploadId": "string",
- "status": "Pending",
- "sizeBytes": 0,
- "partSize": 0,
- "partCount": 0,
- "expiresAt": "2019-08-24T14:15:22Z",
- "parts": [
- {
- "partNumber": 0,
- "byteRange": {
- "start": 0,
- "end": 0
}, - "size": 0,
- "uploaded": true,
- "url": "string",
- "urlExpiresAt": "2019-08-24T14:15:22Z"
}
], - "jobId": "string"
}Beta. Enabled on request; see Initiate a recording upload for how to get access.
Returns the part plan with, for each part, whether the storage already holds it. Parts still missing come
with a fresh signed URL, so this is the call to make to resume an interrupted upload or after a URL expired.
status tells whether the upload is still pending or was completed; the import job id is present either way.
| uploadId required | string[^\/#\?]+? |
{- "uploadId": "string",
- "status": "Pending",
- "sizeBytes": 0,
- "partSize": 0,
- "partCount": 0,
- "expiresAt": "2019-08-24T14:15:22Z",
- "parts": [
- {
- "partNumber": 0,
- "byteRange": {
- "start": 0,
- "end": 0
}, - "size": 0,
- "uploaded": true,
- "url": "string",
- "urlExpiresAt": "2019-08-24T14:15:22Z"
}
], - "jobId": "string"
}Beta. Enabled on request; see Initiate a recording upload for how to get access.
Discards an upload that will not be completed: the parts already stored are dropped. Aborting an upload that already expired or was aborted is a no-op.
| uploadId required | string[^\/#\?]+? |
{- "message": "string",
- "error": [
- {
- "message": "Invalid body, check 'errors' property for more info.",
- "errors": [
- {
- "property": "name",
- "constraints": {
- "property1": "name must be a string",
- "property2": "name must be a string"
}
}
]
}
]
}Beta. Enabled on request; see Initiate a recording upload for how to get access.
Call once every part has been uploaded to complete the upload.
Safe to retry: calling it again for a completed upload returns the same job id. If parts are missing the
answer is 409 with their numbers; upload them and call complete again.
| uploadId required | string[^\/#\?]+? |
{- "success": true,
- "jobId": "string",
- "message": "string"
}Transcripts related endpoints. With tl;dv you can get the transcript of a meeting, in a structured format.
Get the transcript of a meeting by its id. The transcript is returned in a human readable format. The transcript is returned only if it is complete
| meetingId required | string[^\/#\?]+? |
const axios = require('axios'); let config = { method: 'get', maxBodyLength: Infinity, url: 'https://pasta.tldv.io/v1alpha1/meetings/653663ac7c8dbd00130f11d9/transcript', headers: { 'Content-Type': 'application/json', 'x-api-key': '••••••' } }; axios.request(config) .then((response) => { console.log(JSON.stringify(response.data)); }) .catch((error) => { console.log(error); });
{- "id": "string",
- "meetingId": "string",
- "data": [
- {
- "speaker": "string",
- "text": "string",
- "startTime": 0,
- "endTime": 0
}
]
}Notes related endpoints. With tl;dv you can get the notes of a meeting, in a structured format and in Markdown format.
Get the notes of a meeting by its id. The notes are returned in a human readable format
| meetingId required | string[^\/#\?]+? |
const axios = require('axios'); let config = { method: 'get', maxBodyLength: Infinity, url: 'https://pasta.tldv.io/v1alpha1/meetings/653663ac7c8dbd00130f11d9/notes', headers: { 'Content-Type': 'application/json', 'x-api-key': '••••••' } }; axios.request(config) .then((response) => { console.log(JSON.stringify(response.data)); }) .catch((error) => { console.log(error); });
{- "structuredNotes": [
- {
- "segmentId": "string",
- "timestamp": 0,
- "text": "string",
- "topicId": "string"
}
], - "markdownContent": "string",
- "topics": [
- {
- "id": "string",
- "order": 0,
- "title": "string",
- "summary": "string"
}
]
}Highlights (or notes) related endpoints. With tl;dv you can get the highlights of your meetings.
(deprecated) Get the meetings notes of a meeting, by its id. The meetings notes are returned in a human readable format and they are returned only if the transcript is complete. Use the /meetings/:meetingId/notes endpoint instead.
| meetingId required | string[^\/#\?]+? |
const axios = require('axios'); let config = { method: 'get', maxBodyLength: Infinity, url: 'https://pasta.tldv.io/v1alpha1/meetings/653663ac7c8dbd00130f11d9/highlights', headers: { 'Content-Type': 'application/json', 'x-api-key': '••••••' } }; axios.request(config) .then((response) => { console.log(JSON.stringify(response.data)); }) .catch((error) => { console.log(error); });
{- "meetingId": "string",
- "data": [
- {
- "text": "string",
- "startTime": 0,
- "source": "manual",
- "topic": {
- "title": "string",
- "summary": "string"
}
}
]
}| id required | string non-empty Unique identifier for the webhook payload |
| event required | string Value: "MeetingReady" The event type that triggered this webhook |
required | object (MeetingWebhookPayload) Meeting data included in a MeetingReady webhook event |
| executedAt required | string non-empty The date and time the webhook was executed |
{- "id": "webhook-123",
- "event": "MeetingReady",
- "data": {
- "id": "meeting-123",
- "name": "Team Standup Meeting",
- "happenedAt": "2024-01-15T10:00:00Z",
- "template": {
- "id": "template-1",
- "label": "Standup Template"
}, - "extraProperties": {
- "conferenceId": "conf-123"
}, - "phoneNumber": "+1 555 010 0100",
- "metadata": {
- "crmRecordId": "rec_123"
}, - "duration": 1800,
- "notes": {
- "markdownContent": "## Summary\n- Discussed progress and blockers",
- "structuredNotes": [
- {
- "segmentId": "segment-1",
- "timestamp": 12,
- "text": "Discussed progress and blockers",
- "topicId": "topic-1"
}
], - "topics": [
- {
- "id": "topic-1",
- "order": 0,
- "title": "Progress update",
- "summary": "Where the team stands"
}
]
}, - "transcript": {
- "id": "transcript-123",
- "meetingId": "meeting-123",
- "data": [
- {
- "speaker": "John Doe",
- "text": "Hello everyone, welcome to our standup meeting.",
- "startTime": 0,
- "endTime": 5
}
]
}
}
}This webhook is sent when a meeting transcript is ready. You can configure this webhook to receive notifications when transcripts are successfully generated.
| id required | string non-empty Unique identifier for the webhook payload |
| event required | string Value: "TranscriptReady" The event type that triggered this webhook |
required | object (GetTranscriptByMeetingIdResponse) |
| executedAt required | string non-empty The date and time the webhook was executed |
{- "id": "webhook-456",
- "event": "TranscriptReady",
- "data": {
- "id": "meeting-123",
- "meetingId": "meeting-123",
- "data": {
- "transcript": "Hello everyone, welcome to our standup meeting. Today we will discuss our progress and any blockers...",
- "segments": [
- {
- "startTime": 0,
- "endTime": 5,
- "text": "Hello everyone, welcome to our standup meeting."
}
]
}
}
}