unkey
  • BlogRead product updates and insights
  • Case studiesPractical results from real deployments
  • ChangelogTrack what shipped, improved, and fixed
  • GlossaryClear definitions for API terms
PricingDocs
DiscordGitHub5.3k
LoginSign Up
  • BlogRead product updates and insights
  • Case studiesPractical results from real deployments
  • ChangelogTrack what shipped, improved, and fixed
  • GlossaryClear definitions for API terms
PricingDocsDiscordGitHub5.3k
LoginSign Up
  1. Blog
  2. /Deploy an API with preview environments and request logs

Deploy an API with preview environments and request logs

A walkthrough of deploying an API on Unkey from GitHub, with preview environments and request logs.

Deploy an API with preview environments and request logs
Author:James Perkins
James Perkins
OCT 13, 2026
12 min
Explore with AI
Open in ChatGPTOpen in ClaudeOpen in Perplexity

When you ship a public API, you need somewhere to run it and a preview for each pull request. You also need to check who is calling, and you want logs for the day something breaks. We'll set all of that up on Unkey by connecting a GitHub repository. Every push deploys the app, Compute runs it, and API Management issues and checks the keys. Unkey also records each request the app serves.

What you'll have at the end

You get a production URL on unkey.app that updates when you push to your default branch, and a preview URL for every other branch and fork pull request. The gateway checks API keys and rate limits each caller, and you get logs and metrics without adding an SDK.

You need a Compute plan before the first deploy. The dashboard asks you to Choose a Compute plan, and only a workspace admin can pick one.

The sample API

The app is perkinsjr/Demo-for-deploy, a small Go API built with Gin. It listens on port 8080, which is the port Unkey uses by default, and serves five routes.

RouteWhat it returns
GET /health{"status":"healthy"} and a timestamp
GET /itemsEvery item and a count
GET /items/:idOne item, or 404 with {"error":"Item not found"}
POST /itemsThe item it created
GET /whoamiThe X-Unkey-Principal header as JSON, or {"principal":null}

Step 1: Connect the repository and deploy

You can connect and deploy from the dashboard, the API, or the CLI. All three get you the same app.

From the dashboard

  1. Open Projects, choose Create project, and enter a Project Name. Unkey generates the Slug from the name. You'll use that slug whenever you call the API or the CLI.

The Create New Project dialog with Project Name and Slug fields

  1. Choose Create app, then choose GitHub repository as the source. The first time you do this in a workspace, GitHub asks you to install the Unkey GitHub App and give it access to the repository.

Deploy a new app, with GitHub repository as the source

Then pick the repository.

The Import a GitHub repository step listing the account's repositories

  1. On Set up your app, enter a name and pick the branch. If the app needs a secret, such as a database URL, choose Edit next to environment variables and add it. If you mark it sensitive, it stays write-only. Use the environment dropdown to give Production and Preview separate values.

The Set up your app step: name api, branch main, automatic build, one instance in four regions

The Environment variables dialog scoped to the Preview environment

  1. Choose Deploy. The build log streams while the image builds.

A deployment building from main, with the build log open

The same deployment at Ready, with Show URLs open and one instance in each of four regions

From the API

Create a project, then create an app connected to your repository. Unkey reads defaultBranch from the repository, so you don't need to set it.

curl -X POST https://api.unkey.com/v2/projects.createProject \
  -H "Authorization: Bearer $UNKEY_ROOT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"my-project","slug":"my-project"}'
curl -X POST https://api.unkey.com/v2/apps.createApp \
  -H "Authorization: Bearer $UNKEY_ROOT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"project":"my-project","name":"api","slug":"api","git":{"repository":"owner/repo"}}'

Deploy the default branch to production.

curl -X POST https://api.unkey.com/v3/deployments.createDeployment \
  -H "Authorization: Bearer $UNKEY_ROOT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"project":"my-project","app":"api","environment":"production","git":{"branch":"main"}}'

The call returns 201 and a deploymentId. Poll deployments.getDeployment with that id until status is ready.

curl -X POST https://api.unkey.com/v2/deployments.getDeployment \
  -H "Authorization: Bearer $UNKEY_ROOT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"deploymentId":"d_xxx"}'

Once it's ready, the app is live at https://<project>-<app>-<workspace>.unkey.app.

From the CLI

Install the CLI and export your root key.

