{
  "openapi": "3.1.0",
  "info": {
    "title": "Craft API",
    "version": "1.0.0",
    "summary": "Read-only access to your company's Craft users, teams, field recordings, and calls.",
    "description": "The Craft API gives programmatic, read-only access to the data Craft produces for your company: the people on your team, the teams they belong to, AI-analyzed in-home field recordings, and AI-analyzed call-center calls — including full transcripts.\n\nAuthenticate with a company-scoped API key sent as a Bearer token. Successful responses are wrapped in an envelope with a `data` key; list endpoints add a `pagination` key. Failures return an `error` object: a human-readable `detail` string, or — when a query parameter is rejected — one entry per bad parameter.\n\nEvery endpoint is read-only: `POST`, `PUT`, `PATCH`, and `DELETE` return `405 Method Not Allowed`. Every request is scoped to the company that owns the API key, so records belonging to other companies are never returned — they are indistinguishable from records that do not exist and yield `404`.",
    "termsOfService": "https://www.craftflow.com/terms-of-service",
    "contact": {
      "name": "Craft Support",
      "email": "support@craftflow.com",
      "url": "https://docs.craftflow.com/api/introduction"
    }
  },
  "externalDocs": {
    "description": "Craft API documentation",
    "url": "https://docs.craftflow.com/api/introduction"
  },
  "servers": [
    {
      "url": "https://data.craftflow.co/api/v1",
      "description": "Production. The API is versioned in the URL path; existing versions keep working when a new one ships."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Users",
      "description": "People in your company who use Craft."
    },
    {
      "name": "Teams",
      "description": "Groups that users and records are organized into."
    },
    {
      "name": "Field Recordings",
      "description": "In-home appointment recordings captured by reps, with AI scoring and transcripts."
    },
    {
      "name": "Calls",
      "description": "Call-center calls ingested from a phone-system integration or handled by the AI voice agent, with AI scoring, summaries, and transcripts."
    }
  ],
  "paths": {
    "/users/": {
      "get": {
        "operationId": "listUsers",
        "summary": "List users",
        "description": "Returns a paginated list of the people in your company, each with their name, email, active status, and Craft permission codes. Call this to resolve a person's user ID before filtering other resources by them, to sync your roster into another system, or to check who is still active. System accounts are excluded: AI voice-agent users, the `Unassigned` placeholder used for unattributed calls, and any account on the `craftflow.co` or `craftflow.com` domain.",
        "tags": [
          "Users"
        ],
        "parameters": [
          {
            "name": "is_active",
            "in": "query",
            "required": false,
            "description": "Return only users whose active status matches this value. Omit to return both active and deactivated users.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "ordering",
            "in": "query",
            "required": false,
            "description": "Field to sort by. Prefix with `-` for descending order. Only `created_at` is sortable.",
            "schema": {
              "type": "string",
              "enum": [
                "created_at",
                "-created_at"
              ],
              "default": "-created_at"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-indexed page number to return. Pages beyond the last page return `404`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "page_size",
            "in": "query",
            "required": false,
            "description": "Number of results per page. Maximum 100; larger values are clamped to 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of users.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserListResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/users/{id}/": {
      "get": {
        "operationId": "getUser",
        "summary": "Retrieve a user",
        "description": "Returns a single user by ID, with the same fields as the list endpoint. Call this when you already hold a user ID — for example the `user` object embedded in a call or field recording — and need that person's email or permission codes. Not every embedded user is retrievable here: this endpoint applies the same exclusions as `listUsers`, so AI voice-agent users, the `Unassigned` placeholder and Craft-domain accounts return `404`.",
        "tags": [
          "Users"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The user's unique identifier, as returned in the `id` field of any user object.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The requested user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/teams/": {
      "get": {
        "operationId": "listTeams",
        "summary": "List teams",
        "description": "Returns a paginated list of the teams in your company, each with its name and description. Call this to map team IDs to names when grouping users or reporting by team.",
        "tags": [
          "Teams"
        ],
        "parameters": [
          {
            "name": "ordering",
            "in": "query",
            "required": false,
            "description": "Field to sort by. Prefix with `-` for descending order. Only `created_at` is sortable.",
            "schema": {
              "type": "string",
              "enum": [
                "created_at",
                "-created_at"
              ],
              "default": "-created_at"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-indexed page number to return. Pages beyond the last page return `404`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "page_size",
            "in": "query",
            "required": false,
            "description": "Number of results per page. Maximum 100; larger values are clamped to 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of teams.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TeamListResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/teams/{id}/": {
      "get": {
        "operationId": "getTeam",
        "summary": "Retrieve a team",
        "description": "Returns a single team by ID, with its name, description, and creation time. Call this to resolve one team without paging the full list.",
        "tags": [
          "Teams"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The team's unique identifier, as returned in the `id` field of any team object.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The requested team.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TeamResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/recordings/": {
      "get": {
        "operationId": "listRecordings",
        "summary": "List field recordings",
        "description": "Returns a paginated list of in-home field recordings for your company — each with its name, start and end time, audio duration, AI score, and the rep who recorded it. Call this to find recordings for a date range or a rep before fetching a transcript. Transcripts and custom fields are omitted here; use the retrieve and transcript endpoints for those.",
        "tags": [
          "Field Recordings"
        ],
        "parameters": [
          {
            "name": "ordering",
            "in": "query",
            "required": false,
            "description": "Field to sort by. Prefix with `-` for descending order. Only `created_at` is sortable.",
            "schema": {
              "type": "string",
              "enum": [
                "created_at",
                "-created_at"
              ],
              "default": "-created_at"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-indexed page number to return. Pages beyond the last page return `404`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "page_size",
            "in": "query",
            "required": false,
            "description": "Number of results per page. Maximum 100; larger values are clamped to 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of field recordings, without custom fields or transcripts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordingListResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/recordings/{id}/": {
      "get": {
        "operationId": "getRecording",
        "summary": "Retrieve a field recording",
        "description": "Returns a single field recording by ID, including the `custom_fields` array that the list endpoint omits — the per-company fields Craft extracts from the conversation or that a person filled in. Call this when you need the structured outcome of one appointment. The transcript is not included; use `getRecordingTranscript` for that.",
        "tags": [
          "Field Recordings"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The field recording's unique identifier, as returned in the `id` field of any recording object.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The requested field recording, with its custom field values.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordingDetailResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/recordings/{id}/transcript/": {
      "get": {
        "operationId": "getRecordingTranscript",
        "summary": "Retrieve a field recording transcript",
        "description": "Returns the diarized transcript of one field recording: an ordered list of utterances with speaker label, text, and millisecond timings, each broken down into word-level timings. Call this when you need the actual conversation — to summarize an appointment, search for what was said, or feed it to your own model. Transcripts can be large, so fetch them one recording at a time rather than in bulk.",
        "tags": [
          "Field Recordings"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The field recording's unique identifier, as returned in the `id` field of any recording object.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The field recording's transcript.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordingTranscriptResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/calls/": {
      "get": {
        "operationId": "listCalls",
        "summary": "List calls",
        "description": "Returns a paginated list of call-center calls for your company — each with its direction, source, AI sentiment, AI score, summary, booking and qualification flags, and the user the call is attributed to. Call this to pull call outcomes into a dashboard or warehouse, or to find the calls whose transcripts you want. Attribution is not a reliable test for whether a person handled the call — see `user` on the Call schema. Transcripts are omitted here; use `getCallTranscript`.",
        "tags": [
          "Calls"
        ],
        "parameters": [
          {
            "name": "source",
            "in": "query",
            "required": false,
            "description": "Return only calls from this source. `integration` means the call came from a connected phone system; `voice_agent` means Craft's AI voice agent handled it.",
            "schema": {
              "type": "string",
              "enum": [
                "integration",
                "voice_agent"
              ]
            }
          },
          {
            "name": "ordering",
            "in": "query",
            "required": false,
            "description": "Field to sort by. Prefix with `-` for descending order. Only `created_at` is sortable.",
            "schema": {
              "type": "string",
              "enum": [
                "created_at",
                "-created_at"
              ],
              "default": "-created_at"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-indexed page number to return. Pages beyond the last page return `404`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "page_size",
            "in": "query",
            "required": false,
            "description": "Number of results per page. Maximum 100; larger values are clamped to 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of calls, without transcripts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CallListResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/calls/{id}/": {
      "get": {
        "operationId": "getCall",
        "summary": "Retrieve a call",
        "description": "Returns a single call by ID with the same fields as the list endpoint — summary, sentiment, score, booking and cancellation flags, and the user the call is attributed to (see `user` on the Call schema for how AI and unattributed calls are represented). Call this to inspect one call you already have an ID for. The transcript is not included; use `getCallTranscript`.",
        "tags": [
          "Calls"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The call's unique identifier, as returned in the `id` field of any call object.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The requested call.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CallResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/calls/{id}/transcript/": {
      "get": {
        "operationId": "getCallTranscript",
        "summary": "Retrieve a call transcript",
        "description": "Returns the transcript of one call: an ordered list of utterances with speaker label, text, and millisecond start time. Call this when the call summary is not enough and you need what was actually said — to answer a question about a call, extract commitments, or run your own analysis. Transcripts can be large, so fetch them one call at a time rather than in bulk.",
        "tags": [
          "Calls"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The call's unique identifier, as returned in the `id` field of any call object.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call's transcript.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CallTranscriptResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "A Craft API key, sent as `Authorization: Bearer cf_live_...`. Keys are created by a Full Admin or Super Admin under Company Settings → API Keys in the Craft web app, are scoped to a single company, and can be revoked or given an expiry at any time. A missing, unknown, revoked, or expired key returns `401`."
      }
    },
    "responses": {
      "BadRequest": {
        "description": "A query parameter was rejected — for example a `source` value outside the documented enum.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ValidationError"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "The API key is missing, unknown, revoked, or expired.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "The API key is valid but is not allowed to perform this request.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "No such record for the company that owns this API key, or the requested page is past the last page. Records belonging to other companies are reported as not found.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "The API key exceeded its rate limit of 1,000 requests per hour. Wait and retry.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "The envelope returned for every failed request.",
        "properties": {
          "error": {
            "type": "object",
            "description": "Details of the failure.",
            "properties": {
              "detail": {
                "type": "string",
                "description": "Human-readable explanation of what went wrong, for example `Invalid API key.` or `API key has expired.`"
              }
            },
            "required": [
              "detail"
            ]
          }
        },
        "required": [
          "error"
        ]
      },
      "ValidationError": {
        "type": "object",
        "description": "The envelope returned when a query parameter fails validation. The object under `error` is keyed by the rejected parameter name instead of carrying a `detail` string, so handle it separately from `Error`.",
        "properties": {
          "error": {
            "type": "object",
            "description": "One entry per rejected query parameter, keyed by the parameter name.",
            "additionalProperties": {
              "type": "array",
              "description": "Validation messages for that parameter.",
              "items": {
                "type": "string",
                "description": "A human-readable validation message, for example `Select a valid choice. Integration is not one of the available choices.`"
              }
            }
          }
        },
        "required": [
          "error"
        ]
      },
      "Pagination": {
        "type": "object",
        "description": "Page metadata returned alongside `data` by every list endpoint.",
        "properties": {
          "count": {
            "type": "integer",
            "description": "Total number of records matching the request, across all pages.",
            "readOnly": true
          },
          "page": {
            "type": "integer",
            "description": "1-indexed number of the page in this response.",
            "readOnly": true
          },
          "page_size": {
            "type": "integer",
            "description": "Number of records requested per page for this response.",
            "readOnly": true
          },
          "total_pages": {
            "type": "integer",
            "description": "Total number of pages available at this page size.",
            "readOnly": true
          }
        },
        "required": [
          "count",
          "page",
          "page_size",
          "total_pages"
        ]
      },
      "NestedUser": {
        "type": "object",
        "description": "Minimal reference to a user, embedded in calls and field recordings. Use `getUser` with the `id` for the full record — but expect `404` for system accounts (AI voice-agent users, the `Unassigned` placeholder) and Craft-domain accounts, which `listUsers` and `getUser` exclude.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The user's unique identifier.",
            "readOnly": true
          },
          "full_name": {
            "type": "string",
            "description": "The user's full display name.",
            "readOnly": true
          }
        },
        "required": [
          "id",
          "full_name"
        ]
      },
      "User": {
        "type": "object",
        "description": "A person in your company who uses Craft. Read-only.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The user's unique identifier.",
            "readOnly": true
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "The user's email address. Unique across Craft.",
            "readOnly": true
          },
          "first_name": {
            "type": "string",
            "description": "The user's first name. Empty string if never set.",
            "readOnly": true
          },
          "last_name": {
            "type": "string",
            "description": "The user's last name. Empty string if never set.",
            "readOnly": true
          },
          "full_name": {
            "type": "string",
            "description": "The user's full display name. Empty string if never set.",
            "readOnly": true
          },
          "is_active": {
            "type": "boolean",
            "description": "Whether the user can currently sign in to Craft. Deactivated users are still returned, with `false`.",
            "readOnly": true
          },
          "permissions": {
            "type": "array",
            "description": "Craft permission codes granted to this user. Empty if the user has none. The `full_admin` and `super_admin` codes are never returned by this API.",
            "items": {
              "type": "string",
              "enum": [
                "ridealong_admin",
                "ridealong_coach",
                "ridealong_record",
                "call_center_ai_admin",
                "call_center_ai_coach",
                "call_center_ai_record",
                "inside_sales_admin",
                "inside_sales_rep",
                "rep_assignment_assigner"
              ]
            },
            "readOnly": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the user record was created, as an ISO 8601 timestamp.",
            "readOnly": true
          }
        },
        "required": [
          "id",
          "email",
          "first_name",
          "last_name",
          "full_name",
          "is_active",
          "permissions",
          "created_at"
        ]
      },
      "Team": {
        "type": "object",
        "description": "A group that users and records are organized into. Read-only.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The team's unique identifier.",
            "readOnly": true
          },
          "name": {
            "type": "string",
            "description": "The team's name, as shown in the Craft web app.",
            "readOnly": true
          },
          "description": {
            "type": "string",
            "description": "Free-text description of the team. Empty string if never set.",
            "readOnly": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the team was created, as an ISO 8601 timestamp.",
            "readOnly": true
          }
        },
        "required": [
          "id",
          "name",
          "description",
          "created_at"
        ]
      },
      "Recording": {
        "type": "object",
        "description": "An in-home appointment recorded by a rep and analyzed by Craft. Read-only.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The field recording's unique identifier.",
            "readOnly": true
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Label for the recording, usually the customer or job it belongs to. Null if none was set.",
            "readOnly": true
          },
          "start_time": {
            "type": "string",
            "format": "date-time",
            "description": "When the recording started, as an ISO 8601 timestamp.",
            "readOnly": true
          },
          "end_time": {
            "type": "string",
            "format": "date-time",
            "description": "When the recording ended, as an ISO 8601 timestamp.",
            "readOnly": true
          },
          "audio_duration_seconds": {
            "type": "integer",
            "description": "Length of the recorded audio in seconds. `0` when no audio was captured.",
            "readOnly": true
          },
          "score": {
            "type": "number",
            "description": "Craft's overall score for the appointment, from 0.0 to 1.0. `0.0` before scoring has run.",
            "readOnly": true
          },
          "user": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/NestedUser"
              },
              {
                "type": "null"
              }
            ],
            "description": "The rep who made the recording, or null if that user has since been removed.",
            "readOnly": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the recording record was created, as an ISO 8601 timestamp.",
            "readOnly": true
          }
        },
        "required": [
          "id",
          "name",
          "start_time",
          "end_time",
          "audio_duration_seconds",
          "score",
          "user",
          "created_at"
        ]
      },
      "RecordingCustomField": {
        "type": "object",
        "description": "One custom field value on a field recording. Custom fields are configured per company and are either extracted by Craft from the conversation or entered by a person.",
        "properties": {
          "name": {
            "type": "string",
            "description": "Human-readable name of the custom field.",
            "readOnly": true
          },
          "slug": {
            "type": "string",
            "description": "Stable machine-readable identifier for the custom field, unique within your company. Match on this rather than on `name`.",
            "readOnly": true
          },
          "value_type": {
            "type": "string",
            "enum": [
              "text",
              "number",
              "boolean",
              "single_select",
              "multi_select"
            ],
            "description": "The field's configured data type, which determines the JSON type of `value`.",
            "readOnly": true
          },
          "value": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "number"
              },
              {
                "type": "boolean"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              {
                "type": "null"
              }
            ],
            "description": "The stored value. Its JSON type follows `value_type`: `text` and `single_select` return a string, `number` a number, `boolean` a boolean, `multi_select` an array of strings. Null when the field has no value.",
            "readOnly": true
          },
          "source": {
            "type": "string",
            "enum": [
              "ai",
              "manual"
            ],
            "description": "Whether Craft extracted the value from the conversation (`ai`) or a person entered it (`manual`).",
            "readOnly": true
          }
        },
        "required": [
          "name",
          "slug",
          "value_type",
          "value",
          "source"
        ]
      },
      "RecordingDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Recording"
          },
          {
            "type": "object",
            "description": "The extra field returned only when retrieving a single recording.",
            "properties": {
              "custom_fields": {
                "type": "array",
                "description": "Custom field values attached to this recording. Empty array if none. Returned only by `getRecording`, not by `listRecordings`.",
                "items": {
                  "$ref": "#/components/schemas/RecordingCustomField"
                },
                "readOnly": true
              }
            },
            "required": [
              "custom_fields"
            ]
          }
        ],
        "description": "A field recording plus its custom field values."
      },
      "RecordingWord": {
        "type": "object",
        "description": "A single word within a field recording utterance, with its own timing.",
        "properties": {
          "text": {
            "type": "string",
            "description": "The word as transcribed.",
            "readOnly": true
          },
          "start": {
            "type": "integer",
            "description": "Milliseconds from the start of the recording to the start of this word.",
            "readOnly": true
          },
          "end": {
            "type": "integer",
            "description": "Milliseconds from the start of the recording to the end of this word.",
            "readOnly": true
          },
          "speaker": {
            "type": "string",
            "description": "Label of the speaker who said this word, matching the parent utterance's `speaker`.",
            "readOnly": true
          }
        },
        "required": [
          "text",
          "start",
          "end",
          "speaker"
        ]
      },
      "RecordingUtterance": {
        "type": "object",
        "description": "One continuous turn of speech in a field recording.",
        "properties": {
          "speaker": {
            "type": "string",
            "description": "Label of the speaker. Once Craft has identified speakers this is a name, otherwise a diarization label such as `A` or `B`.",
            "readOnly": true
          },
          "text": {
            "type": "string",
            "description": "What was said, as transcribed. Personally identifying information is redacted.",
            "readOnly": true
          },
          "start": {
            "type": "integer",
            "description": "Milliseconds from the start of the recording to the start of this utterance.",
            "readOnly": true
          },
          "end": {
            "type": "integer",
            "description": "Milliseconds from the start of the recording to the end of this utterance.",
            "readOnly": true
          },
          "words": {
            "type": "array",
            "description": "Word-level breakdown of this utterance, in order.",
            "items": {
              "$ref": "#/components/schemas/RecordingWord"
            },
            "readOnly": true
          }
        },
        "required": [
          "speaker",
          "text",
          "start",
          "end",
          "words"
        ]
      },
      "RecordingTranscript": {
        "type": "object",
        "description": "A field recording's transcript, returned by `getRecordingTranscript`.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The field recording's unique identifier, echoing the ID in the request path.",
            "readOnly": true
          },
          "transcript": {
            "description": "Utterances in the order they were spoken. Recordings that have not been transcribed return either an empty array or an empty object — accept both.",
            "oneOf": [
              {
                "type": "array",
                "description": "Transcribed utterances, oldest first. Empty until transcription finishes.",
                "items": {
                  "$ref": "#/components/schemas/RecordingUtterance"
                }
              },
              {
                "type": "object",
                "description": "An empty object, returned for recordings stored before transcription ran.",
                "additionalProperties": false
              }
            ],
            "readOnly": true
          }
        },
        "required": [
          "id",
          "transcript"
        ]
      },
      "Call": {
        "type": "object",
        "description": "A call-center call ingested from a phone-system integration or handled by Craft's AI voice agent, analyzed by Craft. Read-only.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The call's unique identifier.",
            "readOnly": true
          },
          "start_time": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the call started, as an ISO 8601 timestamp. Null if the source system did not report it.",
            "readOnly": true
          },
          "end_time": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the call ended, as an ISO 8601 timestamp. Null if the source system did not report it.",
            "readOnly": true
          },
          "audio_duration_seconds": {
            "type": "integer",
            "description": "Length of the call audio in seconds. `0` when no audio was captured.",
            "readOnly": true
          },
          "direction": {
            "type": "string",
            "enum": [
              "inbound",
              "outbound"
            ],
            "description": "Whether the customer called in or the company called out.",
            "readOnly": true
          },
          "source": {
            "type": "string",
            "enum": [
              "integration",
              "voice_agent"
            ],
            "description": "How the call record reached Craft: `integration` from a connected phone system, `voice_agent` from Craft's AI voice agent.",
            "readOnly": true
          },
          "sentiment": {
            "type": "string",
            "enum": [
              "positive",
              "neutral",
              "negative"
            ],
            "description": "Sentiment Craft detected on the call. `neutral` before analysis has run.",
            "readOnly": true
          },
          "score": {
            "type": "number",
            "description": "Craft's overall score for the call, from 0.0 to 1.0. `0.0` before scoring has run.",
            "readOnly": true
          },
          "summary": {
            "type": "string",
            "description": "Craft's plain-language summary of the call. Empty string before analysis has run.",
            "readOnly": true
          },
          "is_qualified": {
            "type": "boolean",
            "description": "Whether the caller was a qualified lead.",
            "readOnly": true
          },
          "is_booked": {
            "type": "boolean",
            "description": "Whether an appointment was booked on this call.",
            "readOnly": true
          },
          "is_excused": {
            "type": "boolean",
            "description": "Whether this call is excused from booking-rate reporting — for example because the customer booked on a later call.",
            "readOnly": true
          },
          "excused_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the call was excused, as an ISO 8601 timestamp. Null if it was never excused.",
            "readOnly": true
          },
          "excuse_reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Human-readable reason the call was excused. Null if it was never excused.",
            "readOnly": true
          },
          "is_cancellation": {
            "type": "boolean",
            "description": "Whether this call was the customer cancelling an appointment.",
            "readOnly": true
          },
          "has_cancellation": {
            "type": "boolean",
            "description": "Whether the appointment booked on this call was later cancelled.",
            "readOnly": true
          },
          "user": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/NestedUser"
              },
              {
                "type": "null"
              }
            ],
            "description": "The user the call is attributed to. For `source: voice_agent` calls this is Craft's AI agent account for the company, whose `full_name` ends in `(AI)`. Calls that a phone-system integration could not attribute to a person carry the company's placeholder account, named `Unassigned`. Both are excluded from `listUsers` and `getUser`. Null only when the attributed user has since been removed.",
            "readOnly": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the call record was created in Craft, as an ISO 8601 timestamp.",
            "readOnly": true
          }
        },
        "required": [
          "id",
          "start_time",
          "end_time",
          "audio_duration_seconds",
          "direction",
          "source",
          "sentiment",
          "score",
          "summary",
          "is_qualified",
          "is_booked",
          "is_excused",
          "excused_at",
          "excuse_reason",
          "is_cancellation",
          "has_cancellation",
          "user",
          "created_at"
        ]
      },
      "CallUtterance": {
        "type": "object",
        "description": "One continuous turn of speech in a call.",
        "properties": {
          "speaker": {
            "type": "string",
            "description": "Label of the speaker, such as `CSR` or `Customer`, or a diarization label such as `A` or `B`.",
            "readOnly": true
          },
          "text": {
            "type": "string",
            "description": "What was said, as transcribed.",
            "readOnly": true
          },
          "start": {
            "type": "integer",
            "description": "Milliseconds from the start of the call to the start of this utterance.",
            "readOnly": true
          },
          "end": {
            "type": "integer",
            "description": "Milliseconds from the start of the call to the end of this utterance. Absent when the source transcript did not report an end time.",
            "readOnly": true
          }
        },
        "required": [
          "speaker",
          "text",
          "start"
        ]
      },
      "CallTranscript": {
        "type": "object",
        "description": "A call's transcript, returned by `getCallTranscript`.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The call's unique identifier, echoing the ID in the request path.",
            "readOnly": true
          },
          "transcript": {
            "type": "array",
            "description": "Utterances in the order they were spoken. Empty until transcription finishes.",
            "items": {
              "$ref": "#/components/schemas/CallUtterance"
            },
            "readOnly": true
          }
        },
        "required": [
          "id",
          "transcript"
        ]
      },
      "UserListResponse": {
        "type": "object",
        "description": "A page of users.",
        "properties": {
          "data": {
            "type": "array",
            "description": "The users on this page.",
            "items": {
              "$ref": "#/components/schemas/User"
            },
            "readOnly": true
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination",
            "description": "Page metadata for this response."
          }
        },
        "required": [
          "data",
          "pagination"
        ]
      },
      "UserResponse": {
        "type": "object",
        "description": "A single user.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/User",
            "description": "The requested user."
          }
        },
        "required": [
          "data"
        ]
      },
      "TeamListResponse": {
        "type": "object",
        "description": "A page of teams.",
        "properties": {
          "data": {
            "type": "array",
            "description": "The teams on this page.",
            "items": {
              "$ref": "#/components/schemas/Team"
            },
            "readOnly": true
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination",
            "description": "Page metadata for this response."
          }
        },
        "required": [
          "data",
          "pagination"
        ]
      },
      "TeamResponse": {
        "type": "object",
        "description": "A single team.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Team",
            "description": "The requested team."
          }
        },
        "required": [
          "data"
        ]
      },
      "RecordingListResponse": {
        "type": "object",
        "description": "A page of field recordings.",
        "properties": {
          "data": {
            "type": "array",
            "description": "The field recordings on this page, without custom fields or transcripts.",
            "items": {
              "$ref": "#/components/schemas/Recording"
            },
            "readOnly": true
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination",
            "description": "Page metadata for this response."
          }
        },
        "required": [
          "data",
          "pagination"
        ]
      },
      "RecordingDetailResponse": {
        "type": "object",
        "description": "A single field recording with its custom field values.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/RecordingDetail",
            "description": "The requested field recording."
          }
        },
        "required": [
          "data"
        ]
      },
      "RecordingTranscriptResponse": {
        "type": "object",
        "description": "A single field recording's transcript.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/RecordingTranscript",
            "description": "The requested transcript."
          }
        },
        "required": [
          "data"
        ]
      },
      "CallListResponse": {
        "type": "object",
        "description": "A page of calls.",
        "properties": {
          "data": {
            "type": "array",
            "description": "The calls on this page, without transcripts.",
            "items": {
              "$ref": "#/components/schemas/Call"
            },
            "readOnly": true
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination",
            "description": "Page metadata for this response."
          }
        },
        "required": [
          "data",
          "pagination"
        ]
      },
      "CallResponse": {
        "type": "object",
        "description": "A single call.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Call",
            "description": "The requested call."
          }
        },
        "required": [
          "data"
        ]
      },
      "CallTranscriptResponse": {
        "type": "object",
        "description": "A single call's transcript.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/CallTranscript",
            "description": "The requested transcript."
          }
        },
        "required": [
          "data"
        ]
      }
    }
  }
}
