npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

lacuna-sdk

v0.3.1

Published

Official TypeScript SDK for the Lacuna Music API — generate AI music programmatically.

Readme

lacuna-sdk

npm version License: MIT Node.js

Official TypeScript SDK for the Lacuna Music API. Generate AI music programmatically, await results, and verify webhooks from any Node 18+ runtime.

  • Fully typed against the published OpenAPI spec
  • Works in Node, Bun, and Deno (via npm)
  • Webhook signature verification with constant-time comparison
  • Automatic retries with Retry-After honoring and exponential backoff
  • Polling helper that waits for terminal task state

Looking for the command-line tool? See lacuna-toolkit. For the MCP server, see lacuna-mcp.


Table of contents


Installation

npm install lacuna-sdk
# or
pnpm add lacuna-sdk
# or
yarn add lacuna-sdk

Quickstart

import Lacuna from 'lacuna-sdk'

const lacuna = new Lacuna({ apiKey: process.env.LACUNA_API_KEY })

const task = await lacuna.music.generations.create({
  style: 'lofi hip hop, mellow piano, 70 bpm',
  title: 'Late Night Study',
  instrumental: true,
})

const finished = await lacuna.music.generations.waitFor(task.id)
console.log(finished.tracks[0]?.audio_url)

Authentication

Generate an API key from your Lacuna profile dashboard. Keys begin with lyr_live_ and are shown once at creation — store them in a secrets manager.

The SDK reads the key from one of two sources, in order:

  1. The apiKey option passed to new Lacuna({ apiKey }).
  2. The LACUNA_API_KEY environment variable.
const lacuna = new Lacuna({ apiKey: process.env.LACUNA_API_KEY })

Music API access requires the Pro plan or above. Requests from lower tiers receive 403 permission_error / tier_insufficient.


SDK reference

Creating a generation

const task = await lacuna.music.generations.create({
  lyrics: '[Verse]\nSunlight on the water\n[Chorus]\nLet it shine',
  style: 'indie folk, female vocal, 90 bpm, warm',
  title: 'Sunlight',
  model: 'aether',
})

task.id      // 'cm123abc...'
task.status  // 'pending'

| Field | Type | Notes | | ---------------------- | -------------------------- | ---------------------------------------------------------------- | | style | string (required) | Up to 1000 chars. | | title | string (required) | Up to 200 chars. | | lyrics | string | Required unless instrumental: true. Up to 5000 chars. | | instrumental | boolean | Default false. | | model | 'aether' | Default aether (Lacuna Aether). | | vocal_gender | 'm' \| 'f' | Lead vocal hint. | | negative_tags | string | Style tags to avoid. | | style_weight | number (0–1) | | | weirdness_constraint | number (0–1) | | | audio_weight | number (0–1) | |

Credits are deducted on this call and refunded automatically if the upstream provider fails. The default cost is 50 credits per request — see pricing.

Retrieving a generation

const task = await lacuna.music.generations.retrieve('cm123abc')

if (task.status === 'ready') {
  for (const track of task.tracks) {
    console.log(track.audio_url, track.duration, track.lyrics)
  }
}

status is one of 'pending' | 'ready' | 'failed'. A failed task is a normal outcome — inspect task.error for the reason; credits are refunded automatically.

Waiting for completion

const task = await lacuna.music.generations.waitFor('cm123abc', {
  pollInterval: 5_000, // ms — default 5_000
  timeout: 600_000,    // ms — default 600_000 (10 minutes)
})

Throws PollingTimeoutError when the timeout elapses before the task finishes. Pass an AbortSignal to cancel early:

const controller = new AbortController()
setTimeout(() => controller.abort(), 30_000)

await lacuna.music.generations.waitFor(task.id, { signal: controller.signal })

For long-running production workflows prefer the job.completed webhook over polling.

Client options

const lacuna = new Lacuna({
  apiKey: process.env.LACUNA_API_KEY,
  baseURL: 'https://lacuna.fm/api/v1', // override for staging
  timeout: 60_000,                     // ms per request, default 60_000
  maxRetries: 2,                       // retries on 429/5xx (except `model_unavailable`), default 2
  defaultHeaders: { 'X-App-Name': 'my-pipeline' },
  fetch: customFetch,                  // custom fetch impl (proxy, instrumentation)
})

Retries use the server's Retry-After header when present, falling back to jittered exponential backoff (500ms × 2^attempt).


Webhooks

Lacuna emits four event types: job.completed, job.failed, credits.low, key.expiring. Each request carries an X-Lacuna-Signature header in the form t=<unix>,v1=<hex>, where the hex is HMAC-SHA256(secret, "${t}.${rawBody}").

