Skip to content

Latest commit

 

History

History
379 lines (267 loc) · 69.6 KB

API Management.md

File metadata and controls

379 lines (267 loc) · 69.6 KB
  • Consumption Tier: No Entra ID integration, private endpoint support for inbound connections, developer portal, built-in cache, built-in analytics, backup and restore, management over Git, direct management API, Azure Monitor and Log Analytics request logs, Static IP.
  • Developer Tier: Like Premium, without multi-region deployment and availability zones.
  • Basic Tier: No Entra ID integration, and workspaces.
  • Standard Tier: Has Entra ID Integration and worspaces.
  • Premium Tier: Has VNET, multiple custom domain names, self-hosted gateway

Components

  • API Gateway: Endpoint that routes API calls, verifies credentials, enforces quotas and limits, transforms requests/responses specified in policy statements, caches responses, and produces logs/metrics for monitoring.
  • Management Plane: Administrative interface for service settings, define and import API schema, packaging APIs into products, policy setup, analytics, and user management.
  • Developer Portal: Auto-generated website for API documentation that enables developers to access API details, use interactive consoles, create an account and subscribe for API keys, analyze usage, download API definitions, and manage API keys.

Products

Products bundle APIs for developers. They have a title, description, and usage terms. They can be Open (usable without subscription) or Protected (requires subscription). Subscription approval is either auto-approved or needs admin approval.

Groups

Groups control product visibility to developers. API Management has three unchangeable system groups:

  • Administrators: Manage service instances, APIs, operations, and products. Azure subscription admins are in this group.
  • Developers: Authenticated portal users can build apps using your APIs. They access the Developer portal to use API operations and can be created by admins, invited, or self-registered. They belong to multiple groups and can subscribe to group-visible products.
  • Guests: Unauthenticated portal users with potential read-only access, such as viewing APIs but not calling them.

Apart from system groups, admins can form custom groups or use external groups from related Microsoft Entra ID tenants.

API gateways, such as Azure's API Management gateway, manage communication between clients and multiple front- and back-end services, which eliminates the need for clients to know specific endpoints (think of gateways as reverse proxy). These gateways streamline service integration, particularly when services are updated or introduced. They also take care of tasks like SSL termination, authentication, and rate limiting. Azure's API Management gateway specifically proxies API requests, enforces policies, and gathers telemetry data.

  • Managed gateways are default components deployed in Azure for each API Management instance. They handle all API traffic, regardless of where the APIs are hosted.
  • Self-hosted gateways are optional, containerized versions of managed gateways. They are suited for hybrid and multicloud environments, allowing management of on-premises APIs and APIs across multiple clouds from a single Azure API Management service.

A set of statements executed sequentially on an API's request or response. They can be applied at different scopes:

  • Global Scope: Apply organization-wide policies that affect every API and operation. Ideal for enforcing security measures like IP filtering or logging across all APIs.
  • Workspace Scope: Target APIs within a specific workspace. Best for internal organization, separating APIs based on teams or departments.
  • Product Scope: Group multiple APIs under a single product to simplify the subscription process. Best for external organization, grouping APIs based on functionality or business logic.
  • API Scope: Apply policies that affect all operations within a specific API. Useful for API-specific transformations or validations.
  • Operation Scope: Fine-grained control over individual API operations. Ideal for operation-specific validations or transformations.

Types of policies

  • Inbound: Applied before routing to the backend. Examples: validation, authentication, rate limiting, request transformation.
  • Backend: Applied before the request reaches the backend. Examples: URL rewriting, setting headers.
  • Outbound: Applied to the response before sent to the client. Examples: response transformation, caching, adding headers.

Note: If client expects response in certain format (example: XML), check question to see what kind of endpoint is used (example: JSON). If they are different, transform policy should be applied (example: json-to-xml-policy)

Policy Configuration

<base />: execute the default policies that are defined at other scopes (e.g., the Product or Global scope). Provides the ability to enforce policy evaluation order.

<policies>
  <inbound>
    <!-- statements to be applied to the request go here -->
  </inbound>
  <backend>
    <!-- statements to be applied before the request is forwarded to
         the backend service go here -->
  </backend>
  <outbound>
    <!-- statements to be applied to the response go here -->
  </outbound>
  <on-error>
    <!-- statements to be applied if there is an error condition go here -->
  </on-error>
