Welcome to the official APIKlone documentation. APIKlone is an enterprise-grade API Virtualization and stateful mocking proxy. Instantly record backend traffic, replay mocked responses conditionally, and auto-generate complex schemas to unblock your frontend and testing teams.
Note: APIKlone runs completely locally or isolated in your cloud. There is no bloated desktop application required; simply configure the proxy via the intuitive web dashboard or headless REST API.
Core Concepts: What is a Config?
Throughout APIKlone, the primary unit of scale is the Configuration (or "Config").
A Config maps exactly to a single target host. When you create a proxy, all endpoints under that host are automatically covered. For instance, pointing APIKlone to your Salesforce instance counts as one config, no matter how many hundreds of Salesforce endpoints you intend to mock or pass-through.
| Concept | Description |
|---|---|
| Config | A logical grouping tied to a specific upstream base URL (e.g. https://api.stripe.com). |
| Service Mock | An individual endpoint mapping (e.g. /v1/charges) that contains headers, payloads, and matching rules. |
| Traffic Recording | The ability for APIKlone to proxy a request natively, and seamlessly capture both the outbound request and inbound response for future local regeneration. |
Deploy in your cloud
APIKlone Self-Hosted is one container plus somewhere permanent to keep its data. Pick your cloud for step-by-step commands, including how to scale it out.
Permanent storage is not optional. A container's own disk is wiped on every restart and upgrade. Each guide attaches storage so your configs, mocks, users and API keys survive.
APIKlone on Azure Container Apps
Runs the container on Azure Container Apps with an Azure Files share mounted as its data folder. You need the Azure CLI in a Bash shell; Azure Cloud Shell works.
Deploy
Sign in and set names
Registering the providers is needed once per subscription. The storage account name must be globally unique: 3 to 24 lowercase letters and numbers.
az login
az extension add --name containerapp --upgrade
az provider register --namespace Microsoft.App
az provider register --namespace Microsoft.OperationalInsights
RESOURCE_GROUP="apiklone-rg"
LOCATION="uksouth"
ENVIRONMENT="apiklone-env"
APP_NAME="apiklone"
STORAGE_ACCOUNT="apiklonedata$RANDOM"
FILE_SHARE="apiklone-data"
STORAGE_LINK="apiklonestorage"
Create the environment
az group create --name $RESOURCE_GROUP --location $LOCATION
az containerapp env create \
--name $ENVIRONMENT \
--resource-group $RESOURCE_GROUP \
--location $LOCATION
Create the storage
A 5 GiB share is plenty for most teams; mock files are small.
az storage account create \
--name $STORAGE_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--kind StorageV2 \
--sku Standard_LRS \
--min-tls-version TLS1_2
az storage share-rm create \
--name $FILE_SHARE \
--storage-account $STORAGE_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--quota 5 \
--enabled-protocols SMB
Attach the storage to the environment
Container Apps connects to Azure Files with the storage account key.
STORAGE_KEY=$(az storage account keys list \
--account-name $STORAGE_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--query "[0].value" --output tsv)
az containerapp env storage set \
--name $ENVIRONMENT \
--resource-group $RESOURCE_GROUP \
--storage-name $STORAGE_LINK \
--storage-type AzureFile \
--azure-file-account-name $STORAGE_ACCOUNT \
--azure-file-account-key $STORAGE_KEY \
--azure-file-share-name $FILE_SHARE \
--access-mode ReadWrite
Deploy APIKlone
A volume mount needs a YAML definition. Paste the block as it is; the shell fills in the variables. The mount options give the container's non-root user (ID 1000) ownership of the share.
ACTIVATION_KEY="paste-your-activation-key-here"
ENVIRONMENT_ID=$(az containerapp env show \
--name $ENVIRONMENT \
--resource-group $RESOURCE_GROUP \
--query id --output tsv)
cat > apiklone.yaml <<EOF
location: $LOCATION
type: Microsoft.App/containerApps
properties:
managedEnvironmentId: $ENVIRONMENT_ID
configuration:
activeRevisionsMode: Single
ingress:
external: true
targetPort: 3000
transport: auto
allowInsecure: false
secrets:
- name: activation-key
value: "$ACTIVATION_KEY"
template:
containers:
- name: apiklone
image: docker.io/apiklone/apiklone:latest
resources:
cpu: 0.5
memory: 1Gi
env:
- name: ACTIVATION_KEY
secretRef: activation-key
- name: PORT
value: "3000"
probes:
- type: Liveness
httpGet:
path: /health
port: 3000
periodSeconds: 30
- type: Readiness
httpGet:
path: /health
port: 3000
periodSeconds: 10
volumeMounts:
- volumeName: apiklone-data
mountPath: /app/data
scale:
minReplicas: 1
maxReplicas: 1
volumes:
- name: apiklone-data
storageType: AzureFile
storageName: $STORAGE_LINK
mountOptions: "uid=1000,gid=1000,dir_mode=0770,file_mode=0660"
EOF
az containerapp create \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP \
--yaml apiklone.yaml
rm apiklone.yaml # it contains your activation key in plain text
Open it and sign in
az containerapp show \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP \
--query properties.configuration.ingress.fqdn --output tsvOpen https://<that-address> and sign in as admin / admin. Change that password straight away from the Team page: the address is public.
Check the data is on the share
You should see users.properties at least.
az storage file list \
--share-name $FILE_SHARE \
--account-name $STORAGE_ACCOUNT \
--account-key $STORAGE_KEY \
--query "[].name" --output tsv
Make it scalable
The file share is already shared by every replica, so scaling out is two changes: keep each signed-in browser on one replica, then raise the replica count.
# 1. Keep each browser on the replica that holds its sign-in session
az containerapp ingress sticky-sessions set \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP \
--affinity sticky
# 2. Run 2 to 5 replicas, adding one for every 50 concurrent requests
az containerapp update \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP \
--min-replicas 2 \
--max-replicas 5 \
--scale-rule-name http-load \
--scale-rule-type http \
--scale-rule-http-concurrency 50- Session affinity needs single revision mode and HTTP ingress, which is what this guide sets up.
- For sustained heavy traffic, use a Premium file share: every mocked call reads its mock file from the share, so share latency becomes response latency.
- To give one replica more headroom instead: az containerapp update --cpu 1.0 --memory 2.0Gi.
Read how APIKlone behaves with more than one replica before you scale out.
Day to day
| Upgrade | az containerapp update --name $APP_NAME --resource-group $RESOURCE_GROUP --image docker.io/apiklone/apiklone:<version> |
| Change a setting | az containerapp update ... --set-env-vars APP_BASE_URL=https://<that-address> |
| Back up | az storage share snapshot --name $FILE_SHARE --account-name $STORAGE_ACCOUNT --account-key $STORAGE_KEY, or enable Azure Backup on the share for a schedule. |
| Read the logs | az containerapp logs show --name $APP_NAME --resource-group $RESOURCE_GROUP --tail 100 --follow |
| Remove everything | az group delete --name $RESOURCE_GROUP deletes the app and all its data. |
If something goes wrong
| What you see | Likely cause |
|---|---|
| ACTIVATION FAILED in the log, replica restarting | The activation key is missing, mistyped or expired. Set the secret again and restart the revision. |
| EACCES / permission denied on /app/data | The mount options are missing, or the storage link is not ReadWrite. |
| Volume mount error at start-up | The storage link name does not match step 4, or the account key was rotated. Re-run az containerapp env storage set with the current key. |
| Configs disappear after a restart | The share is not mounted at /app/data. |
APIKlone on Google Cloud Run
Runs the container on Cloud Run and stores its data in a Cloud Storage bucket, which APIKlone supports natively. You need the gcloud CLI; Cloud Shell works.
Deploy
Sign in and set names
Bucket names are global, so the project ID is used as a prefix.
gcloud auth login
PROJECT_ID="your-project-id"
REGION="europe-west2"
SERVICE="apiklone"
BUCKET="${PROJECT_ID}-apiklone-data"
SERVICE_ACCOUNT="apiklone-run"
SA_EMAIL="${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com"
gcloud config set project $PROJECT_ID
gcloud services enable \
run.googleapis.com \
secretmanager.googleapis.com \
storage.googleapis.com
Create the storage bucket
APIKlone talks to Cloud Storage directly, so there is no volume to mount. Keep the bucket private.
gcloud storage buckets create gs://$BUCKET \
--location=$REGION \
--uniform-bucket-level-access \
--public-access-prevention
Create an identity for the service
The service runs as its own service account, with access to this one bucket only. No key file is created or downloaded.
gcloud iam service-accounts create $SERVICE_ACCOUNT \
--display-name="APIKlone on Cloud Run"
gcloud storage buckets add-iam-policy-binding gs://$BUCKET \
--member="serviceAccount:$SA_EMAIL" \
--role="roles/storage.objectAdmin"
Store the activation key
printf '%s' "paste-your-activation-key-here" | \
gcloud secrets create apiklone-activation-key \
--replication-policy=automatic \
--data-file=-
gcloud secrets add-iam-policy-binding apiklone-activation-key \
--member="serviceAccount:$SA_EMAIL" \
--role="roles/secretmanager.secretAccessor"
Deploy APIKlone
Setting GCS_BUCKET_NAME switches APIKlone to bucket storage. --no-cpu-throttling keeps the CPU available between requests, which the config reload and delayed async callbacks rely on.
gcloud run deploy $SERVICE \
--image=docker.io/apiklone/apiklone:latest \
--region=$REGION \
--service-account=$SA_EMAIL \
--port=3000 \
--allow-unauthenticated \
--set-env-vars=GCS_BUCKET_NAME=$BUCKET \
--set-secrets=ACTIVATION_KEY=apiklone-activation-key:latest \
--cpu=1 \
--memory=1Gi \
--no-cpu-throttling \
--min-instances=1 \
--max-instances=1--allow-unauthenticated makes the address public, which your applications need in order to call the mocks. APIKlone's own sign-in protects the admin screens.
Open it and sign in
gcloud run services describe $SERVICE \
--region=$REGION \
--format='value(status.url)'Open that address and sign in as admin / admin. Change that password straight away from the Team page.
Check the data is in the bucket
You should see users.properties at least.
gcloud storage ls gs://$BUCKET
Make it scalable
Bucket storage is shared by every instance, so scaling out is one command.
gcloud run services update $SERVICE \
--region=$REGION \
--session-affinity \
--min-instances=2 \
--max-instances=5 \
--concurrency=80- Cloud Run's session affinity is best effort. If an instance is removed or reaches its concurrency limit, the next request goes to another instance and that user signs in again.
- Every mocked call reads its mock from the bucket. Keep the bucket in the same region as the service to keep that fast.
- For production, pull the image through an Artifact Registry remote repository instead of directly from Docker Hub; Google recommends it for availability.
Read how APIKlone behaves with more than one replica before you scale out.
Day to day
| Upgrade | gcloud run services update $SERVICE --region=$REGION --image=docker.io/apiklone/apiklone:<version> |
| Change a setting | gcloud run services update $SERVICE --region=$REGION --update-env-vars=APP_BASE_URL=https://<that-address> |
| Back up | Turn on object versioning so overwritten or deleted files can be restored: gcloud storage buckets update gs://$BUCKET --versioning |
| Read the logs | gcloud run services logs read $SERVICE --region=$REGION --limit=100 |
| Read the audit trail | On Cloud Run the audit file lives on the instance's temporary disk, so read audit entries from Cloud Logging instead: gcloud logging read 'resource.type="cloud_run_revision" AND textPayload:"[AUDIT]"' --limit=50 |
If something goes wrong
| What you see | Likely cause |
|---|---|
| Deploy fails: container did not listen on the port | Usually the activation key. Check the logs for ACTIVATION FAILED; the server exits before it starts listening. |
| Deploy fails with a permission error on the secret | The service account lacks secretAccessor on the secret (step 4). |
| 403 storage errors in the log | The service account lacks storage.objectAdmin on the bucket (step 3), or the bucket name is wrong. |
| Configs disappear after a new revision | GCS_BUCKET_NAME is not set, so data went to the instance's temporary disk. |
| Deploy refused: unauthenticated access not allowed | An organisation policy blocks public services. Ask your cloud admin for an exception or put a load balancer in front. |
APIKlone on AWS ECS Fargate
Runs the container on ECS Fargate behind an Application Load Balancer, with an EFS file system mounted as its data folder. You need the AWS CLI v2 in a Bash shell; CloudShell works.
Deploy
Set names and find the network
This guide uses your account's default VPC and two of its public subnets. Use your own VPC and subnet IDs if you have a network standard.
export AWS_DEFAULT_REGION="eu-west-2"
APP="apiklone"
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
VPC_ID=$(aws ec2 describe-vpcs \
--filters Name=isDefault,Values=true \
--query "Vpcs[0].VpcId" --output text)
read SUBNET_1 SUBNET_2 <<< $(aws ec2 describe-subnets \
--filters Name=vpc-id,Values=$VPC_ID Name=default-for-az,Values=true \
--query "Subnets[0:2].SubnetId" --output text)
Create the firewall rules
Three security groups, each only reachable from the one before it: internet → load balancer → container → file system.
ALB_SG=$(aws ec2 create-security-group --group-name $APP-alb \
--description "APIKlone load balancer" --vpc-id $VPC_ID \
--query GroupId --output text)
TASK_SG=$(aws ec2 create-security-group --group-name $APP-task \
--description "APIKlone containers" --vpc-id $VPC_ID \
--query GroupId --output text)
EFS_SG=$(aws ec2 create-security-group --group-name $APP-efs \
--description "APIKlone file system" --vpc-id $VPC_ID \
--query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id $ALB_SG \
--protocol tcp --port 80 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-id $ALB_SG \
--protocol tcp --port 443 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-id $TASK_SG \
--protocol tcp --port 3000 --source-group $ALB_SG
aws ec2 authorize-security-group-ingress --group-id $EFS_SG \
--protocol tcp --port 2049 --source-group $TASK_SG
Create the storage
An encrypted EFS file system, reachable from both subnets. The access point makes the container's non-root user (ID 1000) the owner of its own folder.
EFS_ID=$(aws efs create-file-system \
--encrypted \
--throughput-mode elastic \
--tags Key=Name,Value=$APP-data \
--query FileSystemId --output text)
# Wait until this prints "available" before continuing
aws efs describe-file-systems --file-system-id $EFS_ID \
--query "FileSystems[0].LifeCycleState" --output text
aws efs create-mount-target --file-system-id $EFS_ID \
--subnet-id $SUBNET_1 --security-groups $EFS_SG
aws efs create-mount-target --file-system-id $EFS_ID \
--subnet-id $SUBNET_2 --security-groups $EFS_SG
ACCESS_POINT_ID=$(aws efs create-access-point \
--file-system-id $EFS_ID \
--posix-user Uid=1000,Gid=1000 \
--root-directory 'Path=/apiklone,CreationInfo={OwnerUid=1000,OwnerGid=1000,Permissions=750}' \
--query AccessPointId --output text)
Store the activation key
The execution role lets ECS pull the image, write logs and read this one secret, nothing else.
SECRET_ARN=$(aws secretsmanager create-secret \
--name $APP/activation-key \
--secret-string "paste-your-activation-key-here" \
--query ARN --output text)
aws iam create-role --role-name $APP-execution \
--assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ecs-tasks.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
aws iam attach-role-policy --role-name $APP-execution \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
aws iam put-role-policy --role-name $APP-execution \
--policy-name read-activation-key \
--policy-document "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"secretsmanager:GetSecretValue\",\"Resource\":\"$SECRET_ARN\"}]}"
Define the container
Paste the block as it is; the shell fills in the variables.
aws logs create-log-group --log-group-name /ecs/$APP
aws ecs create-cluster --cluster-name $APP
cat > task.json <<EOF
{
"family": "$APP",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"executionRoleArn": "arn:aws:iam::${ACCOUNT_ID}:role/${APP}-execution",
"containerDefinitions": [
{
"name": "apiklone",
"image": "docker.io/apiklone/apiklone:latest",
"essential": true,
"portMappings": [{ "containerPort": 3000, "protocol": "tcp" }],
"environment": [{ "name": "PORT", "value": "3000" }],
"secrets": [{ "name": "ACTIVATION_KEY", "valueFrom": "$SECRET_ARN" }],
"mountPoints": [{ "sourceVolume": "apiklone-data", "containerPath": "/app/data" }],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/$APP",
"awslogs-region": "$AWS_DEFAULT_REGION",
"awslogs-stream-prefix": "apiklone"
}
}
}
],
"volumes": [
{
"name": "apiklone-data",
"efsVolumeConfiguration": {
"fileSystemId": "$EFS_ID",
"transitEncryption": "ENABLED",
"authorizationConfig": { "accessPointId": "$ACCESS_POINT_ID" }
}
}
]
}
EOF
aws ecs register-task-definition --cli-input-json file://task.json
Create the load balancer and start the service
ALB_ARN=$(aws elbv2 create-load-balancer --name $APP \
--subnets $SUBNET_1 $SUBNET_2 --security-groups $ALB_SG \
--query "LoadBalancers[0].LoadBalancerArn" --output text)
TG_ARN=$(aws elbv2 create-target-group --name $APP \
--protocol HTTP --port 3000 --vpc-id $VPC_ID \
--target-type ip --health-check-path /health \
--query "TargetGroups[0].TargetGroupArn" --output text)
aws elbv2 create-listener --load-balancer-arn $ALB_ARN \
--protocol HTTP --port 80 \
--default-actions Type=forward,TargetGroupArn=$TG_ARN
aws ecs create-service \
--cluster $APP \
--service-name $APP \
--task-definition $APP \
--desired-count 1 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[$SUBNET_1,$SUBNET_2],securityGroups=[$TASK_SG],assignPublicIp=ENABLED}" \
--load-balancers "targetGroupArn=$TG_ARN,containerName=apiklone,containerPort=3000" \
--health-check-grace-period-seconds 60assignPublicIp=ENABLED lets the container pull its image from Docker Hub from a public subnet. The container itself still only accepts traffic from the load balancer.
Add HTTPS, then sign in
Get the load balancer's address:
aws elbv2 describe-load-balancers --load-balancer-arns $ALB_ARN \
--query "LoadBalancers[0].DNSName" --output textThe listener from step 6 is plain HTTP. Use it only to check the service is up (/health). Before anyone signs in, point a domain at the load balancer, request a certificate for it in AWS Certificate Manager, and add an HTTPS listener.
CERT_ARN="arn-of-your-acm-certificate"
aws elbv2 create-listener --load-balancer-arn $ALB_ARN \
--protocol HTTPS --port 443 \
--certificates CertificateArn=$CERT_ARN \
--default-actions Type=forward,TargetGroupArn=$TG_ARNThen open https://<your-domain> and sign in as admin / admin. Change that password straight away from the Team page.
Make it scalable
The EFS file system is already shared by every task, so scaling out is two changes: keep each signed-in browser on one task, then let the service add tasks under load.
# 1. Keep each browser on the task that holds its sign-in session
aws elbv2 modify-target-group-attributes --target-group-arn $TG_ARN \
--attributes \
Key=stickiness.enabled,Value=true \
Key=stickiness.type,Value=lb_cookie \
Key=stickiness.lb_cookie.duration_seconds,Value=86400
# 2. Run 2 to 5 tasks, adding tasks when average CPU passes 60%
aws application-autoscaling register-scalable-target \
--service-namespace ecs \
--resource-id service/$APP/$APP \
--scalable-dimension ecs:service:DesiredCount \
--min-capacity 2 --max-capacity 5
aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--resource-id service/$APP/$APP \
--scalable-dimension ecs:service:DesiredCount \
--policy-name cpu-target \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{"TargetValue":60.0,"PredefinedMetricSpecification":{"PredefinedMetricType":"ECSServiceAverageCPUUtilization"}}'- Tasks are spread across the two subnets, so the service survives the loss of one availability zone.
- Every mocked call reads its mock file from EFS. The elastic throughput mode set in step 3 grows with demand, so no tuning is needed.
- To give each task more headroom instead, raise cpu and memory in task.json and register it again.
Read how APIKlone behaves with more than one replica before you scale out.
Day to day
| Upgrade | Change the image tag in task.json, run aws ecs register-task-definition --cli-input-json file://task.json, then aws ecs update-service --cluster $APP --service $APP --task-definition $APP. |
| Change a setting | Add it to environment in task.json and roll it out the same way as an upgrade. |
| Back up | Turn on daily automatic backups: aws efs put-backup-policy --file-system-id $EFS_ID --backup-policy Status=ENABLED |
| Read the logs | aws logs tail /ecs/$APP --follow |
If something goes wrong
| What you see | Likely cause |
|---|---|
| Task stops with ResourceInitializationError mentioning EFS | The mount targets are not ready yet, or the file-system security group does not allow port 2049 from the container group (step 2). |
| Task stops: unable to retrieve secret | The execution role's policy does not cover the secret's ARN (step 4). |
| Task starts, then stops; log shows ACTIVATION FAILED | The activation key is mistyped or expired. |
| Load balancer returns 503; targets unhealthy | The container group does not allow port 3000 from the load-balancer group, or the health check path is not /health. |
| Task cannot pull the image | The subnets have no route to the internet. Use public subnets with a public IP, or private subnets with a NAT gateway. |
How scaling works
One replica is enough for most teams. Before you run more than one, it helps to know what is shared between replicas and what is not.
Stubbed and passthrough traffic
Calls to your stubbed and passthrough endpoints carry no session, so any replica can answer them. This is the traffic that scales.
Data
Configs, mock files, users and API keys live in the shared storage, so every replica sees the same data. A new mock file is served by all replicas immediately.
Config changes
The replica you make a change on applies it at once. The others re-read their configs every 30 seconds, so allow up to that long for a new or changed endpoint to be live everywhere.
Sign-in sessions
Sessions are held in each replica's memory. Turn on session affinity so a signed-in browser stays on one replica. If that replica is removed or restarted, its users sign in again.
Async callbacks
Delayed callbacks are queued in the shared storage and each one is claimed by a single replica, so a callback is not sent twice.
Logs
The Server Logs page shows the replica you happen to be on. With several replicas, read logs in your cloud's log service, which collects all of them.
Guidelines
- Keep at least one replica running. Scaling to zero makes the first call slow and stops delayed async callbacks from being sent.
- Keep storage close. Every mocked call reads its mock from storage, so put the storage in the same region as the containers.
- Lower the reload interval if 30 seconds is too long. Set POLL_INTERVAL_MS, for example to 10000.
- Have one person change a config at a time. All configs are saved together; if two administrators save at the same moment on different replicas, the later save wins.
- API keys work on every replica. Automation that uses the x-api-key header needs no session and no affinity.
Settings reference
APIKlone is configured with environment variables. Only the activation key is required.
| Variable | Default | Purpose |
|---|---|---|
| ACTIVATION_KEY | none (required) | Your licence key. The container exits at start-up without a valid one. |
| PORT | 3000 | Port the server listens on. |
| GCS_BUCKET_NAME | none | Store data in this Cloud Storage bucket instead of the /app/data folder. |
| APP_BASE_URL | http://localhost:3000 | Public address of the instance. Required for single sign-on, so the identity provider can send users back. |
| MANAGEMENT_API_KEY | none | A fixed API key for automation, accepted in the x-api-key header. Store it as a secret. |
| POLL_INTERVAL_MS | 30000 | How often each replica re-reads its configs from storage. |
| MOCK_MAX_DELAY_MS | 60000 | Longest response delay a mock file may set. |
| ASYNC_CALLBACK_BLOCK_PRIVATE | false | Set to true to stop async callbacks being sent to private network addresses. |
For single sign-on, register $APP_BASE_URL/auth/saml/callback (SAML, HTTP-POST binding) or $APP_BASE_URL/auth/oidc/callback (OIDC) with your identity provider. The Team → Configure SSO page shows the exact URL.
Key management best practices
An APIKlone deployment holds a handful of secrets. Knowing what each one unlocks tells you how carefully to guard it.
The keys in a deployment
| Key | What it unlocks | Where it lives |
|---|---|---|
| Activation key | Your licence. Anyone holding it can run APIKlone under your entitlement. | The ACTIVATION_KEY setting. |
| Management API key | Full administrative access to the REST API. | The optional MANAGEMENT_API_KEY setting. |
| API keys | The same API access, one key per pipeline or person. | Created on the API Keys page; stored in the data store. |
| Storage credentials | Everything in the data store. | Azure: the storage account key. Google Cloud and AWS: the service's identity, with no key to hold. |
| SSO client secret | Lets a service pose as APIKlone to your identity provider. | Saved from the SSO settings page into the data store. |
| Admin password | The whole admin interface. | Hashed, in the data store. |
What to do
Keep keys in your cloud's secret store
Never put a key in a container image, a source repository, a deploy script or a chat message. Each guide above stores the activation key as a secret and passes it to the container by reference. Do the same for MANAGEMENT_API_KEY. On Azure, a Container Apps secret can also point at a Key Vault secret if your organisation centralises keys there.
Prefer identities to keys
On Google Cloud the service account attached to Cloud Run reaches the bucket; on AWS the task's roles reach the secret and the file system. Do not create service-account key files or IAM access keys for APIKlone: a key that does not exist cannot leak.
Grant the least access that works
Give the service access to its own bucket or share and its own secret, not to the whole project or account. The guides scope the storage role to one bucket and the secret permission to one secret for this reason.
Give each pipeline and person their own API key
Create labelled keys on the API Keys page instead of sharing the management key. A key is shown once, so store it in the pipeline's own secret store straight away. The audit trail records each action against the key's label, and one key can be revoked without breaking the others.
Rotate on a schedule, and whenever someone leaves
Revoke and replace API keys regularly. Rotate the Azure storage account key and then link the share again with the new key. Renew the activation key before it expires: the container will not start with an expired key. The commands for replacing the activation key are below.
Replace the default admin password first
A new instance starts with the well-known account admin / admin on a public address. Change it before anything else, use single sign-on where your licence includes it, and remove users who leave.
Treat the data store as sensitive
It holds password hashes, API keys, the SSO client secret and recorded traffic. Keep the bucket or share private, limit who can browse it, and leave encryption at rest switched on (it is by default on all three clouds).
Remember that recordings capture credentials
Record mode saves the requests it sees, including headers such as Authorization. Recordings and config export files can therefore contain live tokens for your real backends. Record against test systems where you can, and never commit an export file to a public repository.
Review the audit trail
Sign-ins, failed sign-ins, key generation and revocation, user changes and config changes are all recorded with who did them. Check Server Logs → Audit trail periodically, or alert on [AUDIT] lines in your cloud's log service.
Replacing the activation key
Store the new key, then restart so the container reads it.
| Cloud | Commands |
|---|---|
| Azure | az containerapp secret set --name $APP_NAME --resource-group $RESOURCE_GROUP --secrets activation-key="<new-key>", then restart the active revision with az containerapp revision restart. |
| Google Cloud | printf '%s' "<new-key>" | gcloud secrets versions add apiklone-activation-key --data-file=-, then gcloud run services update $SERVICE --region=$REGION --update-secrets=ACTIVATION_KEY=apiklone-activation-key:latest. |
| AWS | aws secretsmanager put-secret-value --secret-id $APP/activation-key --secret-string "<new-key>", then aws ecs update-service --cluster $APP --service $APP --force-new-deployment. |
Self-Hosted Deployment (Docker / K8s)
For organizations requiring strict data-residency, APIKlone can be deployed inside your own perimeter using Docker. The deployment process is simple and entirely stateless (using a mounted data volume for your mock schemas).
Standard Docker Flow
Pull the latest image from our secure registry and start the container by injecting your Activation Key.
docker pull apiklone/apiklone:latest
docker run -d \
--name apiklone-server \
-e ACTIVATION_KEY="eyJhbGciOiJ..." \
-p 3000:3000 \
-v $(pwd)/data:/app/data \
apiklone/apiklone:latest
Always mount a persistent volume (e.g., /app/data) so that your recorded endpoints and uploaded OpenAPI specs survive container restarts!
Activation Keys
For Self-Hosted instances, you must authenticate your server instance using a cryptographic Activation Key. You can generate and view your active keys by clicking "Profile -> My Key" in the top navigation.
Activation keys contain your configs limit data. The server reads this token offline on boot to validate your license. Trials default to a 30-day expiry window with virtually infinite configurability.
Traffic Recording
APIKlone can serve as a man-in-the-middle to automatically scrape realistic payload schemas without manual effort.
- 1. Create a Config: Set the Target Server to your real backend URL.
- 2. Enable Recording: Toggle the "Recording Mode" switch on the dashboard.
- 3. Send Traffic: Point your frontend to the APIKlone proxy address. APIKlone transparently relays the request to the upstream server, intercepts the HTTP 200 JSON response, and saves it.
- 4. Switch to Mocking: Turn "Recording Mode" off. Future requests matching that URL path will immediately be served locally without hitting the real backend.
Mocks from an OpenAPI spec
Upload a Swagger 2 or OpenAPI 3 file, as JSON or YAML, on the Mock From Spec page or through the REST API. Every operation with a JSON success response becomes a live endpoint with a sample response built from its schema, replying with the status code the spec documents (for example 201 for a create).
Real calls are matched back to the spec's path templates, so a call to /products/42 finds the mock for /products/{id}, and query strings are ignored for matching. The spec stays attached to the config, which is what makes contract validation possible.
Contract validation
Turn on Strict OAS Validation for a config created from a spec, either when you upload it or later from the View Configs page. APIKlone then enforces the spec on every call:
- Requests are enforced, in every mode. A call that breaks the spec, such as a missing required field, a wrong type or a badly formatted ID, gets a 400 naming the field at fault and never reaches the mock or the backend.
- Responses are labelled, in stubbed-response mode. Each mock response is checked and marked in the X-APIKlone-OAS-Validation header as Passed or Failed. Responses are never blocked, so a stale mock does not stop a test.
- Every failure is logged in the server log with the reason.
HTTP/1.1 400 Bad Request
{
"error": "OAS Schema Validation Failed",
"details": [{
"path": "qty",
"errorCode": "required.openapi.requestValidation",
"message": "must have required property 'qty'",
"location": "body"
}]
}
Validation follows the spec exactly. A field missing from required is optional, and extra fields are allowed unless the schema sets additionalProperties: false.
Async callbacks
Some APIs reply at once (often 202 Accepted) and send the real result to the consumer later. Turn on Async API when editing an endpoint, and in stubbed-response mode APIKlone serves the acknowledgement and then sends the callback after the delay you set.
- Where to send it: a URL read from each request's body, a header or a query parameter, or a fixed URL.
- What to send: method, headers and a payload filled from the original request, for example {{request.body.orderId}}, {{uuid}} or {{now}}.
- When: up to 10 callbacks per endpoint, each with its own delay of up to 24 hours.
- Reliably: callbacks are queued in storage, survive restarts, are retried if the consumer is down, and are sent once even with several replicas.
The Send test callback button, or the matching API call, sends it immediately so you can check the receiving side. In record mode only the acknowledgement is recorded; the real backend sends its own callback.
Dynamic responses and delays
In the mock editor, tick Dynamic response to fill placeholders in a mock's headers and body from each request, and set Response delay to hold it back by up to 60 seconds.
{
"statusCode": 201,
"delayMs": 300,
"templating": true,
"headers": { "Content-Type": "application/json" },
"body": {
"orderId": "{{uuid}}",
"sku": "{{request.body.sku}}",
"qty": "{{request.body.qty}}",
"createdAt": "{{now}}"
}
}
Available placeholders: {{request.body.x}}, {{request.query.x}}, {{request.headers.x}}, {{request.params.x}} (path parameters of spec-imported endpoints), {{request.pathSegments.N}}, {{request.path}}, {{request.method}}, {{uuid}}, {{now}} and {{timestamp}}. A value that is only a placeholder keeps its type, so "qty": "{{request.body.qty}}" comes back as a number.
Polymorphic Responses
To recreate complex business logic, you can configure Polymorphic mapping. This allows you to evaluate specific query parameters, headers, or JSON body fields, and return entirely different HTTP status codes or payloads.
For example, if you send {"amount": 5000}, return an HTTP 200. If you send {"amount": -50}, return an HTTP 400 Validation Error.
Semantic Caching
Drastically reduce costs when interacting with external LLM APIs (like OpenAI or Anthropic). APIKlone can calculate vector embeddings of inbound prompts and intelligently cache semantically similar requests.
By enabling Semantic Cache in your active configuration, identical or closely related prompts will be served from the local cache rather than incurring round-trip latency and token costs from the upstream API provider.
AI Guard Rails
Ensure safe and reliable API behavior by enforcing our AI-powered proxy proxy layer. When enabled, APIKlone actively scans inbound payload payloads against known malicious patterns (e.g., prompt injection, PII exfiltration).
Administrators can configure sensitivity thresholds directly from the dashboard, instantly blocking non-compliant traffic before it reaches your backend systems.
Managing Admin Users
APIKlone comes with an integrated login portal for your engineering team to collaborate on creating mocks.
For Self-Hosted, user profile data and passwords are mathematically secured using PBKDF2 hashing algorithms inside your local DB. To add a user, administrators can invite them directly via the "Users" panel natively inside the dashboard application.
Headless REST API (CI/CD integration)
Everything in the dashboard can be driven from a pipeline. Create an API key on the API Keys page (one per pipeline, so each shows by name in the audit trail) and send it in the x-api-key header. Your instance documents every endpoint at API Docs in the dashboard, with a Swagger UI and the OpenAPI file to download.
A typical pipeline imports the spec with validation on, points the tests at it, and removes it afterwards:
# 1. Create mocks from the spec, with contract validation on
curl -X POST https://<your-apiklone>/api/mock-from-spec \
-H "x-api-key: $APIKLONE_KEY" \
-F "specFile=@openapi.yaml" \
-F "basePath=/orders" \
-F "validateOAS=true"
# -> {"success":true,"groupId":"1791617154089_group","configName":"OrdersAPI",...}
# 2. Run your tests against https://<your-apiklone>/orders/...
# 3. Remove the config when the run is over
curl -X DELETE https://<your-apiklone>/api/configs/1791617154089_group \
-H "x-api-key: $APIKLONE_KEY"
| Task | Endpoint |
|---|---|
| List configs | GET /api/configs |
| Create an endpoint, with an optional async callback | POST /api/configs |
| Switch record, passthrough or stubbed response | PUT /config/{id}/mode |
| Turn contract validation on or off | PUT /api/configs/group/{groupId}/validate |
| Add or replace a mock, with delay and placeholders | POST /config/{id}/mocks, PUT /mocks/{config}/{file} |
| Send a test callback | POST /config/{id}/async/test |
| Export, import, enable or disable a config | GET …/{groupId}/export, POST /api/configs/import, PUT …/{groupId}/enabled |