AWS Marketplace · Quick Setup

Getting Started

End-to-end: AWS Marketplace subscription to your first signed-and-verified AI generation. Seven steps, roughly 15 minutes.

1

Prerequisites

An AWS account with console access. No special setup, no CDK bootstrap, no pre-existing infrastructure. STELR's template uses BootstraplessSynthesizer — the only thing the deploying IAM principal needs is permission to create the resources the stack touches (KMS, Lambda, DynamoDB, S3, Step Functions, API Gateway, EventBridge, SQS, SNS, IAM roles). If your account enforces permission boundaries, see the required IAM policy.

2

Subscribe on AWS Marketplace

Go to the STELR listing and click Continue to Subscribe. Accept the terms. Marketplace triggers automatic provisioning on STELR's side — no separate form or signup.

STELR on AWS Marketplace ↗
Within a few minutes of subscribing, you'll receive an onboarding email at the address on your AWS account. It contains three things you'll need:
  • Customer ID — a UUID you enter when deploying the stack (e.g. a3f7c821-09be-4d12-b501-8e6f3c112a04)
  • Registry JWT (Attestation Token) — used in Step 3 to pre-create an SSM parameter
  • API Key — the key you'll use in every API call; also goes into SSM in Step 3

If the email doesn't arrive within 5 minutes, check spam or contact support@stelrai.com.

3

Pre-flight: create two SSM parameters

Do this before deploying the stack

The stack deploys a Lambda (stelr-key-registrar-customer) that you invoke once after deployment. It reads these two SSM parameters to register your signing key with STELR's registry and seed your API credentials into your stack's DynamoDB table. Create them in the same region you'll deploy to — both values are in the onboarding email from Step 2.

# Registry JWT — authenticates the key registration with STELR
aws ssm put-parameter \
  --name /stelr/registry/attestation-token \
  --value "<Registry JWT from your STELR onboarding email>" \
  --type SecureString \
  --region us-east-1 \
  --profile <YOUR-AWS-PROFILE>

# API key — used in the X-API-Key header on every call
aws ssm put-parameter \
  --name /stelr/api-key \
  --value "<API key from your STELR onboarding email>" \
  --type SecureString \
  --region us-east-1 \
  --profile <YOUR-AWS-PROFILE>
One-time credentials. The credentials page link in the email expires after one view. Copy both values into SSM before closing the tab. If you lose them, contact support@stelrai.com.
4

Launch the CloudFormation stack

From the STELR listing on AWS Marketplace, click Launch. The deployment form shows one field under STELR Configuration: STELR Customer ID (from onboarding email). Paste the UUID from your email. That is the only value you supply.

Click Launch CloudFormation. The stack deploys approximately 96 resources — all inside your own AWS account. Nothing is created in STELR's account.

What gets deployed:

  • Two KMS keys: RSA 4096 for signing (alias/stelr/customer) and AES-256 for ledger encryption (alias/stelr-ledger/customer)
  • DynamoDB governance ledger with three GSIs
  • S3 WORM bucket (Object Lock, Compliance mode) — contents are immutable for the retention period
  • Step Functions Express state machine (Provenance → Policy → Ledger Write)
  • API Gateway with Lambda token authorizer using X-API-Key
  • Eight Lambda functions: generation, verification, async pipeline, key registrar, and support functions
  • IAM deny policy blocking direct Bedrock access + CloudTrail bypass detection via EventBridge → SNS

During deployment

The stack takes 3–6 minutes. Status cycles through CREATE_IN_PROGRESS for each resource. The KMS keys, S3 bucket, and DynamoDB table all provision before the Lambda functions. Refresh the CloudFormation console to track progress. When the status reaches CREATE_COMPLETE, proceed to Step 5.

Required IAM permissions (expand if you use permission boundaries)

Create a CloudFormation service role with this policy. The most common deployment failure is missing iam:CreateRole — the stack creates an execution role for every Lambda.