</policies>

All times are in seconds!
All sizes are in KB!

Add a named value: Dashboard > API Management Services > service > Named values

Types:

  • Plain: Literal string or policy expression
  • Secret: Literal string or policy expression that is encrypted by API Management
  • Key vault: Identifier of a secret stored in an Azure key vault. After update in the key vault, a named value in API Management is updated within 4 hours. Requires managed identity. Configure either a key vault access policy or Azure RBAC access for an API Management managed identity. Key Vault Firewall requires system-assigned managed identity.

Add a secret:

az apim nv create --resource-group $resourceGroup \
    --display-name "named_value_01" --named-value-id named_value_01 \
    --secret true --service-name apim-hello-world --value test

To use a named value in a policy, place its display name inside a double pair of braces like {{ContosoHeader}}. If the value is an expression, it will be evaluated. If the value is the name of another named value - not.

Policy Expressions

Can be used as attribute values or text values in any of the API Management policies. They can be a single C# statement enclosed in @(expression) or a multi-statement C# code block, enclosed in @{expression}, that returns a value.

Example set-body:

<set-body>
  @{
    var response = context.Response.Body.As<JObject>();
    foreach (var key in new [] {"minutely", "hourly", "daily", "flags"}) {
    response.Property (key).Remove ();
    }
  return response.ToString();
  }
</set-body>

Use rate-limit-by-key (limit number of requests) or quota-by-key (limit bandwidth and/or number of requests). Renewal period is in seconds, bandwidth size is in KB. Use counter-key to specify identity or IP.

When quota is exceeded, a 403 Forbidden status is returned.

  • Throttle by IP: counter-key="@(context.Request.IpAddress)"
  • Throttle by Identity: counter-key="@(context.Request.Headers.GetValueOrDefault("Authorization","").AsJwt()?.Subject)"

Azure APIM has built-in support for HTTP response caching using the resource URL as the key. You can modify the key using request headers with the vary-by properties.

  • Get from cache (cache-lookup): inbound
  • Store to cache (cache-store): outbound
  • Others are mixed

Examples

Cache by header
https://myapi.azure-api.net/me
Authorization: Bearer <access_token>

Endpoint is not unique, but the authorization header is for each user.

<cache-lookup>
    <vary-by-header>Authorization</vary-by-header>
</cache-lookup>
Cache by query parameter
https://myapi.azure-api.net/samples?topic=apim&section=caching

Endpoint has two query parameters: topic and section. Use semicolon to separate.

<cache-lookup>
    <vary-by-query-parameter>topic;section</vary-by-query-parameter>
</cache-lookup>
Cache by url
https://myapi.azure-api.net/items/123456

Endpoint has no parameters, but the url is unique. This time use empty string.

<cache-lookup>
    <vary-by-query-parameter></vary-by-query-parameter>
</cache-lookup>
Fragment caching

When you want to add some information from external system to the current response, without fetching it every time. For example /me/tasks returns user's todos and profile, but the profile is stored at /userprofile/{userid}. To avoid fetching profile every time, the following rules must be implemented:

<!-- Extract userId from JWT -->
<set-variable
  name="enduserid"
  value="@(context.Request.Headers.GetValueOrDefault("Authorization","").Split(' ')[1].AsJwt()?.Subject)" />

<!-- Data is supposed to be stored in userprofile (for example) -->
<!-- If userprofile is not cached yet, send a request and store the response in cache -->
<choose>
    <when condition="@(!context.Variables.ContainsKey("userprofile"))">
        <!-- Make an HTTP request to /userprofile/{userid} in order to retrieve it  -->
        <send-request params>options</send-request>

        <!-- Store to cache -->
        <cache-store-value
          key="@("userprofile-" + context.Variables["enduserid"])"
          value="@(((IResponse)context.Variables["userprofileresponse"]).Body.As<string>())" duration="100000" />
    </when>
</choose>

<!-- Use userprofile from cache -->
<cache-lookup-value
  key="@("userprofile-" + context.Variables["enduserid"])"
  variable-name="userprofile" />

API Security

  1. Register an application in Entra ID to represent the API
  2. Configure a JWT validation policy to pre-authorize requests (validate-jwt)

Use the authentication-managed-identity policy to authenticate with a service through managed identity. It gets an access token from Microsoft Entra ID and sets it in the Authorization header using the Bearer scheme. The token is cached until it expires. If no client-id is given, the system-assigned identity is used.

