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
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-idInstall commands
cd examples/docs-quickstarts/spring && mvn dependency:go-offlineHappy 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.
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.
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.
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.
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.
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.
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.
403 missing scope
Issue a key with customers:create, plans:read, subscriptions:write, entitlements:read, usage:write, usage:read, and cleanup permissions.
Entitlement denied
Confirm MONETIZEKIT_PLAN_ID is the isolated plan that grants MONETIZEKIT_FEATURE_KEY and does not grant MONETIZEKIT_DENIED_FEATURE_KEY.
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.
Deploy + smoke test
Platform: Docker Compose.
Build the Spring Boot jar and inject the seven quickstart environment variables.
Run the service image before invoking the jar's smoke entry point.
(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.
monetizekit auth service-token create docs-quickstart --scopes customers:view --ttl 30dmonetizekit customers list