{
  "Version": "2012-10-17",
  "Statement": [
    { "Sid": "CloudFormation", "Effect": "Allow",
      "Action": ["cloudformation:CreateStack","cloudformation:DeleteStack",
        "cloudformation:DescribeStacks","cloudformation:DescribeStackEvents",
        "cloudformation:DescribeStackResource","cloudformation:DescribeStackResources",
        "cloudformation:GetTemplate","cloudformation:ValidateTemplate",
        "cloudformation:UpdateStack"],
      "Resource": "*" },
    { "Sid": "IAM", "Effect": "Allow",
      "Action": ["iam:CreateRole","iam:DeleteRole","iam:GetRole",
        "iam:PutRolePolicy","iam:DeleteRolePolicy","iam:GetRolePolicy",
        "iam:AttachRolePolicy","iam:DetachRolePolicy",
        "iam:ListAttachedRolePolicies","iam:ListRolePolicies","iam:PassRole",
        "iam:CreatePolicy","iam:DeletePolicy","iam:GetPolicy",
        "iam:CreatePolicyVersion","iam:DeletePolicyVersion","iam:ListPolicyVersions",
        "iam:TagRole","iam:UntagRole","iam:TagPolicy","iam:UntagPolicy"],
      "Resource": "*" },
    { "Sid": "KMS", "Effect": "Allow",
      "Action": ["kms:CreateKey","kms:DescribeKey","kms:GetKeyPolicy","kms:PutKeyPolicy",
        "kms:EnableKeyRotation","kms:GetKeyRotationStatus",
        "kms:ScheduleKeyDeletion","kms:CancelKeyDeletion",
        "kms:CreateAlias","kms:DeleteAlias","kms:UpdateAlias","kms:ListAliases",
        "kms:TagResource","kms:UntagResource","kms:ListResourceTags"],
      "Resource": "*" },
    { "Sid": "S3", "Effect": "Allow",
      "Action": ["s3:CreateBucket","s3:DeleteBucket","s3:GetBucketLocation",
        "s3:PutBucketPolicy","s3:GetBucketPolicy","s3:DeleteBucketPolicy",
        "s3:PutBucketVersioning","s3:GetBucketVersioning",
        "s3:PutEncryptionConfiguration","s3:GetEncryptionConfiguration",
        "s3:PutObjectLockConfiguration","s3:GetObjectLockConfiguration",
        "s3:PutLifecycleConfiguration","s3:GetLifecycleConfiguration",
        "s3:PutBucketPublicAccessBlock","s3:GetBucketPublicAccessBlock",
        "s3:PutBucketTagging","s3:GetObject"],
      "Resource": "*" },
    { "Sid": "DynamoDB", "Effect": "Allow",
      "Action": ["dynamodb:CreateTable","dynamodb:DeleteTable",
        "dynamodb:DescribeTable","dynamodb:UpdateTable",
        "dynamodb:UpdateTimeToLive","dynamodb:DescribeTimeToLive",
        "dynamodb:DescribeContinuousBackups","dynamodb:UpdateContinuousBackups",
        "dynamodb:TagResource","dynamodb:UntagResource"],
      "Resource": "*" },
    { "Sid": "Lambda", "Effect": "Allow",
      "Action": ["lambda:CreateFunction","lambda:DeleteFunction",
        "lambda:GetFunction","lambda:GetFunctionConfiguration",
        "lambda:UpdateFunctionCode","lambda:UpdateFunctionConfiguration",
        "lambda:AddPermission","lambda:RemovePermission",
        "lambda:CreateEventSourceMapping","lambda:DeleteEventSourceMapping",
        "lambda:GetEventSourceMapping","lambda:UpdateEventSourceMapping",
        "lambda:PublishLayerVersion","lambda:DeleteLayerVersion",
        "lambda:GetLayerVersion","lambda:AddLayerVersionPermission",
        "lambda:RemoveLayerVersionPermission",
        "lambda:TagResource","lambda:UntagResource"],
      "Resource": "*" },
    { "Sid": "StepFunctions", "Effect": "Allow",
      "Action": ["states:CreateStateMachine","states:DeleteStateMachine",
        "states:DescribeStateMachine","states:UpdateStateMachine",
        "states:TagResource","states:UntagResource"],
      "Resource": "*" },
    { "Sid": "ApiGateway", "Effect": "Allow",
      "Action": ["apigateway:POST","apigateway:GET","apigateway:PUT",
        "apigateway:DELETE","apigateway:PATCH"],
      "Resource": "*" },
    { "Sid": "EventBridgeSNSSQS", "Effect": "Allow",
      "Action": ["events:CreateEventBus","events:DeleteEventBus","events:DescribeEventBus",
        "events:PutRule","events:DeleteRule","events:DescribeRule",
        "events:PutTargets","events:RemoveTargets","events:ListTargetsByRule",
        "events:TagResource","events:UntagResource",
        "sns:CreateTopic","sns:DeleteTopic","sns:GetTopicAttributes",
        "sns:SetTopicAttributes","sns:AddPermission","sns:RemovePermission",
        "sns:TagResource","sns:UntagResource",
        "sqs:CreateQueue","sqs:DeleteQueue","sqs:GetQueueUrl",
        "sqs:GetQueueAttributes","sqs:SetQueueAttributes",
        "sqs:TagQueue","sqs:UntagQueue"],
      "Resource": "*" },
    { "Sid": "CloudWatchLogs", "Effect": "Allow",
      "Action": ["logs:CreateLogGroup","logs:DeleteLogGroup",
        "logs:PutRetentionPolicy","logs:DescribeLogGroups",
        "logs:TagLogGroup","logs:UntagLogGroup",
        "logs:CreateLogDelivery","logs:DeleteLogDelivery",
        "logs:PutResourcePolicy","logs:DescribeResourcePolicies"],
      "Resource": "*" }
  ]
}
5