npm install -g unkey
export UNKEY_ROOT_KEY="<your root key>"

Run the same four calls with the CLI.

unkey api projects create-project --name=my-project --slug=my-project
unkey api apps create-app --project=my-project --name=api --slug=api \
  --git='{"repository":"owner/repo"}'
unkey api deployments create-deployment --project=my-project --app=api \
  --environment=production --git='{"branch":"main"}'
unkey api deployments get-deployment --deployment-id=d_xxx

Step 2: Every branch and pull request gets a preview

Once the repository is connected, every push to the default branch deploys production. A push to any other branch creates a preview with its own branch domain. Each one shows up in GitHub as a deployment. Unkey also keeps one comment on the pull request updated with a Visit Preview link.

The app's Deployments list filtered to Preview, showing the Add /whoami route preview

Open a preview deployment to see its branch, commit, and environment.

A preview deployment for Add /whoami route, with its branch, commit, and Preview environment

A pull request from a fork waits for a project member to approve it, because its code would run with your preview secrets.

Give the preview environment its own values. Point it at a test database and test-mode payment keys. You can also give it a separate keyspace, so a preview key can't call production.

Unkey stops previews in these cases, so you don't have to clean them up yourself.

TriggerWhat stopsWhen
New preview deployment on the same branchThe deployment that was on that branch domain1 minute later
Preview deployment gets no requestsThat deploymentAfter 1 hour idle
You stop it (Stop deployment or deployments.stopDeployment)That deploymentRight away

Wake a stopped preview with Wake deployment or deployments.startDeployment.

Step 3: Require an API key and rate limit each customer

Every Compute app sits behind the gateway. Policies run on the gateway, so it checks each request before the request reaches your code. Start by creating a keyspace in API Management. In the API, a keyspace is called an API.

curl -X POST https://api.unkey.com/v2/apis.createApi \
  -H "Authorization: Bearer $UNKEY_ROOT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"my-api"}'

Save the apiId from the response. You need it every time you create a key.

Now create a key for a customer and attach a rate limit to it. externalId links the key to an identity. With autoApply set, every request made with this key counts against the limit.

curl -X POST https://api.unkey.com/v2/keys.createKey \
  -H "Authorization: Bearer $UNKEY_ROOT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"apiId":"api_xxx","name":"acme blog demo","externalId":"acme","ratelimits":[{"name":"requests","limit":5,"duration":60000,"autoApply":true}]}'

That's 5 requests per minute. It's low enough that you can watch it trip. In production, start closer to 100.

data.key is the full key, and you only see it in this response. Unkey stores just a hash, so hand the key to the customer now.

Next, tell the gateway to check keys. Open the app's Policies and choose Add policy.

  1. Give the policy a name and set Type to Key Auth. The rule reads IF Every request THEN Key Auth. Under Keyspaces, pick the keyspace you just created.

The Add policy form: name Require an API key, Type Key Auth, rule IF Every request THEN Key Auth, with a keyspace selected

  1. Set Environment to Production. ON FAIL shows what callers get back when the check fails, which is 401, 403, or 429.

The rest of the Add policy form, with ON FAIL codes 401, 403, and 429 and Environment set to Production

  1. Choose Add policy.

The Policies list with Require an API key, a Key Auth policy that is on for Production

You don't need a separate rate limit policy. Once the gateway verifies the key, it also enforces the limit you attached to that key.

Policies apply on the next deployment, so redeploy production to turn this one on.

If you'd rather use the API or CLI, you can set the same policy with one command. Both need the keyspace id (ks_xxx), which is on the keyspace Settings page.

unkey api gateway set-policies --project=my-project --app=api --environment=production \
  --policies='[{"name":"Require an API key","enabled":true,"keyauth":{"keyspaces":["ks_xxx"]}}]'
curl -X POST https://api.unkey.com/v2/gateway.setPolicies \
  -H "Authorization: Bearer $UNKEY_ROOT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"project":"my-project","app":"api","environment":"production","policies":[{"name":"Require an API key","enabled":true,"keyauth":{"keyspaces":["ks_xxx"]}}]}'

setPolicies replaces the environment's whole policy list, so include every policy you want to keep. The root key needs environment.*.set_policies.

Once the new deployment is live, call the app without a key.

curl -i https://your-app.unkey.app/items

The gateway answers 401, and the request never reaches your code.