Example: <authentication-managed-identity resource="resource" client-id="clientid of user-assigned identity" output-token-variable-name="token-variable" ignore-error="true|false"/>, where resource could be https://graph.microsoft.com, https://management.azure.com/, etc.

Use cases:

  • Obtain a custom TLS/SSL certificate for the API Management instance from Azure Key Vault
  • Store and manage named values from Azure Key Vault
  • Authenticate to a backend by using a user-assigned identity
  • Log events to an event hub

Via Subscriptions

API Management lets you secure APIs using subscription keys. Developers have to include these keys in HTTP requests when accessing APIs. If not, API Management gateway rejects the requests. The subscription keys come from subscriptions, which developers can get without needing permission from API publishers. Apart from this, OAuth2.0, Client certificates, and IP allow listing are other security methods.

A subscription key is a unique, automatically generated key included in client request headers or as a query string parameter. It's tied to a subscription which can have different scopes providing varied access levels. Subscriptions let you control permissions and policies minutely.

Three main subscription scopes:

  • All APIs: Grants access to all APIs configured in the service.
  • Single API: Access control limited to a specific API and its endpoints.
  • Product: Applies to a particular product (a collection of APIs) in API Management, each with different access rules, usage quotas, and terms of use.

Keys need to be included in every request to a protected API. They can be regenerated if needed, such as if a key is leaked. Subscriptions have a primary and a secondary key to aid in regeneration without downtime. In products with enabled subscriptions, clients must provide a key when calling APIs. They can get a key via a subscription request.

Calling API with Subscription Key

API calls need a valid key in HTTP requests. This can be passed in the request header or as a query string. The default header name is Ocp-Apim-Subscription-Key, and the default query string is subscription-key. APIs can be tested using the developer portal or command-line tools like curl. Here are example curl commands using a header and a URL query string:

curl --header "Ocp-Apim-Subscription-Key: <key string>" https://<apim gateway>.azure-api.net/api/path
curl https://<apim gateway>.azure-api.net/api/path?subscription-key=<key string>

Failure to pass the key results in a 401 Access Denied response.

  • authentication-certificate: Used by APIM to authenticate itself to the backend service.
  • validate-client-certificate (inbound): Used by APIM to validate the client's certificate connecting to the APIM.

Configure access to key vault:

  • Access policy: Access Policies > + Create > Secret permissions > Permissions tab > Get and List ; Principal tab > principal > managed identity > Next > Review + create > Create
  • RBAC access: Access control (IAM) > Add role assignment > Role tab > Key Vault Secrets User ; Members tab > Managed identity > + Select members > identity

TLS Client Authentication

API gateways inspect client certificates for specific attributes, such as Certificate Authority (CA), Thumbprint, Subject, and Expiration Date. These can be combined for custom policies.

Certificates are signed to avoid tampering. Validate received certificates to ensure they're authentic. Trusted CAs or physically delivered self-signed certificates are ways to confirm authenticity.

In the Consumption tier client certificates must be manually enabled on the Custom domains page.

Check the thumbprint against certificates uploaded to API Management

Use <when condition="$(...)"> policy with <return-response> and <set-status code="403" reason="Invalid client certificate" />:

// Client certificate
context.Request.Certificate == null || context.Request.Certificate.Thumbprint != "desired-thumbprint"

// Certificates uploaded to API Management
context.Request.Certificate == null || !context.Request.Certificate.Verify() || !context.Deployment.Certificates.Any(c => c.Value.Thumbprint == context.Request.Certificate.Thumbprint)

// Check the issuer and subject of a client certificate
context.Request.Certificate == null || context.Request.Certificate.Issuer != "trusted-issuer" || context.Request.Certificate.SubjectName.Name != "expected-subject-name"
  • Azure API Management uses a ProxyError object, accessed via context.LastError, for handling errors during request processing.
  • Policies are divided into inbound, backend, outbound, and on-error sections. Processing jumps to the on-error section if an error occurs.
  • If there's no on-error section, callers receive 400 or 500 HTTP response messages during an error.
  • Predefined errors exist for built-in steps and policies, each with a source, condition, reason, and message.
  • Custom behavior, like logging errors or creating new responses, can be configured in the on-error section.