Find your endpoint — CloudFormation Outputs tab

After CREATE_COMPLETE, open the stack in the CloudFormation console and click the Outputs tab. Your stack produces eight outputs:

Output key What it is
ApiEndpoint This is the base URL for every API call. Format: https://<id>.execute-api.<region>.amazonaws.com/customer/. Note the trailing slash — strip it before appending a path (see Step 7).
SigningKeyArn ARN of your RSA 4096 KMS signing key (alias/stelr/customer). The key registrar Lambda publishes the public half to STELR's registry.
LedgerTableName DynamoDB governance ledger (StelrLedger-customer). Append-only; hash-chained. The delete() path raises NotImplementedError by design.
TrustLedgerBucket S3 WORM bucket (stelr-trust-ledger-customer-<account-id>). Object Lock Compliance mode — contents cannot be modified or deleted for the retention period.
BedrockDenyPolicyArn IAM managed policy that blocks bedrock:InvokeModel and related actions for any principal except STELR's execution roles. Attach to workforce roles (optional but recommended for governance completeness).
SecurityAlertsTopicArn SNS topic that fires when CloudTrail detects a Bedrock call from a non-STELR principal. Subscribe your SIEM or security email.
PipelineArn ARN of the Step Functions Express state machine. Every sync generation starts this pipeline synchronously (Provenance → Policy → Ledger Write). Useful for CloudWatch monitoring.
StelrApiEndpoint… CDK-generated duplicate of ApiEndpoint. Same URL. Ignore this one.

Copy the ApiEndpoint value — you'll use it in Step 7.

6

Register your signing key

Manual step — required once

The stack deploys the key registrar Lambda but does not invoke it automatically. Run this once after the stack reaches CREATE_COMPLETE. It retrieves your KMS public key, registers it with STELR's registry, and seeds your API key into your stack's DynamoDB table.