constructEvent verifies the signature and parses the body in one call. Pass the raw body — once the body is parsed, the signature can no longer be verified.

Express

import express from 'express'
import { Webhooks } from 'lacuna-sdk/webhooks'

const app = express()

app.post(
  '/webhooks/lacuna',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    try {
      const event = Webhooks.constructEvent(
        req.body,                                  // Buffer (raw body)
        req.headers['x-lacuna-signature'],
        process.env.LACUNA_WEBHOOK_SECRET!,
      )

      if (Webhooks.isJobCompleted(event)) {
        console.log('Tracks ready:', event.data.tracks)
      } else if (Webhooks.isJobFailed(event)) {
        console.error('Generation failed:', event.data.error)
      }

      res.status(200).end()
    } catch (err) {
      console.error('Invalid webhook signature:', err)
      res.status(400).end()
    }
  },
)

Next.js (App Router)

// app/api/webhooks/lacuna/route.ts
import { Webhooks } from 'lacuna-sdk/webhooks'

export async function POST(request: Request) {
  const rawBody = await request.text()
  try {
    const event = Webhooks.constructEvent(
      rawBody,
      request.headers.get('x-lacuna-signature'),
      process.env.LACUNA_WEBHOOK_SECRET!,
    )

    if (event.type === 'job.completed') {
      // event.data is fully typed
    }

    return new Response(null, { status: 200 })
  } catch {
    return new Response(null, { status: 400 })
  }
}

Available helpers

import { Webhooks } from 'lacuna-sdk/webhooks'

Webhooks.constructEvent(payload, header, secret, options?) // verify + parse
Webhooks.verifySignature(payload, header, secret, options?) // boolean
Webhooks.isJobCompleted(event)
Webhooks.isJobFailed(event)
Webhooks.isCreditsLow(event)
Webhooks.isKeyExpiring(event)
Webhooks.SignatureError                                    // thrown by constructEvent
Webhooks.HEADER                                            // 'x-lacuna-signature'

The default replay window is 5 minutes; tune via { toleranceSeconds }.


Errors

Every error thrown by the SDK extends LacunaError. Catch the base class for a single net, or narrow on subclasses for granular handling.

import {
  Lacuna,
  APIError,
  AuthenticationError,
  InsufficientCreditsError,
  ModelUnavailableError,
  PermissionError,
  RateLimitError,
  PollingTimeoutError,
  APITimeoutError,
  APIConnectionError,
} from 'lacuna-sdk'

try {
  await lacuna.music.generations.create(params)
} catch (err) {
  if (err instanceof RateLimitError) {
    console.log(`Backing off for ${err.retryAfter}s`)
  } else if (err instanceof ModelUnavailableError) {
    // The requested model is circuit-broken. The SDK won't auto-fallback —
    // each model has a different credit cost. Pick another one yourself.
    console.log(
      `Model ${err.model} is down for ~${err.retryAfterSeconds}s; retrying with a different model`
    )
  } else if (err instanceof InsufficientCreditsError) {
    // top up or pause the worker
  } else if (err instanceof APIError) {
    console.error(`HTTP ${err.status} ${err.code}: ${err.message}`)
  }
}

| Class | When it's thrown | | --------------------------- | ----------------------------------------------------------------------------------------------- | | BadRequestError | 400 — invalid request body or parameters (err.param indicates which). | | AuthenticationError | 401 — missing, invalid, expired, or revoked API key. | | InsufficientCreditsError | 402 — credit balance too low. | | PermissionError | 403 — tier not high enough for the Music API. | | NotFoundError | 404 — task does not exist or is not owned by this key. | | RateLimitError | 429 — RPM or concurrent-request cap exceeded. Inspect err.retryAfter. | | ServiceUnavailableError | 503 — server is temporarily unavailable. Inspect err.retryAfter. | | ModelUnavailableError | 503 model_unavailable — the requested model is circuit-broken. Inspect err.model and err.retryAfterSeconds; the SDK does not auto-fallback (different models have different credit costs). | | InternalServerError | 5xx — server or upstream provider failure (other than 503). | | APIError | Base class for any non-2xx response. | | APIConnectionError | Network failure (DNS, TCP reset, fetch threw). | | APITimeoutError | Request exceeded timeout option. | | PollingTimeoutError | waitFor exceeded its timeout option. | | WebhookSignatureError | constructEvent rejected a request (exported from lacuna-sdk/webhooks). |

APIError exposes status, type, code, param, and lower-cased headers so you can build retry, logging, or alerting logic without parsing strings.


Requirements

  • Node.js 18 or newer (uses the global fetch and AbortController).
  • An API key from a Lacuna Pro plan or above.

License

MIT © Louis Tsang