generated: '2026-09-11' method: derived source: openapi/_original/lacuna-music-openapi-original.json note: >- A deliberately small object graph — one aggregate (GenerationTask) owning a collection of Tracks, plus the request shape and four event payloads that all extend one webhook envelope. New this round: an Account entity, added to the published OpenAPI between 2026-08-09 and 2026-09-11, which closes part of the modelling gap recorded below — credits, plan, key identity and rate limits are now readable objects rather than dashboard-only concepts. entities: - name: Account role: singleton (the caller's own account, no id in the path) id_field: id read_by: getMe fields: - {name: id, type: string, required: true} - {name: plan, type: string, enum: [free, basic, pro, ultra], required: true, note: lowercased} - {name: credits, type: object, required: true, fields: [subscription, onetime, total], note: 'subscription credits are spent before one-time credits'} - {name: rate_limits, type: object, required: true, fields: [requests_per_minute, concurrent_generations]} - {name: auth, type: object, required: true, fields: [kind, scopes, key]} - {name: auth.kind, type: string, enum: [api_key, oauth], required: true} - {name: auth.scopes, type: 'array', required: true} - {name: auth.key, type: 'object|null', required: true, fields: [id, name, expires_at], note: 'present only when auth.kind is api_key'} cost: free — the spec states no credits are consumed note: >- A reflexive resource: it describes the credential presenting it rather than a stored record the caller names. There is no /v1/accounts/{id} — you can only read yourself. - name: GenerationTask role: aggregate root id_field: id id_example: cm123abc... fields: - {name: id, type: string, required: true} - {name: status, type: string, enum: [pending, ready, failed], required: true} - {name: model, type: 'string|null', enum: [aether, echo, nocturne, null], required: true} - {name: created_at, type: date-time, required: true} - {name: updated_at, type: date-time, required: true} - {name: credits_used, type: integer, required: true} - {name: credits_refunded, type: integer, required: true} - {name: error, type: 'object|null', required: true, fields: [code, message]} - {name: tracks, type: 'array', required: true} created_by: createGeneration read_by: getGeneration lifecycle: pending -> ready | failed - name: Track role: child id_field: id fields: - {name: id, type: string, required: true} - {name: audio_url, type: 'string (uri)', required: true} - {name: duration, type: 'number|null', required: true, unit: seconds} - {name: title, type: 'string|null', required: true} - {name: lyrics, type: 'string|null', required: true} - {name: image_url, type: 'string|null (uri)', required: true} - {name: tags, type: 'string|null', required: true} - {name: index, type: integer, required: true, description: Track index within this task} note: >- audio_url points at Lacuna's CDN and, per the provider's Agent Skill, does not carry a 24-hour expiry. - name: GenerateRequest role: request shape required: [style, title] conditional_rules: - 'if instrumental is false then lyrics is required and must contain a non-whitespace character' - 'if model is echo or nocturne then vocal_gender, negative_tags, style_weight, weirdness_constraint and audio_weight are forbidden' additional_properties: false - name: WebhookEnvelope role: event base id_field: id id_prefix: evt_ fields: [id, type, created, data] - name: JobCompletedEvent role: event extends: WebhookEnvelope - name: JobFailedEvent role: event extends: WebhookEnvelope - name: CreditsLowEvent role: event extends: WebhookEnvelope - name: KeyExpiringEvent role: event extends: WebhookEnvelope - name: ErrorResponse role: error envelope fields: [error.type, error.code, error.message, error.param, error.model, error.retry_after_seconds] relationships: - from: Account to: GenerationTask kind: has_many via: implicit (ownership — tasks are scoped to the calling credential's account) source: 'NotFound response: "not found or not accessible by this API key"' note: >- An ownership edge that is enforced but not navigable — there is no list operation, so an account cannot enumerate the generations it owns. - from: GenerationTask to: Track kind: has_many via: tracks source: '$ref #/components/schemas/Track' - from: JobCompletedEvent to: GenerationTask kind: belongs_to via: data.task_id - from: JobCompletedEvent to: Track kind: has_many via: data.tracks - from: JobFailedEvent to: GenerationTask kind: belongs_to via: data.task_id - from: JobCompletedEvent to: WebhookEnvelope kind: extends via: allOf - from: JobFailedEvent to: WebhookEnvelope kind: extends via: allOf - from: CreditsLowEvent to: WebhookEnvelope kind: extends via: allOf - from: KeyExpiringEvent to: WebhookEnvelope kind: extends via: allOf id_prefixes: - {prefix: 'evt_', entity: WebhookEnvelope, source: spec example} implicit_entities: note: >- Partially closed this round. Credits, the presenting API key and the subscription plan are now READABLE as nested objects on Account (credits.total, auth.key.expires_at, plan) — but only for the caller, and only for reading. None of them is a resource you can address, list or mutate: there is no /v1/keys to rotate a credential, no /v1/credits to top up, no /v1/models to enumerate the model catalog (the MCP server exposes list_models; REST does not), and no way to list your own past generations. now_readable: [Credit balance, ApiKey (self only), Subscription plan, effective rate limits] still_absent: [Model catalog, ApiKey management, Generation list, Subscription mutation] source: openapi/_original/lacuna-music-openapi-original.json#/components/schemas/Account