Background
Sections
IntroductionFoundations1. Resource Hierarchy2. Resource Manager3. Identity and RBAC4. Regions and Availability5. Naming and TaggingVirtual Machines1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetVirtual Network1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetBlob Storage1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure SQL Database1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Kubernetes Service1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Container Registry1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetMicrosoft Entra ID1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure RBAC1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Functions1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAPI Management1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure App Configuration1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Machine Learning1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Monitor1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure AI Foundry1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and Cheatsheet

4. Getting Started

7 min read

The smallest thing that proves the gateway works: one APIM instance, one API pointing at a public demo backend, one call through it with a subscription key — done three ways, then deleted.

This page is deliberately throwaway. Hard-coded names, no variables, no remote state, no pipeline. Everything production-shaped lives in Deployment.

Read this before you start. Creating a classic-tier APIM instance takes tens of minutes ⚠️ verify current figures — you will not be watching a progress bar for thirty seconds. Two ways to avoid a coffee break: use the Consumption tier (near-instant, but no developer portal and no VNet), or a v2 tier (minutes rather than tens of minutes). The examples below use Developer because it has the full feature set and the lowest classic price; substitute Consumption if you want speed and don't need the portal. Either way, start the create first and read the rest of this page while it runs.

Portal, CLI and Terraform creating one instance

What we're building

client ──(subscription key)──▶ apim-demo-<unique>.azure-api.net/demo/todos/1
                                        │
                                        └──▶ https://jsonplaceholder.typicode.com/todos/1

An API named demo whose backend is a public JSON test service, exposed as one GET operation, and one policy that proves the pipeline ran. No auth beyond the subscription key, because this is hello-world.

1. Azure Portal

The click-path, kept short because the portal's navigation changes often:

  • Create a resource → API Management → new resource group rg-apim-demo, a globally unique name, a region, your email as publisher, pricing tier Developer. Create, and leave it.
  • When it finishes, open the instance → APIs blade → HTTP (or "Add API → HTTP") → display name Demo, web service URL https://jsonplaceholder.typicode.com, API URL suffix demo.
  • On the new API → Add operation → display name Get todo, method GET, URL /todos/{id}.
  • Select that operation → Test tab → set id to 1Send. You should get a JSON todo back, with the subscription key filled in for you by the portal.
  • Optional, to see the pipeline: on the API's Design tab, open the policy editor (the </> icon on Outbound processing) and add <set-header name="x-demo" exists-action="override"> <value>hello</value></set-header> inside <outbound>, after <base />. Re-test and look at the response headers.

That last step is the one worth doing. Everything else you'll automate; the policy editor is where you'll spend your actual time.

2. Azure CLI (az)

Copy-pasteable. Pick a unique name first — the gateway hostname is global DNS.

# --- variables ---------------------------------------------------------------
RG=rg-apim-demo
LOC=uksouth
APIM=apim-demo-$RANDOM$RANDOM          # must be globally unique
EMAIL="you@example.com"

# --- throwaway resource group ------------------------------------------------
az group create -n $RG -l $LOC

# --- the instance (Developer tier; this is the slow part) --------------------
# Drop --no-wait if you'd rather block. Swap Developer for Consumption to go fast.
az apim create \
  --name $APIM \
  --resource-group $RG \
  --location $LOC \
  --publisher-name "Demo Co" \
  --publisher-email "$EMAIL" \
  --sku-name Developer \
  --no-wait

# --- poll until it's ready ---------------------------------------------------
az apim show -n $APIM -g $RG --query provisioningState -o tsv
# repeat until it prints: Succeeded

# --- one API ------------------------------------------------------------------
az apim api create \
  --resource-group $RG --service-name $APIM \
  --api-id demo --display-name "Demo" \
  --path demo --protocols https \
  --service-url https://jsonplaceholder.typicode.com

# --- one operation ------------------------------------------------------------
az apim api operation create \
  --resource-group $RG --service-name $APIM \
  --api-id demo --operation-id get-todo \
  --display-name "Get todo" --method GET \
  --url-template "/todos/{id}" \
  --template-parameters name=id required=true type=string

# --- a subscription key to call it with --------------------------------------
# The built-in all-APIs subscription is the quickest route for a demo.
KEY=$(az apim subscription list -g $RG -n $APIM \
      --query "[?scope contains 'apis'] | [0].primaryKey" -o tsv 2>/dev/null)

# If that comes back empty, list what exists and take a key from one of them:
az apim subscription list -g $RG -n $APIM -o table

# --- call it ------------------------------------------------------------------
curl -i "https://$APIM.azure-api.net/demo/todos/1" \
     -H "Ocp-Apim-Subscription-Key: $KEY"