{
  "error": {
    "code": "err:frontline:client:missing_credentials",
    "message": "Authentication required. Please provide a valid API key."
  }
}

Now call /whoami with the key.

curl -i -H "Authorization: Bearer $API_KEY" https://your-app.unkey.app/whoami

The response headers show the caller's limit and how many requests they have left.

HTTP/2 200
x-ratelimit-limit: 5
x-ratelimit-remaining: 4

The body is the X-Unkey-Principal header that the gateway added. It tells your code who is calling.

{
  "principal": {
    "version": "v1",
    "subject": "acme",
    "type": "API_KEY",
    "identity": {
      "externalId": "acme",
      "meta": {}
    },
    "source": {
      "key": {
        "keyId": "key_xxx",
        "keySpaceId": "ks_xxx",
        "name": "acme blog demo",
        "meta": {}
      }
    }
  }
}

subject is the identity's externalId, or the key id when the key has no identity. The gateway strips any X-Unkey-* header the client sends. Compute deployments are only reachable through the gateway, so inside Compute you can trust that Unkey set the header. If you run the same code somewhere else, a caller can forge it, so don't trust it there.

Once a caller goes over the limit, requests return 429 with a retry-after header that gives the wait in seconds.

HTTP/2 429
retry-after: 53
x-ratelimit-limit: 5
x-ratelimit-remaining: 0
{
  "error": {
    "code": "err:frontline:client:rate_limited",
    "message": "Rate limit exceeded. Please try again later."
  }
}

Preview has its own policy list, so your production policies don't cover it. If a preview shouldn't be public, set policies for it too, or choose All Environments in the dashboard.

Step 4: Find and fix a bug

Say a change to the item lookup ships to production, and customers' GET /items/2 calls start failing.

curl -i -H "Authorization: Bearer $API_KEY" https://your-app.unkey.app/items/2
{ "error": "internal server error" }
  1. Open Requests for the project and filter Status to 5xx. Every failure is a GET /items/2 that returned 500. Open one to see the deployment, branch, and commit that served it.

Requests filtered to 5xx: three GET /items/2 calls returned 500, with one open to show the production deployment, branch, and commit that served it

  1. Open Logs and search for index out of range. The app logged error: get item 2: index out of range on each failed call.

Logs searched for index out of range: three ERROR lines, with error: get item 2: index out of range open

  1. Push the fix to a branch. Unkey builds a preview for it. Call the same path on the preview's branch domain with your key.
curl -i -H "Authorization: Bearer $API_KEY" https://your-app-git-your-branch.unkey.app/items/2
{
  "id": "2",
  "name": "Sample Item 2",
  "description": "Another fake item for testing",
  "created_at": "2026-10-16T14:57:30.985616522Z"
}

The app's Deployments list: the Fix item lookup preview at Ready, above the current production deployment, Look up items by index

  1. Merge the branch. The push to your default branch deploys the fix to production.

The gateway rejects some requests, like ones with a missing key or ones over the rate limit. Those never reach your app, so they don't show up in Requests. You can find those key checks in API Management's verification analytics.

To query the same data with SQL, call analytics.getGatewayRequests. The root key needs project.*.read_gateway_requests. Queries cover the last 7 days.

curl -X POST https://api.unkey.com/v2/analytics.getGatewayRequests \
  -H "Authorization: Bearer $UNKEY_ROOT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"SELECT path, count() AS requests FROM gateway_requests_v1 WHERE response_status >= 500 AND time >= toUnixTimestamp64Milli(now64(3) - INTERVAL 1 HOUR) GROUP BY path ORDER BY requests DESC LIMIT 10"}'

How this compares with GitHub Actions plus Cloud Run, ECS or Kubernetes

The other setups leave more of this to you. Each provider column links to that provider's docs.

StepUnkeyGitHub Actions + Cloud RunGitHub Actions + ECSGitHub Actions + Kubernetes
Deploy on pushConnect the repo and it's built inWorkflow runs gcloud run deployWorkflow registers a task definition and updates the serviceWorkflow applies manifests or sets the image
Preview per branch or PRBuilt-in, with a branch domain and fork approvalWorkflow deploys a revision with --no-traffic --tag to get a tagged URL (docs)Not covered in the linked docs. You script a service per PRNot covered in the linked docs. You script a namespace or release per PR
Clean up previewsAuto-stop after 1 hour idle, or 1 minute after a newer preview on the branchYou remove tags or revisions yourselfYour workflow tears down what it createdYour workflow tears down what it created
API key auth and per-key rate limitsGateway policies backed by API Management keysA separate gateway or code in your appA separate gateway or code in your appAn ingress or gateway add-on, or code in your app
Request logsBuilt-in, plus SQL via the analytics APICloud LoggingCloudWatch, if you configure itWhatever logging stack you run