aws lambda invoke \
  --function-name stelr-key-registrar-customer \
  --region <YOUR-DEPLOYMENT-REGION> \
  --profile <YOUR-AWS-PROFILE> \
  response.json

Expected output — and why it shows an error

{"StatusCode": 200, "FunctionError": "Unhandled", "ExecutedVersion": "$LATEST"}

The FunctionError is expected when the Lambda is invoked directly rather than via CloudFormation. The Lambda completes the registration work (key → STELR's registry, API key → your DynamoDB) and then fails trying to signal CloudFormation — which isn't involved in a direct invocation. The error does not mean the registration failed.

Verify success in CloudWatch Logs:

aws logs filter-log-events \
  --log-group-name /aws/lambda/stelr-key-registrar-customer \
  --filter-pattern "key_registered" \
  --region <YOUR-DEPLOYMENT-REGION> \
  --profile <YOUR-AWS-PROFILE> | jq '.events[].message | fromjson | {message, key_id}'

A successful run produces two log entries: key_registered (key is in STELR's registry) and api_key_seeded (credentials are ready). If neither appears, the SSM parameters from Step 3 were likely missing — create them and re-invoke.

7

Prove it works — sign a generation, then verify it

Set two variables from your CloudFormation Outputs. The ApiEndpoint value includes a trailing slash — the ${STELR_API%/} syntax strips it.

# From CloudFormation Outputs tab
STELR_API="https://<YOUR-API-ID>.execute-api.<REGION>.amazonaws.com/customer"
STELR_KEY="<YOUR-API-KEY>"

POST /generate — invoke Bedrock and sign the output

RESPONSE=$(curl -s -X POST "${STELR_API%/}/generate" \
  -H "X-API-Key: $STELR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt": "Reply in one sentence confirming STELR is running.", "max_tokens": 64}')

echo "$RESPONSE" | jq '{content_id, output, signature_id, signed_at}'

HTTP 201 — successful response shape:

{
  "content_id":   "f4a91b2c-3d5e-4f78-9a01-b2c3d4e5f678",
  "output":       "STELR is running and your AI output has been cryptographically signed.",
  "model_id":     "us.anthropic.claude-haiku-4-5-20251001-v1:0",
  "provider":     "bedrock",
  "signature_id": "f4a91b2c-3d5e-4f78-9a01-b2c3d4e5f678",
  "signature":    "eyJhbGciOiJSUzI1NiJ9...",
  "output_hash":  "a3b4c5d6e7f8...",
  "signed_at":    "2026-09-30T14:23:11.042Z",
  "s3_key":       "tenant/f4a91b2c.../ledger.json",
  "chain_position": 1,
  "correlation_id": "c1d2e3f4-..."
}

content_id and signature_id are the same UUID for synchronous generation — use either to verify.

POST /verify/{content_id} — verify the signature against the output

# Extract the values from the generate response
CONTENT_ID=$(echo "$RESPONSE" | jq -r .signature_id)

curl -s -X POST "${STELR_API%/}/verify/$CONTENT_ID" \
  -H "X-API-Key: $STELR_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"content\": $(echo "$RESPONSE" | jq '.output')}" \
  | jq '{valid, found, tampered, signed_at, verified_at}'

HTTP 200 — successful verification:

{
  "valid":       true,
  "found":       true,
  "tampered":    false,
  "signed_at":   "2026-09-30T14:23:11.042Z",
  "verified_at": "2026-09-30T14:23:17.389Z"
}
"valid": true — your stack is fully operational. The RSA 4096 KMS signature verified, the SHA-256 hash matched, and the ledger entry was located.

Full API surface

Endpoint Body Returns
POST /generate {"prompt":"...","max_tokens":1024,"model_id":"...","metadata":{}}
prompt required; model_id optional (default: us.anthropic.claude-haiku-4-5-20251001-v1:0)
201 — output + signature_id
POST /generate/async Same shape as /generate 202 — content_id immediately; poll GET /generate/status/{content_id}
POST /verify/{content_id} {"content":"<exact AI output text>"} 200 — {"valid":true,"found":true,"tampered":false,...}
POST /verify
or /verify/batch
[{"content_id":"...","content":"..."}] — JSON array, up to 100 items 200 — {"results":[...],"summary":{...}}
All endpoints require X-API-Key: <your-api-key> header. The authorizer validates the key's SHA-256 hash against your DynamoDB ledger — the raw key is never stored. Request bodies (including prompts) are never logged to CloudWatch (data_trace_enabled=false on all stages).

Troubleshooting

Stack creation fails: not authorized to perform or AccessDenied

The stack rolls back to ROLLBACK_COMPLETE immediately or during resource creation

The IAM principal launching the stack is missing one or more of the required permissions. The most common culprits in order of frequency:

  • iam:CreateRole / iam:PassRole — the stack creates an execution role for every Lambda. This is usually the first permission that enterprise permission boundaries restrict.
  • kms:CreateKey — required for both the signing CMK and the ledger encryption CMK.
  • s3:PutObjectLockConfiguration — required to enable Object Lock on the trust ledger bucket; this permission is often omitted from S3 permission sets.

Create a CloudFormation service role with the full policy from Step 4 above, pass it as the IAM role when launching the stack, and redeploy.

Key registrar returns "FunctionError": "Unhandled" with "errorMessage": "'ResponseURL'"

response.json contains a KeyError stack trace instead of a key_id

This is expected behavior for manual invocation. The Lambda registers the key and seeds the API credentials, then tries to signal CloudFormation via a pre-signed ResponseURL — which doesn't exist in a direct invoke event. The registration completes before the error occurs.

To confirm the registration worked, check CloudWatch Logs for both log entries:

aws logs filter-log-events \
  --log-group-name /aws/lambda/stelr-key-registrar-customer \
  --filter-pattern '"key_registered"' \
  --region <YOUR-REGION> --profile <YOUR-PROFILE>

If key_registered or api_key_seeded are absent, the SSM parameters were missing when the Lambda ran. Create them (Step 3) and invoke the Lambda again — registration and seeding are both idempotent.

API calls return 401 or 403

curl returns {"message":"Unauthorized"} or the authorizer policy denies the call

Three distinct causes:

  1. Wrong header name. The authorizer reads X-API-Key — not Authorization. Confirm your curl command uses -H "X-API-Key: $STELR_KEY".
  2. API key not seeded. The key registrar Lambda reads /stelr/api-key from SSM and writes a hashed record to DynamoDB. If that SSM parameter didn't exist when the Lambda ran, the DynamoDB record was never written. Create the SSM parameter (Step 3) and re-invoke stelr-key-registrar-customer.
  3. Key value mismatch. The authorizer stores sha256(raw_key), not the raw key. If you stored a different value in SSM than what you're sending in the header, the hashes won't match. Confirm the exact value in /stelr/api-key matches the key you're using in the header.

/generate returns 502 with BEDROCK_ERROR — AccessDeniedException

The Bedrock invocation fails even though the stack deployed successfully

The default model (us.anthropic.claude-haiku-4-5-20251001-v1:0) is a cross-region inference profile. It routes calls through multiple regions, so you need to enable model access in each region the profile uses — not just your deployment region.

In the AWS console:

  1. Open Amazon Bedrock in the same region as the stack.
  2. Go to Model access in the left sidebar.
  3. Request access to Claude Haiku 4.5 (Anthropic).
  4. Repeat in us-west-2 if your deployment is in us-east-1, and vice versa — the us.* cross-region profile can route to either.

To use a different model, pass "model_id" in your generate request body with any Bedrock model ID you have access to.

Still stuck?

Email support@stelrai.com with your STELR Customer ID, the CloudFormation stack name, your AWS region, and a brief description of the error. Include the correlation_id from any failed API response if you have it.