# The Playcode SDK

> @playcode/sdk for your app's backend: send email, generate video, call any management operation and verify webhooks, with typed errors.
> Source: https://playcode.io/docs/developers/sdk - last reviewed 2026-10-10.

`@playcode/sdk` is the package your app's backend uses to reach Playcode: send email, generate video, call any management operation, and verify webhook deliveries. It has no dependencies, ships ESM and CommonJS builds, and comes installed in new Cloud apps.

## Install

```bash
npm install @playcode/sdk
```

```ts
import { Playcode } from '@playcode/sdk'

const pc = new Playcode() // reads PLAYCODE_SECRET_KEY when a call needs it
```

Creating the client never throws and never calls the network: a missing key fails on the first call that needs it. Inside a Cloud app, the key is already in the environment.

## Configuration

| Option | Environment variable | What it is |
|---|---|---|
| `apiKey` | `PLAYCODE_SECRET_KEY` | The app's secret key (`pcsk_...`), for email and media. Server-side only |
| `apiToken` | `PLAYCODE_TOKEN` | An access token (`pc_live_...`), for `pc.api` |
| `emailUrl` | `PLAYCODE_EMAIL_URL` | Playcode Email's address. Set it only for a test environment |
| `mediaUrl` | `PLAYCODE_MEDIA_URL` | Playcode Media's address. Set it only for a test environment |
| `apiOrigin` | `PLAYCODE_API_ORIGIN` | The API's address; `https://playcode.io` by default |
| `timeoutMs` | | How long one email or video request may take; 10 seconds by default |

The environment is read at call time, so values loaded after the client was created still count.

## Email: `pc.email.send()`

```ts
const { status, messageId } = await pc.email.send({
  to: 'anna@example.com',
  subject: 'Your invoice',
  html: '<p>Thank you!</p>',
  replyTo: 'support@example.com',
  idempotencyKey: invoice.id,
})
```

| Field | What it does |
|---|---|
| `to` | One address or a list. Leave it out to email the project owner's account address |
| `subject` | The subject line |
| `html`, `text` | The body: at least one of them |
| `from` | The sender. An address at your verified domain is used as written; otherwise the email goes out from the app's `playcode.email` address, with this name on it |
| `replyTo` | Where replies go |
| `headers` | Stored with the email, not sent |
| `idempotencyKey` | Sending again with the same key returns the first email instead of a second one |

The answer's `status` is `sending`, `held`, `captured` or `withheld`: see [Send email from your app](/docs/add-to-your-app/send-email). New values can appear, so treat an unknown one as not delivered yet. The SDK never retries a send by itself.

`import { sendEmail } from '@playcode/sdk/email'` gives the same call without the client.

## Video and images: `pc.media`

```ts
const job = await pc.media.video({ prompt: 'A paper boat on a lake at dawn', quality: 'draft', idempotencyKey: id })
const { url } = await job.wait()
```

`pc.media.video()` starts a render and answers a job: `job.wait()` waits until the video is ready, 15 minutes at most by default, `job.get()` reads its state, and `job.cancel()` stops it. `quality` is `standard`, `high` or `draft`. The result is an address on Playcode, and each second of video is paid from the workspace's credits.

The app's key needs the media service for video: ask the agent to switch on media for the app. `pc.media.image()` is in the SDK too, but your app's key cannot use it yet: see Limits.

## Management API: `pc.api`

```ts
const pc = new Playcode({ apiToken: process.env.PLAYCODE_TOKEN })

const domain = await pc.api.domainGet({ domain: 'example.com' })
if (domain.uid) await pc.api.domainUpdate({ uid: domain.uid, input: { autoRenew: true } })
```

`pc.api` has one typed method for each operation of the API at the SDK's release. Its credential is an access token, never the app's secret key. Each write carries an `Idempotency-Key`, and the same key rides every retry: the client retries a network error, a 429 or a 5xx 2 times, so a retry never doubles a write. Pass your own key as the second argument: `{ idempotencyKey }`. See [The API](/docs/developers/api) and [Access tokens and permissions](/docs/developers/tokens-and-permissions).

## Webhooks: `verifyWebhook()`

```ts
import { verifyWebhook } from '@playcode/sdk'

const { id, event } = verifyWebhook({
  signature: req.headers['playcode-signature'],
  body: rawBody,
  secret: process.env.PLAYCODE_WEBHOOK_SECRET!,
})
```

Pass the raw request body, exactly as it arrived: a re-encoded body does not match the signature. It throws `PlaycodeWebhookError` for a bad signature, or a delivery older than 300 seconds (`toleranceSeconds` changes that). See [Webhooks](/docs/developers/webhooks).

## Errors

| Error | When | What it carries |
|---|---|---|
| `PlaycodeApiError` | Playcode answered with an error | `status`, `code`, `message` (Playcode's own words), `requestId` |
| `PlaycodeConnectionError` | Playcode never answered: network, TLS or timeout | `message`, `cause` |
| `PlaycodeConfigError` | No key or token for the call | `message`, naming the missing variable |
| `PlaycodeWebhookError` | A delivery failed verification | `message` |

## Limits

- The secret key stays on the server. A key in browser code is a leaked key: anyone who has it can send email and use AI as your app.
- The SDK does not call AI models. Your app calls them with the model maker's own SDK through Playcode: see [Add AI to your app](/docs/add-to-your-app/ai-in-your-app).
- Image generation from your app's own key is not available yet: the key cannot carry the image permission today.
- An operation added to the API after the SDK's release needs a newer SDK.