Render's service previews are the closest built-in match for the preview row. You choose Manual or Automatic pull request previews, and Render deletes a preview when its pull request is merged or closed. Unkey also puts a gateway in front of production and previews, and the request log is queryable.

Cost

API Management and Compute are billed separately. You can use either one without the other. Prices below are from unkey.com/pricing.

Compute has three plans. Each one includes a monthly usage credit equal to the fee. Until December 31, 2026, the first month is discounted. Starter costs $0 the first month and $5 after that, Pro costs $20 and then $25, and Business costs $45 and then $50. On Starter, credit left over from that first month carries to the next bill. Unused credits otherwise do not roll over.

StarterProBusiness
Monthly fee$5$25$50
First month$0$20$45
Usage credit each month$5$25$50
Max per instance2 vCPU, 2 GiB8 vCPU, 8 GiB16 vCPU, 32 GiB
Instances per region4816
Custom domains1UnlimitedUnlimited
Log retention3 days7 days14 days
Team membersNoneUnlimitedUnlimited

Every plan allows one concurrent build, git push to deploy, and a preview for each pull request. Preview usage is billed at the same rates as production.

Past the included credit, you pay for the vCPU and memory your app actually uses, the disk you allocate, egress, and active keys checked by a Compute key auth policy. The rates below are rounded.

MeterRate
vCPUabout $0.025 per hour
Memoryabout $9 per GB-month
Diskabout $0.16 per GB-month
Egress$0.05 per GB
Active keys$2 per 1,000 per month

If the default instance (0.25 vCPU, 256 MiB) ran flat out all month, you'd pay about $4.50 for CPU and $2.25 for memory, so about $6.75, and the Starter credit covers most of that. Treat that as the most it could cost. A real API almost never keeps CPU and memory at 100 percent around the clock, so your bill will land well under it. You pay for the CPU and memory your app actually uses, not what you configured. Memory your app holds is billed even between requests. CPU used while your app is idle or waiting on I/O is usually tiny, so it costs very little.

Spending Caps

You can cap the bill with a monthly budget.

  • Set it in whole dollars under Settings > Billing. Only a workspace admin can change it.
  • Admins get an email the first time month-to-date spend crosses 50, 75, and 100 percent.
  • Stop workloads at the budget is off until you turn it on. At 100 percent, running deployments stop and every request gets 402 spend_limit_reached.
  • Remove the budget, turn the stop option off, or wait for the next month, and the live deployment comes back without a redeploy. Raising the budget resumes compute once spend has stayed under the new budget for two checks in a row.
  • Nothing is deleted.

Further reading

  • Deploy from GitHub and Production and preview
  • Gateway policies, API key authentication policy, The principal header
  • Observability and Query gateway requests and runtime logs
  • Plans and pricing
  • Related posts: Best serverless API hosting in 2026, git push to deploy, From endpoint to API

On this page

  1. What you'll have at the end
  2. The sample API
  3. Step 1: Connect the repository and deploy
  4. From the dashboard
  5. From the API
  6. From the CLI
  7. Step 2: Every branch and pull request gets a preview
  8. Step 3: Require an API key and rate limit each customer
  9. Step 4: Find and fix a bug
  10. How this compares with GitHub Actions plus Cloud Run, ECS or Kubernetes
  11. Cost
  12. Spending Caps
  13. Further reading

Turn your API stack into one workflow. Start for free, integrate in minutes, and scale when you need to.

Start for free
unkey

Build better APIs faster

© 2026 Unkey Inc. All rights reserved.

SOC 2 Type II Certified

Company

  • About
  • Careers
  • Source Code
  • Status Page
  • Roadmap

Resources

  • Blog
  • Case Studies
  • Changelog
  • Docs
  • Glossary
  • Feature Request

Connect

  • X (Twitter)
  • Discord
  • Book a Call

Legal

  • Terms of Service
  • Privacy Policy
  • Trust Center