# Spring Quickstart

Complete the raw-HTTP MonetizeKit activation journey in a Spring service.

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

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

## Prerequisites

- Java 21+
- Maven
- 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
cd examples/docs-quickstarts/spring && mvn dependency:go-offline
```

## 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.

```java
private static Customer createCustomer(Config config, String idempotencyKey) throws Exception {
        String payload = """
            {"name":%s,"email":%s}
            """.formatted(
                jsonString("Docs quickstart " + config.runId()),
                jsonString("docs-quickstart+" + config.runId() + "@example.com")
            ).trim();
        ApiResponse response = requestJson(
            config,
            "POST",
            "/customers",
            payload,
            idempotencyKey,
            201
        );
        return new Customer(stringField(response.body(), "id"));
    }
```

Expected HTTP status: 201.

### 2. Select the isolated published plan

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

```java
private static Plan selectPlan(Config config) throws Exception {
        ApiResponse response = requestJson(config, "GET", "/plans", null, null, 200);
        Matcher matcher = Pattern.compile(
            "\\\"id\\\"\\s*:\\s*\\\"((?:\\\\.|[^\\\"])*)\\\"",
            Pattern.DOTALL
        ).matcher(response.body());
        while (matcher.find()) {
            String candidateId = matcher.group(1);
            if (candidateId.equals(config.planId())) {
                return new Plan(candidateId);
            }
        }
        throw new IllegalStateException(
            "Isolated plan " + config.planId() + " is not published in this workspace"
        );
    }
```

Expected HTTP status: 200.

### 3. Attach the plan

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

```java
private static Subscription attachPlan(
        Config config,
        String customerId,
        String planId,
        String idempotencyKey
    ) throws Exception {
        String payload = """
            {"customerId":%s,"planId":%s,"status":"active"}
            """.formatted(jsonString(customerId), jsonString(planId)).trim();
        ApiResponse response = requestJson(
            config,
            "POST",
            "/subscriptions",
            payload,
            idempotencyKey,
            201
        );
        return new Subscription(stringField(response.body(), "id"));
    }
```

Expected HTTP status: 201.

### 4. Check and enforce an entitlement

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

```java
private static EntitlementDecision requireEntitlement(
        Config config,
        String customerId
    ) throws Exception {
        String path = "/entitlements/" + pathSegment(customerId)
            + "/" + pathSegment(config.featureKey());
        ApiResponse response = requestJson(config, "GET", path, null, null, 200);
        EntitlementDecision decision = new EntitlementDecision(
            booleanField(response.body(), "allowed"),
            stringField(response.body(), "featureKey"),
            stringField(response.body(), "reason"),
            stringField(response.body(), "reasonCode")
        );
        if (!decision.allowed()) {
            throw new IllegalStateException(
                "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.

```java
private static String submitUsage(
        Config config,
        String customerId,
        String idempotencyKey
    ) throws Exception {
        String payload = """
            {"customerId":%s,"meterId":%s,"value":1}
            """.formatted(jsonString(customerId), jsonString(config.meterId())).trim();
        return requestJson(
            config,
            "POST",
            "/usage/events",
            payload,
            idempotencyKey,
            201
        ).body();
    }
```

Expected HTTP status: 201.

### 6. Verify the observed usage

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

```java
private static String validateUsage(Config config, String customerId) throws Exception {
        String path = "/usage/" + pathSegment(customerId)
            + "/" + pathSegment(config.meterId());
        return requestJson(config, "GET", path, null, null, 200).body();
    }
```

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. Build the Spring Boot jar and inject the seven quickstart environment variables.
2. Run the service image before invoking the jar's smoke entry point.

```bash
(cd examples/docs-quickstarts/spring && mvn -q package && java -jar target/monetizekit-docs-quickstart-1.0.0.jar --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 auth service-token create docs-quickstart --scopes customers:view --ttl 30d
```

```bash
monetizekit customers list
```