# Express Quickstart

Complete the raw-HTTP MonetizeKit activation journey in an Express service.

<!-- docs-source -->
Source: https://learning.monetizekit.app/docs/quickstarts/express
<!-- /docs-source -->

Express uses the Raw HTTP path in JavaScript. Complete every step with an isolated non-production workspace before deploying.

## Prerequisites

- Node 22+
- pnpm
- A scoped non-production API key

## Environment setup

```bash
MONETIZEKIT_BASE_URL=https://app.monetizekit.app/api/v1
MONETIZEKIT_API_KEY=${MONETIZEKIT_API_KEY}
MONETIZEKIT_PLAN_ID=${MONETIZEKIT_PLAN_ID}
MONETIZEKIT_FEATURE_KEY=${MONETIZEKIT_FEATURE_KEY}
MONETIZEKIT_DENIED_FEATURE_KEY=${MONETIZEKIT_DENIED_FEATURE_KEY}
MONETIZEKIT_METER_ID=${MONETIZEKIT_METER_ID}
MONETIZEKIT_RUN_ID=replace-with-a-unique-run-id
```

## Install commands

```bash
pnpm add express
```

## Happy path

### 1. Create a customer

Create a unique test customer. Raw HTTP variants send a deterministic idempotency key; the current Node SDK does not expose one for customer creation.

```javascript
async function createCustomer(config, idempotencyKey) {
  return requestJson(
    config,
    "/customers",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify({
        name: `Docs quickstart ${config.runId}`,
        email: `docs-quickstart+${config.runId}@example.com`,
      }),
    },
    201,
  );
}
```

Expected HTTP status: 201.

### 2. Select the isolated published plan

List published plans and select the exact MONETIZEKIT_PLAN_ID provisioned for this run.

```javascript
async function selectPlan(config) {
  const plans = await requestJson(config, "/plans", { method: "GET" }, 200);
  const plan = plans.data?.find((candidate) => candidate.id === config.planId);
  if (!plan) {
    throw new Error(`Isolated plan ${config.planId} is not published in this workspace`);
  }
  return plan;
}
```

Expected HTTP status: 200.

### 3. Attach the plan

Create the customer's subscription. Raw HTTP variants reuse a deterministic idempotency key.

```javascript
async function attachPlan(config, customerId, planId, idempotencyKey) {
  return requestJson(
    config,
    "/subscriptions",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify({ customerId, planId, status: "active" }),
    },
    201,
  );
}
```

Expected HTTP status: 201.

### 4. Check and enforce an entitlement

Request the feature decision and stop protected work unless `allowed` is true.

```javascript
async function requireEntitlement(config, customerId) {
  const decision = await requestJson(
    config,
    `/entitlements/${encodeURIComponent(customerId)}/${encodeURIComponent(config.featureKey)}`,
    { method: "GET" },
    200,
  );
  if (!decision.allowed) {
    throw new Error(
      `Entitlement denied (${decision.reasonCode}): ${decision.reason}`,
    );
  }
  return decision;
}
```

Expected HTTP status: 200.

### 5. Submit usage

Record one metered event with a deterministic idempotency key that remains stable across retries.

```javascript
async function submitUsage(config, customerId, idempotencyKey) {
  return requestJson(
    config,
    "/usage/events",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify({
        customerId,
        meterId: config.meterId,
        value: 1,
      }),
    },
    201,
  );
}
```

Expected HTTP status: 201.

### 6. Verify the observed usage

Read the meter after submission and include the observed result in the smoke output.

```javascript
async function validateUsage(config, customerId) {
  return requestJson(
    config,
    `/usage/${encodeURIComponent(customerId)}/${encodeURIComponent(config.meterId)}`,
    { method: "GET" },
    200,
  );
}
```

Expected HTTP status: 200.

## Troubleshooting

### 401 missing_api_key / invalid_api_key

Confirm the API key is present, active, and uses the non-production prefix for the environment you intend to test.

[Read the full guide](/docs/troubleshooting/auth-issues)

### 403 missing scope

Issue a key with customers:create, plans:read, subscriptions:write, entitlements:read, usage:write, usage:read, and cleanup permissions.

[Read the full guide](/docs/troubleshooting/auth-issues)

### Entitlement denied

Confirm MONETIZEKIT_PLAN_ID is the isolated plan that grants MONETIZEKIT_FEATURE_KEY and does not grant MONETIZEKIT_DENIED_FEATURE_KEY.

[Read the full guide](/docs/guides/entitlement-patterns)

### A retry records duplicate usage

Keep MONETIZEKIT_RUN_ID stable for retries of the same logical smoke run so the generated Idempotency-Key is reused.

[Read the full guide](/docs/troubleshooting/metering-issues)

## Deploy + smoke test

Platform: Docker Compose.

1. Install dependencies and inject the seven quickstart environment variables.
2. Build and run the service image before invoking its smoke entry point.

```bash
node examples/docs-quickstarts/express/quickstart.mjs --smoke
```

Expected: One JSON object with outcome "passed", allowed and denied entitlement decisions, and observed usage data, followed by successful subscription/customer cleanup.

## CLI links

Use the CLI to authenticate and inspect the same workspace after the smoke test.

```bash
monetizekit customers list
```

```bash
monetizekit workspace list
```