Versions and Revisions

  • Use Revisions for non-breaking changes, allowing for testing and updates without affecting existing users. Users can access different revisions by using a different query string at the same endpoint.
  • Use Versions for breaking changes, requiring publishing and potentially requiring users to update their applications.

Versioning schemes:

  • Path-based versioning: https://apis.contoso.com/products/v1 and https://apis.contoso.com/products/v2
  • Header-based versioning: For example, custom header named Api-Version, and clients specify v1 or v2
  • Query string-based versioning: https://apis.contoso.com/products?api-version=v1 and https://apis.contoso.com/products?api-version=v2

Header-based versioning if the URL has to stay the same. Revisions and other types of versioning schemas require modified URL.

Creating separate gateways or web APIs would force users to access a different endpoint. A separate gateway provides complete isolation.

az apim api release create --resource-group $resourceGroup \
    --api-id demo-conference-api --api-revision 2 --service-name apim-hello-world \
    --notes 'Testing revisions. Added new "test" operation.'

az group deployment create --resource-group $resourceGroup --template-file ./apis.json --parameters apiRevision="20191206" apiVersion="v1" serviceName=<serviceName> apiVersionSetName=<versionSetName> apiName=<apiName> apiDisplayName=<displayName>

Create OpenAPI documentation for the backend API, then import it into APIM. This enables integration with APIM and allows for automatic discovery of all endpoints. APIM becomes a facade for the backend API, providing customization without altering the backend API itself.

Azure API Management emits metrics every minute, providing near real-time visibility into the state and health of your APIs. The most frequently used metrics are 'Capacity' and 'Requests'. 'Capacity' helps you make decisions about upgrading/downgrading your API Management services, while 'Requests' helps you analyze API traffic.

Working with API Management instance

Create new APIM:

az apim create --name MyAPIMInstance --resource-group $resourceGroup --location eastus --publisher-name "My Publisher" --publisher-email publisher@example.com --sku-name Developer
# or
New-AzApiManagement -ResourceGroupName RESOURCE_GROUP -Name NAME -Location LOCATION -Organization ORGANIZATION -AdminEmail ADMIN_EMAIL [-Sku SKU_NAME] [-Tags TAGS]
Name Example Notes Sections
Check HTTP header <check-header name="header name" failed-check-httpcode="code" failed-check-error-message="message" ignore-case="true | false">
  <value>Value1</value>
  <value>Value2</value>
</check-header>
When multiple value elements are specified, the check is considered a success if any one of the values is a match. inbound
Get authorization context <get-authorization-context
  provider-id="authorization provider id"
  authorization-id="authorization id"
  context-variable-name="variable name"
  identity-type="managed | jwt"
  identity="JWT bearer token"
  ignore-error="true | false" />
context-variable-name is the name of the context variable to receive the Authorization object ({accessToken: string, claims: Record<string, object>}). Configure identity-type=jwt when the access policy for the authorization is assigned to a service principal. Only /.default app-only scopes are supported for the JWT. inbound
Restrict caller IPs <ip-filter action="allow | forbid">
  <address>address</address>
  <address-range from="address" to="address" />
</ip-filter>
At least one address or address-range element is required. inbound
Validate Microsoft Entra ID token Simple token validation: <validate-azure-ad-token tenant-id="{{aad-tenant-id}}">
  <client-application-ids>
    <application-id>{{aad-client-application-id}}</application-id>
  </client-application-ids>
</validate-azure-ad-token>
Validate that audience and claim are correct: <validate-azure-ad-token tenant-id="{{aad-tenant-id}}" output-token-variable-name="jwt">
  <client-application-ids>
    <application-id>{{aad-client-application-id}}</application-id>
  </client-application-ids>
  <audiences>
    <audience>@(context.Request.OriginalUrl.Host)</audience>
  </audiences>
  <required-claims>
    <claim name="ctry" match="any">
      <value>US</value>
    </claim>
  </required-claims>