Two things to notice in that output. Without the header you get 401 — that's the subscription check running before any policy of yours. With it you get the backend's JSON, plus APIM's own response headers. That 401 is the entire "subscription key" concept in one observation.

PowerShell note: APIM is not a service where a PowerShell-first shop is especially likely — the az CLI and Terraform dominate here, and serious configuration work uses the APIOps tooling in Deployment rather than either shell. The Az.ApiManagement module exists (New-AzApiManagement, New-AzApiManagementApi) if your organisation standardises on it.

3. Terraform (minimal)

The smallest possible version. The parameterised, production-shaped module is in Deployment — this one hard-codes everything on purpose.

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
}

resource "azurerm_resource_group" "demo" {
  name     = "rg-apim-demo"
  location = "uksouth"
}

resource "azurerm_api_management" "demo" {
  name                = "apim-demo-changeme-0001" # globally unique
  resource_group_name = azurerm_resource_group.demo.name
  location            = azurerm_resource_group.demo.location
  publisher_name      = "Demo Co"
  publisher_email     = "you@example.com"
  sku_name            = "Developer_1" # <tier>_<units>

  # Expect this resource to take tens of minutes. Raise the timeout, not your blood pressure.
  timeouts {
    create = "90m"
    update = "90m"
    delete = "90m"
  }
}

resource "azurerm_api_management_api" "demo" {
  name                = "demo"
  resource_group_name = azurerm_resource_group.demo.name
  api_management_name = azurerm_api_management.demo.name
  revision            = "1"
  display_name        = "Demo"
  path                = "demo"
  protocols           = ["https"]
  service_url         = "https://jsonplaceholder.typicode.com"
}

resource "azurerm_api_management_api_operation" "get_todo" {
  operation_id        = "get-todo"
  api_name            = azurerm_api_management_api.demo.name
  api_management_name = azurerm_api_management.demo.name
  resource_group_name = azurerm_resource_group.demo.name
  display_name        = "Get todo"
  method              = "GET"
  url_template        = "/todos/{id}"

  template_parameter {
    name     = "id"
    required = true
    type     = "string"
  }
}

# One policy, so you can see the pipeline do something.
resource "azurerm_api_management_api_policy" "demo" {
  api_name            = azurerm_api_management_api.demo.name
  api_management_name = azurerm_api_management.demo.name
  resource_group_name = azurerm_resource_group.demo.name

  xml_content = <<XML
<policies>
  <inbound><base /></inbound>
  <backend><base /></backend>
  <outbound>
    <base />
    <set-header name="x-demo" exists-action="override">
      <value>hello</value>
    </set-header>
  </outbound>
  <on-error><base /></on-error>
</policies>
XML
}

output "gateway_url" {
  value = azurerm_api_management.demo.gateway_url
}
terraform init
terraform plan
terraform apply

Note the sku_name formatDeveloper_1, Standard_2, Premium_3: tier and unit count in one string, which is where most first-time azurerm APIM errors come from. And note the explicit timeouts block: the provider's default create timeout is not always generous enough for a classic tier, and a timeout mid-create leaves you with a real instance that Terraform doesn't know about.

On v2 tiers in Terraform: sku_name values for the v2 tiers (StandardV2_1 and friends) are supported in recent azurerm versions ⚠️ verify current support and exact strings for your provider version; if a v2-only feature isn't covered, that's what the azapi provider is for — see Deployment.

What just happened

Whichever path you took, you created one ARM resource (the service) and then three child ARM resources (an API, an operation, a policy). That's the concept from Architecture made concrete: API configuration is infrastructure here. The Terraform state file contains your policy XML.

Try these three one-minute experiments before tearing it down — each one teaches something the docs take a page to say:

  1. Call without the key. 401. Subscription resolution happens before your policies.
  2. Call /demo/todos/notanumber. It goes through. APIM only validates what you tell it to — add validate-parameters or an OpenAPI schema and it stops.
  3. Add <rate-limit calls="3" renewal-period="60" /> to the API's inbound policy (after <base />) and call four times. The fourth returns 429. That's the whole traffic-control story in three lines.

Teardown

Delete the resource group — one command, and the cleanest teardown Azure gives you. There is no AWS equivalent of "delete this container and everything in it goes," and it's worth appreciating once.

az group delete -n rg-apim-demo --yes --no-wait
# Terraform equivalent
terraform destroy

One APIM-specific catch. The instance is soft-deleted, not gone: its globally unique name stays reserved for a retention period, so re-creating with the same name fails ⚠️ verify the current retention window. Purge it if you need the name back:

az apim deletedservice list -o table
az apim deletedservice purge --service-name <name> --location <location>

Put that in your notes now — it will otherwise cost you twenty confused minutes the first time a pipeline re-creates an instance.


Next: Deployment →

← Back to the Azure API Management overview · ← Previous: Architecture