# Generation from your own server

> Submit generation and analysis jobs from a backend or app you host yourself: a server client with a personal token, or generation on a signed-in person's own credits.

Canonical: https://maru.audio/docs/generation-from-your-server
Documentation index: https://maru.audio/llms.txt

> **Simpler on Maru.** A Maru module doesn't need a server or a token to
> generate. `face.appGeneration` asks the host, Maru shows the consent page,
> and each person's own credits pay. See
> [Add generation to an instrument](https://maru.audio/docs/generative-features.md#add-generation-to-an-instrument).
> It's free to start, and you never handle keys.

Use this page when your own backend or app submits jobs. There are two ways,
depending on whose account pays:

| Who pays | Credential | Section |
| --- | --- | --- |
| Your account | A personal token with `inference:run`, on your server only | [Connect with a token](#connect-with-a-token) |
| The signed-in person | Their creator-app session, after they approve generation | [Generate on the person's credits](#generate-on-the-persons-credits) |

The jobs, tasks and results are the same either way; the
[jobs reference](https://maru.audio/docs/generative-features.md#the-same-request-through-http-cli-and-mcp)
covers HTTP, CLI and MCP forms, audio inputs and account samples.

## Connect with a token

Use `maruaudio/api` from your app server with an existing personal token that
has `inference:run`. Keep the token in your secret manager. After `maru login`,
the CLI uses its saved session; the MCP connector uses its approved grant.

```ts
import { createMaruClient } from "maruaudio/api"

const maru = createMaruClient({
  auth: { apiKey: process.env.MARU_TOKEN! },
})

// Persist this key before submitting, so a lost response is recoverable.
const requestKey = crypto.randomUUID()
const handle = await maru.audio.jobs.submit({
  schema: "maru-audio-job/v1",
  requestKey,
  task: {
    type: "generate_audio",
    prompt: "a soft wooden percussion hit, dry recording",
    audioType: "one-shot",
    seconds: 2,
    seeds: [1042],
  },
  persistence: { mode: "artifact_only" },
})
const result = await maru.audio.jobs.wait(handle.id)
if (result.handle.status !== "done") {
  throw new Error(result.handle.error?.message ?? result.handle.status)
}
const take = result.artifacts.find(artifact => artifact.role === "audio_variant")
if (!take) throw new Error("No audio take was returned")
const { bytes } = await maru.audio.artifacts.download({
  jobId: handle.id,
  artifactId: take.artifactId,
})
// Hand these verified encoded bytes to your existing audio/resource loader.
```

A raw job creates private artifacts. It does not add a sample to Generated
Sounds. The client verifies the downloaded byte length and SHA-256 against the
artifact receipt. Browser-compatible client code returns bytes; only the Node
CLI writes local files.

Inspect `await maru.audio.capabilities()` before offering an action. Availability
requires an implemented executor and a live compatible worker. A catalog entry
alone does not mean a model is deployed. A server without a suitable worker
refuses the submission before creating a job or charging credits.

`jobs.wait` polls until `done`, `error` or `cancelled` and returns that terminal
state. Use its `onUpdate` option for interface updates. Cancel an explicit user
request with `jobs.cancel(id)`. Use `jobs.events(id, lastSequence)` to replay
persisted events after reconnecting; keep the returned sequence with the job.

If submission loses its response, call `jobs.lookup(requestKey)` or repeat the
same submission with the same key. Both recover the accepted job. Changing the
input under that key is refused. A new key means a new musical request.

## Generate on the person's credits

A creator app that runs outside Maru uses the generation SDK's hosted
connector with an app credential: an authenticated `fetch` and your client id.
The connector adds the app header to every request. With a creator app OAuth
session, `createCreatorAppGenerationConnector` builds that credential from the
session's request function:

```ts
import { createCreatorAppGenerationConnector } from "@maruaudio/gen-sdk/service/hosted"

const cloud = createCreatorAppGenerationConnector({ config, request: session.request })
// equivalent to credentials: { kind: "app", fetch, clientId }
```

Here `config` contains your registered `{ issuer, clientId }`, and `session`
is the signed-in creator app session from `createCreatorAppSession` in
`maruaudio/api`. Request the person's `inference:run` consent during sign-in.
This connector uses the app-bound account gateway rather than the personal
token API example above. In the source workspace, collect its audio like this:

```ts
import { createAudioGenerationService, collectGenerationJob } from "@maruaudio/gen-sdk/service"

await cloud.ready
const service = createAudioGenerationService({ connectors: [cloud] })
try {
  const result = await collectGenerationJob(service.submit(cloud.id, {
    prompt: "a soft wooden percussion hit",
    audioType: "one-shot",
    seconds: 2,
    model: "auto",
    seeds: [1042],
  }))
  // result.items contains indexed PCM audio. Check result.itemErrors and
  // result.terminal before handing it to your app's existing sample loader.
  console.log(result.items, result.itemErrors, result.terminal)
} finally {
  service.dispose()
}
```

An app owns playback, storage and the choice of when to install the new sample.
No Node filesystem helper is needed for this browser-compatible service.

## Keys and secrets

| Where your code runs | What it holds |
| --- | --- |
| A module face, a browser app, a DAW plug-in | The public app id only. Sign-in uses PKCE. Never a secret. |
| Your own server that signs people in | The app id, optionally a client secret from Creator apps |

A client secret is optional and only for an app whose token exchange runs on
its own server. Creator apps shows it once and keeps only a digest; pass it as
`clientSecret` to a server-mode `createCreatorAppSession`. Maru refuses a
secret sent from a browser. A module-only app cannot have one.

## Related

- [Maru sign-in in your own app](https://maru.audio/docs/sign-in-in-your-app.md)
- [Self-hosting overview](https://maru.audio/docs/self-hosting.md)