</validate-azure-ad-token>
To validate a JWT that was provided by another identity provider, use the generic validate-jwt. You can secure the whole API with Entra ID authentication by applying the policy on the API level, or you can apply it on the API operation level and use claims for more granular control. inbound
Validate client certificate <validate-client-certificate
  validate-revocation="true | false"
  validate-trust="true | false"
  validate-not-before="true | false"
  validate-not-after="true | false"
  ignore-error="true | false">
  <identities>
    <identity 
      thumbprint="certificate thumbprint"
      serial-number="certificate serial number"
      common-name="certificate common name"
      subject="certificate subject string"
      dns-name="certificate DNS name"
      issuer-subject="certificate issuer"
      issuer-thumbprint="certificate issuer thumbprint"
      issuer-certificate-id="certificate identifier" />
  </identities>
</validate-client-certificate>
identities is not required inbound
Control flow <choose>
  <when condition="Boolean expression | Boolean constant">
    <!— one or more policy statements to be applied if the above condition is true -->
  </when>
  <when condition="Boolean expression | Boolean constant">
    <!— one or more policy statements to be applied if the above condition is true -->
  </when>
  <otherwise>
    <!— one or more policy statements to be applied if none of the above conditions are true -->
  </otherwise>
</choose>
The choose policy must contain at least one <when/> element. The <otherwise/> element is optional. Any
Limit concurrency <limit-concurrency key="expression" max-count="number">
  <!— nested policy statements -->
</limit-concurrency>
Prevents enclosed policies from executing by more than the specified number of requests at any time. When that number is exceeded, new requests will fail immediately with the 429 Too Many Requests status. Example key: key="@((string)context.Variables["connectionId"])" code. Any
Rate limit <rate-limit-by-key calls="number"
  counter-key="key value"
  renewal-period="seconds"
/>
Renewal period is in seconds inbound
Quota <quota-by-key counter-key="key value" bandwidth="kilobytes" renewal-period="seconds" /> Bandwidth is in KB, renewal period is in seconds inbound
Emit custom metrics <emit-metric name="name of custom metric" value="value of custom metric" namespace="metric namespace">
  <dimension name="dimension name" value="dimension value" />
</emit-metric>
Sends custom metrics in the specified format to Application Insights. You can configure at most 10 custom dimensions for this policy. Counts toward the usage limits for custom metrics per region in a subscription. Any
Forward request <forward-request http-version="1 | 2or1 | 2" timeout="time in seconds" continue-timeout="time in seconds" follow-redirects="false | true" buffer-request-body="false | true" buffer-response="true | false" fail-on-error-status-code="false | true"/> By default, this policy is set at the global scope. backend
Log to event hub <log-to-eventhub logger-id="id of the logger entity" partition-id="index of the partition where messages are sent" partition-key="value used for partition assignment">
Expression returning a string to be logged
</log-to-eventhub>
The policy is not affected by Application Insights sampling. All invocations of the policy will be logged. Max message size: 200 KB (otherwise truncated). Any
Mock response <mock-response status-code="code" content-type="media type"/> Cancels normal pipeline execution. It prioritizes response content examples, using schemas when available and generating sample responses (or no content is returned). Policy expressions can't be used in attribute values for this policy. inbound, outbound, on-error
Retry <retry
  condition="Boolean expression or literal"
  count="number of retry attempts"
  interval="retry interval in seconds"
  max-interval="maximum retry interval in seconds"
  delta="retry interval delta in seconds"
  first-fast-retry="boolean expression or literal">
  <!-- One or more child policies. No restrictions. -->
</retry>
Executes its child policies once and then retries their execution until the retry condition becomes false or retry count is exhausted. When only the interval and delta are specified, the wait time between retries increases: interval + (count - 1)*delta. Any
Return response <return-response response-variable-name="existing context variable">
  <set-status>...</set-status>
  <set-header>...</set-header>
  <set-body>...</set-body>
</return-response>
Pipeline cancelation, body removal, custom or default response return to caller. Context variable and policy statements modify response if both provided. Any
Send request <send-request mode="new | copy" response-variable-name="" timeout="seconds" ignore-error="false | true">
  <set-url>request URL</set-url>
  <set-method>...</set-method>
  <set-header>...</set-header>
  <set-body>...</set-body>
  <authentication-certificate thumbprint="thumbprint" />
  <proxy>...</proxy>
