{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/audivo-dev/json-schema/audivo-dev-job-group-response-schema.json",
  "title": "JobGroupResponse",
  "description": "One group's rollup. `member_count` is how many members the confirm set out to create; `members` is how many exist, which is fewer only while the group is `pending` (or after it was `abandoned` part way). `credits_reserved` is what the group still holds against the balance — the reservations of members not yet terminal; `credits_settled` is what it has been charged, job members and cache reads together; `credits_released` is what came back from members that failed, were cancelled, or settled below their ceiling. None of the three is a bill: each job member carries its own figures, and the ledger is the record.",
  "x-generated": "2026-10-30",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/audivo-dev-openapi.yaml#/components/schemas/JobGroupResponse",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "group_id",
    "status",
    "quote_id",
    "member_count",
    "members",
    "member_counts",
    "credits_reserved",
    "credits_settled",
    "credits_released",
    "created_at",
    "completion_deadline",
    "completed_at",
    "abandoned_at"
  ],
  "properties": {
    "group_id": {
      "$ref": "#/$defs/GroupId"
    },
    "status": {
      "$ref": "#/$defs/JobGroupStatus"
    },
    "quote_id": {
      "description": "The quote this group confirmed, or `null` for a single-episode submission.",
      "anyOf": [
        {
          "$ref": "#/$defs/QuoteId"
        },
        {
          "enum": [
            null
          ]
        }
      ]
    },
    "member_count": {
      "type": "integer",
      "minimum": 1
    },
    "members": {
      "type": "array",
      "maxItems": 100,
      "items": {
        "$ref": "#/$defs/JobGroupMember"
      }
    },
    "member_counts": {
      "$ref": "#/$defs/JobGroupMemberCounts"
    },
    "credits_reserved": {
      "$ref": "#/$defs/CreditAmount"
    },
    "credits_settled": {
      "$ref": "#/$defs/CreditAmount"
    },
    "credits_released": {
      "$ref": "#/$defs/CreditAmount"
    },
    "created_at": {
      "type": "string",
      "format": "date-time"
    },
    "completion_deadline": {
      "type": "string",
      "format": "date-time",
      "description": "Past this, a still-`pending` group is swept."
    },
    "completed_at": {
      "$ref": "#/$defs/NullableDateTime"
    },
    "abandoned_at": {
      "$ref": "#/$defs/NullableDateTime"
    }
  },
  "$defs": {
    "CreditAmount": {
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991,
      "description": "A non-negative JavaScript-safe integer credit amount. The upper bound matches `Number.MAX_SAFE_INTEGER`, which is enforced by the domain ledger so arithmetic and JSON round-trips cannot silently lose cents worth of credit precision."
    },
    "EpisodeId": {
      "type": "string",
      "pattern": "^ep_[a-z2-7]{16}$",
      "description": "Canonical episode identifier."
    },
    "GroupId": {
      "type": "string",
      "pattern": "^grp_[A-Za-z0-9]{16,32}$",
      "maxLength": 40,
      "description": "Opaque, server-generated job group identifier."
    },
    "JobEpisodeTitle": {
      "description": "The episode's title as it was when this job or read was created, or `null` when it was created without one. For an upload it is the title given when the file was announced. Where a feed gave no title, a catalog read (`GET /v1/episodes/{episode_id}/transcript`) answers the identifier the catalog stores in its place.",
      "anyOf": [
        {
          "type": "string",
          "minLength": 1,
          "maxLength": 300
        },
        {
          "type": "null"
        }
      ]
    },
    "JobGroupCachedReadMember": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "kind",
        "read_id",
        "episode_id",
        "show_title",
        "episode_title",
        "credits_charged",
        "created_at"
      ],
      "description": "A member the confirm found already transcribed: charged `credits_charged` (the cached-read price for this account) at confirm, with no job behind it. `read_id` is the job-shaped id the read was charged under — the id its ledger event and access record carry — so every credit a group spent is explainable from the group alone, and it is the handle its transcript is fetched by: `GET /v1/reads/{read_id}` delivers it, in any format, and charges nothing further. The job poll cannot, because this member has no job.",
      "properties": {
        "kind": {
          "const": "cached_read"
        },
        "read_id": {
          "$ref": "#/$defs/JobId"
        },
        "episode_id": {
          "$ref": "#/$defs/EpisodeId"
        },
        "show_title": {
          "$ref": "#/$defs/JobShowTitle"
        },
        "episode_title": {
          "$ref": "#/$defs/JobEpisodeTitle"
        },
        "credits_charged": {
          "$ref": "#/$defs/CreditAmount"
        },
        "created_at": {
          "type": "string",
          "format": "date-time"
        }
      }
    },
    "JobGroupJobMember": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "kind",
        "job_id",
        "episode_id",
        "show_title",
        "episode_title",
        "status",
        "estimated_credits",
        "reserved_credits",
        "created_at"
      ],
      "description": "A member with a job behind it. `reserved_credits` is the ceiling the confirm reserved; `settled_credits` and `released_credits` appear as the job's terminal state sets them, as on `JobStatus`. Poll or cancel the job itself through `/v1/transcripts/{job_id}`.",
      "properties": {
        "kind": {
          "const": "job"
        },
        "job_id": {
          "$ref": "#/$defs/JobId"
        },
        "episode_id": {
          "$ref": "#/$defs/EpisodeId"
        },
        "show_title": {
          "$ref": "#/$defs/JobShowTitle"
        },
        "episode_title": {
          "$ref": "#/$defs/JobEpisodeTitle"
        },
        "status": {
          "$ref": "#/$defs/JobState"
        },
        "estimated_credits": {
          "$ref": "#/$defs/CreditAmount"
        },
        "reserved_credits": {
          "$ref": "#/$defs/CreditAmount"
        },
        "settled_credits": {
          "$ref": "#/$defs/CreditAmount"
        },
        "released_credits": {
          "$ref": "#/$defs/CreditAmount"
        },
        "created_at": {
          "type": "string",
          "format": "date-time"
        }
      }
    },
    "JobGroupMember": {
      "description": "One member of a group — a job, or a settled cache read. `kind` discriminates.",
      "oneOf": [
        {
          "$ref": "#/$defs/JobGroupJobMember"
        },
        {
          "$ref": "#/$defs/JobGroupCachedReadMember"
        }
      ]
    },
    "JobGroupMemberCounts": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "validating",
        "queued",
        "downloading",
        "transcribing",
        "merging",
        "completed",
        "failed",
        "cancelled",
        "cached_read"
      ],
      "description": "How many members are in each job state, plus how many were settled cache reads (which have no state — they were delivered at confirm). The nine sum to `member_count` on a complete group.",
      "properties": {
        "validating": {
          "type": "integer",
          "minimum": 0
        },
        "queued": {
          "type": "integer",
          "minimum": 0
        },
        "downloading": {
          "type": "integer",
          "minimum": 0
        },
        "transcribing": {
          "type": "integer",
          "minimum": 0
        },
        "merging": {
          "type": "integer",
          "minimum": 0
        },
        "completed": {
          "type": "integer",
          "minimum": 0
        },
        "failed": {
          "type": "integer",
          "minimum": 0
        },
        "cancelled": {
          "type": "integer",
          "minimum": 0
        },
        "cached_read": {
          "type": "integer",
          "minimum": 0
        }
      }
    },
    "JobGroupStatus": {
      "type": "string",
      "enum": [
        "pending",
        "complete",
        "abandoned"
      ],
      "description": "`pending` while a confirm is still fanning out (or died doing so); `complete` once every member landed; `abandoned` once the sweeper returned a pending group's reservations past its deadline. A cancelled group is `complete` with `cancelled` members."
    },
    "JobId": {
      "type": "string",
      "pattern": "^job_[A-Za-z0-9]{16,32}$",
      "maxLength": 40,
      "description": "Opaque, server-generated job identifier."
    },
    "JobShowTitle": {
      "description": "The show's title as it was when this job or read was created (ADR-0036), or `null` when it was created without one: before 0.14.0, or from a feed that names no show. For an upload it is `Uploads`. Copied once, never updated.",
      "anyOf": [
        {
          "type": "string",
          "minLength": 1,
          "maxLength": 300
        },
        {
          "type": "null"
        }
      ]
    },
    "JobState": {
      "type": "string",
      "enum": [
        "validating",
        "queued",
        "downloading",
        "transcribing",
        "merging",
        "completed",
        "failed",
        "cancelled"
      ],
      "description": "Legal transitions: validating → queued|failed|cancelled; queued → downloading|failed|cancelled; downloading → transcribing|failed; transcribing → merging|failed; merging → completed|failed. Terminal states (completed, failed, cancelled) have no further transitions. Cancellation is legal only from validating/queued; every non-terminal state may fail."
    },
    "NullableDateTime": {
      "description": "An ISO 8601 timestamp, or `null`.",
      "anyOf": [
        {
          "type": "string",
          "format": "date-time"
        },
        {
          "enum": [
            null
          ]
        }
      ]
    },
    "QuoteId": {
      "type": "string",
      "pattern": "^qte_[A-Za-z0-9]{16,32}$",
      "maxLength": 40,
      "description": "Opaque, server-generated quote identifier."
    }
  }
}