</send-request>
Default timeout: 60sec Any
Set HTTP proxy <proxy url="http://hostname-or-ip:port" username="username" password="password" /> Only HTTP is supported between the gateway and the proxy. Basic and NTLM authentication only. username and password are not required. inbound
Set request method <set-method>HTTP method</set-method> Policy expressions are allowed. inbound, on-error
Set Status Code <set-status code="HTTP status code" reason="description"/> - Any
Set Variable <set-variable name="variable name" value="Expression | String literal" />
<set-variable name="IsMobile" value="@(context.Request.Headers.GetValueOrDefault("User-Agent","").Contains("iPad") || context.Request.Headers.GetValueOrDefault("User-Agent","").Contains("iPhone"))" />
If the expression contains a literal it will be converted to a string Any
Authenticate with Basic <authentication-basic username="username" password="password" /> Sets the HTTP Authorization header. Recommended using named values to provide credentials, with secrets protected in a key vault. inbound
Authenticate with managed identity <authentication-managed-identity resource="resource" client-id="clientid of user-assigned identity" output-token-variable-name="token-variable" ignore-error="true|false"/>
<authentication-managed-identity resource="AD_application_id" output-token-variable-name="msi-access-token" ignore-error="false" />
<!--Application (client) ID of your own Entra ID Application-->
<set-header name="Authorization" exists-action="override">
  <value>@("Bearer " + (string)context.Variables["msi-access-token"])</value>
</set-header>
After successfully obtaining the token, the policy will set the value of the token in the Authorization header using the Bearer scheme. Both system-assigned identity and any of the multiple user-assigned identities can be used to request a token. inbound
Get from cache <cache-lookup vary-by-developer="true | false" vary-by-developer-groups="true | false" caching-type="prefer-external | external | internal" downstream-caching-type="none | private | public" must-revalidate="true | false" allow-private-response-caching="@(expression to evaluate)">
  <vary-by-header>Accept</vary-by-header>
  <vary-by-header>Accept-Charset</vary-by-header>
  <vary-by-header>Authorization</vary-by-header>
  <vary-by-header>header name</vary-by-header>
  <vary-by-query-parameter>parameter name</vary-by-query-parameter>
</cache-lookup>
vary-by-header Add one or more of these elements to start caching responses per value of specified header, such as Accept, Accept-Charset, Accept-Encoding, Accept-Language, Authorization, Expect, From, Host, If-Match. inbound
Get value from cache <cache-lookup-value key="cache key value" default-value="value to use if cache lookup resulted in a miss" variable-name="name of a variable looked up value is assigned to" caching-type="prefer-external | external | internal" /> caching-type: internal to use the built-in API Management cache, external to use Redis. Any
Store to cache <cache-store duration="seconds" cache-response="true | false" /> Use with cache-lookup in inbound outbound
Store value in cache <cache-store-value key="cache key value" value="value to cache" duration="seconds" caching-type="prefer-external | external | internal" /> The operation is asynchronous. caching-type: internal to use the built-in API Management cache, external to use Redis. Any
--- --- --- ---
CORS <cors allow-credentials="false | true" terminate-unmatched-request="true | false">
  <allowed-origins>
    <origin>origin uri</origin>
  </allowed-origins>
  <allowed-methods preflight-result-max-age="number of seconds">
    <method>HTTP verb</method>
  </allowed-methods>
  <allowed-headers>
    <header>header name</header>
  </allowed-headers>
  <expose-headers>
    <header>header name</header>
  </expose-headers>
</cors>
Configure CORS at multiple scopes. Base element at operation, API, and product. Only cors evaluated on OPTIONS preflight. Other policies on approved request. Policy can be used once. inbound
Find and replace string in body <find-and-replace from="what to replace" to="replacement" /> Policy expressions are allowed. Any
Set backend service <set-backend-service base-url="base URL of the backend service" backend-id="name of the backend entity specifying base URL of the backend service" /> Redirect an incoming request to a different backend than the one specified in the API settings for that operation. Great for choose inbound, backend
Set body <set-body template="liquid" xsi-nil="blank | null">new body value as text</set-body> preserveContent not needed when providing new body. Inbound pipeline: no response yet, so no preserveContent. Outbound pipeline: request already sent, so no preserveContent. Exception if used in inbound GET with no body. inbound, outbound, backend
Set header <set-header name="header name" exists-action="override | skip | append | delete"><value>value</value></set-header> For multiple headers with the same name add additional value elements Any
Rewrite URL <rewrite-uri template="/v2/US/hardware/{storenumber}&{ordernumber}?City=city&State=state" /> Transform human/browser-friendly URL into the URL format expected by the web service inbound