SAP API Management for Beginners – Part 4: OAuth 2.0, KVM, Traffic Policies, Threat Protection&CORS

SAP API Management for Beginners – Part 4: OAuth 2.0, KVM, Traffic Policies, Threat Protection & CORS

Part 4 of a 5-part beginner series on SAP API Management within SAP Integration Suite.

A quick recap

In Part 1, we toured every APIM feature. In Part 2, we built Northwind_API_V1 — 75 auto-discovered resources, conditional flows, and the defaultRaiseFaultPolicy. In Part 3, we added the VerifyAPIKey policy, created a Product and Application, tested the full subscription flow, explored Debug, versioning, and policy templates.

After Part 3, our proxy’s PreFlow has one policy:

 
xml
<preFlow>
<name>PreFlow</name>
<request>
<isRequest>true</isRequest>
<steps>
<step>
<policy_name>VerifyAPIKey</policy_name>
<sequence>1</sequence>
</step>
</steps>
</request>
</preFlow>

And the <policies> section lists two entries:

 
xml
<policies>
<policy type=“RaiseFault”>defaultRaiseFaultPolicy</policy>
<policy type=“VerifyAPIKey”>VerifyAPIKey</policy>
</policies>

By the end of this post, both sections will be much bigger. We’ll add six policies — one at a time, each with placement rationale, XML config, and a Postman test.

1. Policy execution order — the golden rule

Before adding anything, let’s establish the execution model. This is the single biggest source of beginner confusion.

The golden rule: reject early, transform late.

Here’s where each type of policy belongs, and why:

 
ProxyEndpoint PreFlow (Incoming Request) — consumer-facing:
1. VerifyAPIKey ← authenticate first (already done)
2. ValidateOAuthToken ← second auth layer (this post)
3. JSONThreatProtection ← validate payload before processing
4. SpikeArrest ← throttle bursts
5. QuotaLimit ← cap total calls

ProxyEndpoint PostFlow (Outgoing Response) — consumer-facing:
6. AddCORSHeaders ← add Access-Control headers to response

TargetEndpoint PreFlow (Incoming Request) — backend-facing:
7. KVM-GetCredentials ← read backend credentials from secure store
8. InjectBasicAuth ← encode and inject Authorization header

💡This is the APIM equivalent of the “credential direction” insight from the Event Mesh series. Consumer-facing policies (who are you? how much can you call? is your payload safe?) go on ProxyEndpoint. Backend-facing policies (what credentials does the backend need?) go on TargetEndpoint. Mixing them up is the #1 reason policies “don’t work.”

After this post, the Proxy Endpoint PreFlow will have five steps:

 
xml
<preFlow>
<name>PreFlow</name>
<request>
<isRequest>true</isRequest>
<steps>
<step>
<policy_name>VerifyAPIKey</policy_name>
<sequence>1</sequence>
</step>
<step>
<policy_name>ValidateOAuthToken</policy_name>
<sequence>2</sequence>
</step>
<step>
<policy_name>JSONThreatProtection</policy_name>
<sequence>3</sequence>
</step>
<step>
<policy_name>SpikeArrest</policy_name>
<sequence>4</sequence>
</step>
<step>
<policy_name>QuotaLimit</policy_name>
<sequence>5</sequence>
</step>
</steps>
</request>
</preFlow>

And the Target Endpoint PreFlow (currently empty) will gain two steps:

 
xml
<preFlow>
<name>PreFlow</name>
<request>
<isRequest>true</isRequest>
<steps>
<step>
<policy_name>KVM-GetCredentials</policy_name>
<sequence>1</sequence>
</step>
<step>
<policy_name>InjectBasicAuth</policy_name>
<sequence>2</sequence>
</step>
</steps>
</request>
</preFlow>

Let’s build them one at a time.

2. OAuth 2.0 — APIM as its own token server

API keys are simple but limited — they’re long-lived strings that don’t expire unless you manually revoke them. OAuth 2.0 is the industry standard for token-based authentication, where consumers get short-lived access tokens.

Here’s the question most tutorials get wrong: where does the token come from? Most guides tell you to set up SAP IAS, Azure Entra ID, or Okta as an external OAuth provider. That works — but it adds complexity, cost, and another system to manage.

APIM can generate OAuth tokens itself. No external provider needed. We’ll build a dedicated OauthService proxy that acts as the token server, and then validate those tokens on our Northwind_API_V1 proxy.

2.1 The architecture — two proxies working together

 
Step 1 — Consumer gets a token from APIM:
POST https://<apim-host>/oauth/GenerateToken
Body: grant_type=client_credentials
&client_id=<Application Key>
&client_secret=<Application Secret>
→ APIM generates and returns an access_token

Step 2 — Consumer calls the API with that token:
GET https://<apim-host>/V1/ProxyNorthwindAPI/Customers
Header: Authorization: Bearer <access_token>
→ APIM validates the token → Route to Northwind

Two proxies:

OauthService at /oauth — generates tokens (using GenerateAccessToken)Northwind_API_V1 at /V1/ProxyNorthwindAPI — validates tokens (using VerifyAccessToken)

2.2 Build the OauthService proxy

This proxy has no backend — it IS the service. APIM’s built-in OAuth engine handles everything.

Step 1: In the API Portal, go to DevelopCreateAPI.

Step 2: This time, select URL (not API Provider):

Field Value SelectURLURLhttp://none.com/NameOauthServiceTitleOauthServiceAPI Base Path/oauthService TypeREST

💡Why http://none.com/? This proxy never calls a backend. The OAuth policy generates the token and returns it directly to the consumer. The target URL is a placeholder — APIM requires one, but it’s never used.

Here’s what the Target Endpoint looks like in the exported XML — notice provider_id=NONE:

 
xml
<TargetEndPoint xmlns=“http://www.sap.com/apimgmt”>
<name>default</name>
<url>http://none.com/</url>
<provider_id>NONE</provider_id>

</TargetEndPoint>

Step 3: Click Create.

Step 4: Go to the Resources tab. Add one resource:

Field Value Resource Path/GenerateTokenMethodsAll enabled (GET, POST, PUT, DELETE, etc.)

The resource XML:

 
xml
<APIResource xmlns=“http://www.sap.com/apimgmt”>
<name>GenerateToken</name>
<canShowGet>true</canShowGet>
<canShowPost>true</canShowPost>
<canShowPut>true</canShowPut>
<canShowDelete>true</canShowDelete>
<canShowHead>true</canShowHead>
<canShowOption>true</canShowOption>
<canShowPatch>true</canShowPatch>
<resource_path>/GenerateToken</resource_path>
</APIResource>

⚠️All methods are enabled because the OAuth token endpoint needs to accept POST (the standard) but also supports other methods for flexibility.

2.3 Add the GenerateAccessToken policy

This is the key difference from validation. Instead of VerifyAccessToken, we use GenerateAccessToken.

Step 1: Open the OauthService proxy → PoliciesEdit.

Step 2: In the conditional flows on the left, you’ll see the GenerateToken flow. Select it.

Step 3: Under Security Policies, click + next to OAuth v2.0.

Field Value Policy NameOauthv2StreamIncoming Request

Step 4: Update the XML:

 
xml
<OAuthV2 async=“false” continueOnError=“false”
enabled=“true” xmlns=“http://www.sap.com/apimgmt”>
<Operation>GenerateAccessToken</Operation>
<GenerateResponse/>
<SupportedGrantTypes>
<GrantType>client_credentials</GrantType>
</SupportedGrantTypes>
</OAuthV2>
Element Value What it does OperationGenerateAccessTokenAPIM generates a token (not validates)GenerateResponse(empty element)Tells APIM to return the token directly in the responseGrantTypeclient_credentialsConsumer authenticates with Application Key + Secret

💡Notice the difference: On OauthService, the operation is GenerateAccessToken. On Northwind_API_V1, it will be VerifyAccessToken. Generate vs Verify — one proxy creates tokens, the other checks them.

Step 5: The policy is placed on the conditional flow for /GenerateToken, not on the PreFlow. Here’s what the proxy endpoint XML looks like:

 
xml
<conditionalFlows>
<conditionalFlow>
<name>GenerateToken</name>
<request>
<isRequest>true</isRequest>
<steps>
<step>
<policy_name>Oauthv2</policy_name>
<sequence>1</sequence>
</step>
</steps>
</request>
<conditions>
(proxy.pathsuffix MatchesPath “/GenerateToken” …)
AND (request.verb = “POST” OR request.verb = “GET” …)
</conditions>
</conditionalFlow>
</conditionalFlows>

Step 6: Update, Save, Deploy.

The main proxy XML for OauthService:

 
xml
<APIProxy xmlns=“http://www.sap.com/apimgmt”>
<name>OauthService</name>
<title>OauthService</title>
<isVersioned>false</isVersioned>
<service_code>REST</service_code>
<APIState>Active</APIState>
<policies>
<policy type=“RaiseFault”>defaultRaiseFaultPolicy</policy>
<policy type=“OAuthV2”>Oauthv2</policy>
</policies>
</APIProxy>

2.4 Add the Product to OauthService

The consumer’s Application Key and Secret are used as OAuth client_id and client_secret. For this to work, the Application must be subscribed to a Product that includes the OauthService proxy.

Step 1: Go to Engage → select Northwind_Demo_Product → Edit.

Step 2: Add the OauthService proxy alongside Northwind_API_V1.

Step 3: Save and Publish.

Now the same Application (Demo_Test_App) is subscribed to both proxies — it can generate tokens through OauthService and use them on Northwind_API_V1.

2.5 Test token generation in Postman

Step 1: Create a POST request:

 
POST https://<your-apim-host>/oauth/GenerateToken
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=<your-Application-Key>
&client_secret=<your-Application-Secret>

⚠️The Content-Type must be application/x-www-form-urlencoded, NOT application/json. This is the same gotcha from the Event Mesh series with the XSUAA token endpoint. OAuth token endpoints always expect form-encoded bodies.

Expected response:

 
json
{
access_token: “eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…”,
token_type: “BearerToken”,
expires_in: “3599”,
scope: “”
}

🖼[Screenshot: Postman showing the token generation response with access_token, token_type, and expires_in]

💡The client_id and client_secret are your Application Key and Application Secret from Part 3. APIM validates them against the registered Application, confirms it’s subscribed to a Product that includes the OauthService proxy, and generates a token. No external IdP involved.

2.6 Add token validation to Northwind_API_V1

Now we need the Northwind proxy to accept and validate these tokens.

Step 1: Open Northwind_API_V1 → PoliciesEdit.

Step 2: Select PreFlow under ProxyEndpoint (Incoming Request).

Step 3: Under Security Policies, click + next to OAuth v2.0.

Field Value Policy NameValidateOAuthTokenStreamIncoming Request

Step 4: Update the XML — notice the operation is VerifyAccessToken:

 
xml
<OAuthV2 async=“false” continueOnError=“false”
enabled=“true”
xmlns=“http://www.sap.com/apimgmt”>
<Operation>VerifyAccessToken</Operation>
<SupportedGrantTypes/>
<Tokens/>
</OAuthV2>

Step 5: Update, Save, Deploy.

2.7 Test the full OAuth flow in Postman

Step 1: Generate a token (from section 2.5). Copy the access_token.

Step 2: Call the Northwind proxy with the token:

 
GET https://<apim-host>/V1/ProxyNorthwindAPI/Customers?$top=3&$format=json
Headers:
Authorization: Bearer <your-access-token>

Expected: 200 OK with Northwind Customers data.

Step 3: Try with a fabricated token:

 
Authorization: Bearer fake-token-12345

Expected: 401 Unauthorized — InvalidAccessToken

Step 4: Wait for the token to expire (default 3599 seconds = ~1 hour), then retry:

Expected: 401 Unauthorized — access_token_expired

💡OAuth vs API Key — when to use which:

Approach Use when API Key onlySimple integrations, internal consumers, quick setupOAuth onlyToken-based auth with automatic expiry, more secureBoth togetherAPI key identifies the Application (analytics/quota), OAuth token authenticates the identity (authorization). Best for production

2.8 What the export structure looks like

You now have two proxies:

 
OauthService/
├── APIProxy/
│ ├── OauthService.xml ← service_code: REST, no versioning
│ ├── APIProxyEndPoint/default.xml ← base_path: /oauth
│ ├── APITargetEndPoint/default.xml ← url: http://none.com/, provider_id: NONE
│ ├── Policy/Oauthv2.xml ← GenerateAccessToken + client_credentials
│ └── APIResource/GenerateToken.xml ← /GenerateToken, all methods

Northwind_API_V1/
├── APIProxy/
│ ├── Northwind_API_V1.xml
│ ├── Policy/ValidateOAuthToken.xml ← VerifyAccessToken
│ └── … (75 resources, 9+ policies)

💡This pattern scales beautifully. One OauthService proxy serves tokens for ALL your API proxies — Northwind, S/4HANA, CPI endpoints. You create it once, add it to every Product, and every consumer gets OAuth for free.

2.9 Alternative: External OAuth provider

If your enterprise requires integration with an existing IdP (SAP IAS, Azure Entra ID, Okta), you can skip the OauthService proxy and configure APIM to trust external tokens instead. In that case:

Configure the OAuth Provider in ConfigureAPI Portal Settings (JWKS URI, token endpoint, etc.)The ValidateOAuthToken policy on Northwind_API_V1 validates tokens from the external providerThe consumer gets tokens from the external IdP, not from APIM

Both approaches use the same VerifyAccessToken policy on the API proxy — the only difference is where the token comes from. For this series (and for most beginner setups), the APIM-native OauthService approach is simpler and self-contained.

2.10 What changed in the Northwind export

A new file appears in the Policy/ folder: ValidateOAuthToken.xml. The main proxy XML now lists three policies:

 
xml
<policies>
<policy type=“RaiseFault”>defaultRaiseFaultPolicy</policy>
<policy type=“VerifyAPIKey”>VerifyAPIKey</policy>
<policy type=“OAuthV2”>ValidateOAuthToken</policy>
</policies>

3. Key Value Maps — secure credential storage

Our Northwind_API_V1 proxy connects to a public service with no authentication. But in production (S/4HANA, third-party APIs), the backend requires credentials. Key Value Maps (KVM) are APIM’s secure credential store — the equivalent of Security Material in CPI.

3.1 Create a KVM

Step 1: Go to ConfigureKey Value Maps.

Step 2: Click Create.

Field Value NameBackend_CredentialsEncryptedYes

Step 3: Add entries:

Key Value usernameAPIM_COMM_USERpassword<your-communication-user-password>

Step 4: Click Save.

🔒Encrypted KVMs mask values after save. You can’t read them back — only overwrite. This is the right way to handle credentials, not hardcoding them in the API Provider or in policy XML.

🖼[Screenshot: KVM creation with Backend_Credentials name and two encrypted entries]

3.2 Read KVM values in a policy

Where: TargetEndpoint → PreFlow → Incoming Request

⚠️Why TargetEndpoint? Look at our exported Target Endpoint — it has provider_id=Northwind_API and relativePath=/Northwind/Northwind.svc/. These credentials are for authenticating to that backend, not for the consumer. Consumer auth (API key, OAuth) lives on ProxyEndpoint. Backend auth lives on TargetEndpoint. Same credential direction principle from Event Mesh — who the credentials belong to determines where they go.

Step 1: In the Policy Editor, select PreFlow under TargetEndpoint (Incoming Request).

Step 2: Under Mediation Policies, click + next to Key Value Map Operations.

Field Value Policy NameKVM-GetCredentialsStreamIncoming Request

Step 3: Update the XML:

 
xml
<KeyValueMapOperations mapIdentifier=“Backend_Credentials”
async=“true” continueOnError=“false”
enabled=“true”
xmlns=“http://www.sap.com/apimgmt”>
<Get assignTo=“private.backend.username” index=“1”>
<Key><Parameter>username</Parameter></Key>
</Get>
<Get assignTo=“private.backend.password” index=“1”>
<Key><Parameter>password</Parameter></Key>
</Get>
<Scope>environment</Scope>
</KeyValueMapOperations>
Element What it does mapIdentifier=”Backend_Credentials”References the KVM we createdassignTo=”private.backend.username”Stores the value in a flow variableScopeenvironment — accessible across all proxies

💡The private. prefix is critical. Variables named private.xxx are automatically excluded from debug traces and analytics logs. Without it, your credentials appear in plain text during debugging. Always use private. for sensitive values.

3.3 Inject Basic Auth header

Add a Basic Authentication policy right after the KVM policy on TargetEndpoint PreFlow:

Field Value Policy NameInjectBasicAuthStreamIncoming Request
 
xml
<BasicAuthentication async=“true” continueOnError=“false”
enabled=“true”
xmlns=“http://www.sap.com/apimgmt”>
<Operation>Encode</Operation>
<IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
<User ref=“private.backend.username”/>
<Password ref=“private.backend.password”/>
<AssignTo createNew=“true”>request.header.Authorization</AssignTo>
</BasicAuthentication>
Element What it does Operation: EncodeBase64-encodes username:password into Authorization: Basic xxxUser ref / Password refReads from the flow variables set by the KVM policyAssignToWrites the encoded value into the outbound Authorization header

Step 4: Update, Save, Deploy.

💡What this achieves: The consumer sends only an API key (and optionally an OAuth token). APIM internally reads the backend credentials from the KVM and injects the Authorization header before calling the backend. The consumer never sees or handles backend credentials. This decoupling of consumer identity from backend identity is fundamental.

⚠️For Northwind specifically: Since Northwind is a public service, these backend credentials aren’t actually needed — Northwind accepts unauthenticated requests. But the pattern is identical for S/4HANA, where the Communication User credentials in the KVM authenticate against the Communication Arrangement. We’re building the pattern here so it’s ready when you swap the backend.

🖼[Screenshot: TargetEndpoint PreFlow with KVM-GetCredentials and InjectBasicAuth policies]

4. Spike Arrest — throttle traffic bursts

Where: ProxyEndpoint → PreFlow → Incoming Request (after authentication policies)

Field Value Policy NameSpikeArrestStreamIncoming Request
 
xml
<SpikeArrest async=“true” continueOnError=“false”
enabled=“true”
xmlns=“http://www.sap.com/apimgmt”>
<Identifier ref=“request.header.APIKey”/>
<Rate>12pm</Rate>
<UseEffectiveCount>true</UseEffectiveCount>
</SpikeArrest>
Element Value What it does Identifierrequest.header.APIKeyThrottle per Application — each consumer gets their own spike limitRate12pm12 per minuteUseEffectiveCounttrueCount across all APIM runtime nodes

⚠️Understanding the smoothing: 12pm does NOT mean “allow 12 calls then block until the minute resets.” APIM distributes evenly: 12/min = one every 5 seconds. If two requests arrive within 1 second, the second is rejected — even if only 2 calls have happened the whole minute. This catches spikes, not aggregate overuse. For aggregate limits, use Quota (next section).

Testing in Postman:

Send 3 rapid requests to /V1/ProxyNorthwindAPI/Customers?$top=3&$format=json (with your API key). Click Send as fast as you can.

Request 1: 200 OKRequest 2 or 3:

 
json
{
fault: {
faultstring: “Spike arrest violation. Allowed rate: 12pm”,
detail: {
errorcode: “policies.ratelimit.SpikeArrestViolation”
}
}
}

HTTP Status: 429 Too Many Requests

Wait 5 seconds, send again — 200 OK.

💡For production, set this higher. 12pm is intentionally low for easy testing. Real-world values: 30ps (30 per second) or 1000pm depending on backend capacity.

🖼[Screenshot: Postman showing the 429 SpikeArrestViolation response]

5. Quota — cap total API calls

While Spike Arrest handles bursts, Quota controls the total over a longer period.

Where: ProxyEndpoint → PreFlow → Incoming Request (after SpikeArrest)

Field Value Policy NameQuotaLimitStreamIncoming Request
 
xml
<Quota async=“true” continueOnError=“false”
enabled=“true” type=“calendar”
xmlns=“http://www.sap.com/apimgmt”>
<Identifier ref=“request.header.APIKey”/>
<Allow countRef=“apiproduct.developer.quota.limit” count=“1000”/>
<Interval ref=“apiproduct.developer.quota.interval”>1</Interval>
<Distributed>true</Distributed>
<StartTime>2025-01-01 00:00:00</StartTime>
<Synchronous>true</Synchronous>
<TimeUnit ref=“apiproduct.developer.quota.timeunit”>day</TimeUnit>
</Quota>

⚠️Element order matters in APIM policy XML. The schema enforces a strict sequence: Allow → Interval → Distributed → StartTime → Synchronous → TimeUnit. If you put TimeUnit before StartTime, you’ll get: Invalid content was found starting with element ‘StartTime’. No child element is expected at this point. This is one of those errors where the XML looks correct but the parser rejects it — the elements are all valid, just in the wrong order.

Element What it does countRef=”apiproduct.developer.quota.limit”Reads the limit from the Product’s Quota settings — different Products can have different limitscount=”1000″Fallback if the Product doesn’t define a quotaTimeUnit: dayCounter resets daily at midnightDistributed: trueCount across all APIM runtime nodes

💡Spike Arrest vs Quota — the quick rule:

Spike Arrest = speed limit (requests per second/minute) — prevents floodsQuota = data plan (total calls per day/month) — prevents overuse

You almost always want both.

Testing: Temporarily set count=”5″, redeploy, and send 6 requests:

Requests 1–5: 200 OKRequest 6: 429 QuotaViolation

⚠️Remember to reset to count=”1000″ after testing.

Dynamic Quota from the Product:

The countRef attribute makes quotas dynamic. To configure it on the Product side:

Go to Engage → select Northwind_Demo_Product → EditSet Calls: 5000, Interval: 1, Time Unit: DaySave and Publish

Now a “Free” Product can have 100 calls/day and a “Premium” Product 10,000 — same proxy, same policy, different limits.

6. JSON Threat Protection — block malicious payloads

For APIs that accept POST or PUT (our Northwind_API_V1 supports POST on collections and PUT on single entities), protect against oversized or deeply nested payloads.

Where: ProxyEndpoint → PreFlow → Incoming Request (after VerifyAPIKey, before SpikeArrest)

Field Value Policy NameJSONThreatProtectionStreamIncoming Request
 
xml
<JSONThreatProtection async=“true” continueOnError=“false”
enabled=“true”
xmlns=“http://www.sap.com/apimgmt”>
<Source>request</Source>
<ArrayElementCount>50</ArrayElementCount>
<ContainerDepth>10</ContainerDepth>
<ObjectEntryCount>50</ObjectEntryCount>
<ObjectEntryNameLength>128</ObjectEntryNameLength>
<StringValueLength>5000</StringValueLength>
</JSONThreatProtection>

⚠️Element order matters here too. <Source> must come first — before any of the limit elements. If you put it last, you’ll get: Invalid content was found starting with element ‘StringValueLength’. One of ‘Source’ is expected. This is the same strict-ordering pattern we saw with the Quota policy — always check the policy template for the correct element sequence.

Element Limit Blocks ArrayElementCount50Arrays with 50+ items (memory-bomb payloads)ContainerDepth10Nesting deeper than 10 levels (stack overflow attacks)ObjectEntryCount50Objects with 50+ keysObjectEntryNameLength128Key names longer than 128 charactersStringValueLength5000Strings longer than 5,000 characters (content injection)SourcerequestApply to the incoming request body only

Testing in Postman:

Create a POST request to /V1/ProxyNorthwindAPI/Customers with an 11-level deep JSON body:

 
json
{
a: { b: { c: { d: { e: { f: { g: { h: { i: { j: { k: “too deep” } } } } } } } } } }
}

Expected: 400 Bad Request

 
json
{
fault: {
faultstring: “JSONThreatProtection[JSONThreatProtection]: Exceeded container depth…”,
detail: {
errorcode: “steps.jsonthreatprotection.ExecutionFailed”
}
}
}

💡This works together with the conditional flows from Part 2. The conditional flow for Customers (collection) allows POST. If the POST passes the conditional flow check but the JSON body is malicious, JSONThreatProtection catches it. Two layers: method enforcement + payload validation.

🖼[Screenshot: Postman showing the 400 JSONThreatProtection violation]

7. CORS — enable browser-based consumers

If a web application (SAP Build Apps, Fiori, React) calls your /V1/ProxyNorthwindAPI endpoint from a browser, it will fail with a CORS error. Browsers enforce the Same-Origin Policy — they block requests to different domains unless the response includes Access-Control-* headers.

Where: ProxyEndpoint → PostFlow → Outgoing Response

⚠️Why PostFlow Outgoing Response? CORS headers go on the response, not the request. And on the ProxyEndpoint (consumer-facing), because the browser’s CORS check happens between the consumer and APIM — not between APIM and Northwind.

Field Value Policy NameAddCORSHeadersStreamOutgoing Response
 
xml
<AssignMessage async=“false” continueOnError=“false”
enabled=“true”
xmlns=“http://www.sap.com/apimgmt”>
<Set>
<Headers>
<Header name=“Access-Control-Allow-Origin”>*</Header>
<Header name=“Access-Control-Allow-Methods”>GET, POST, PUT, DELETE, OPTIONS</Header>
<Header name=“Access-Control-Allow-Headers”>APIKey, Content-Type, Authorization</Header>
<Header name=“Access-Control-Max-Age”>3600</Header>
</Headers>
</Set>
<IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
<AssignTo createNew=“false” type=“response”/>
</AssignMessage>
Header Why it matters Allow-Origin: *Allow any domain. In production, restrict to specific origins like https://myapp.launchpad.cfapps.us10.hana.ondemand.comAllow-Headers: APIKey, …APIKey must be listed here — otherwise the browser strips it from the requestMax-Age: 3600Cache the preflight response for 1 hour

⚠️OPTIONS preflight: Browsers send a preflight OPTIONS request before the real call. If VerifyAPIKey runs on OPTIONS too, it’ll reject the preflight (which has no API key). Use a conditional flow to skip key verification for OPTIONS requests — add the condition (request.verb != “OPTIONS”) around the VerifyAPIKey policy step.

Step 5: Update, Save, Deploy.

🖼[Screenshot: Policy Editor showing AddCORSHeaders on ProxyEndpoint PostFlow (Outgoing Response)]

8. The complete policy lineup — what the export looks like now

After this post, your proxy has eight policies. Here’s the full picture:

8.1 Policy summary table

# Policy Location Stream Purpose 1VerifyAPIKeyProxyEndpoint PreFlowIncoming RequestIdentify the consumer (Application)2ValidateOAuthTokenProxyEndpoint PreFlowIncoming RequestAuthenticate the consumer (identity)3JSONThreatProtectionProxyEndpoint PreFlowIncoming RequestBlock malicious payloads4SpikeArrestProxyEndpoint PreFlowIncoming RequestThrottle traffic bursts5QuotaLimitProxyEndpoint PreFlowIncoming RequestCap daily API calls6AddCORSHeadersProxyEndpoint PostFlowOutgoing ResponseEnable browser access7KVM-GetCredentialsTargetEndpoint PreFlowIncoming RequestRead backend credentials8InjectBasicAuthTargetEndpoint PreFlowIncoming RequestAdd Authorization header to backend call

Plus the auto-generated defaultRaiseFaultPolicy on the DefaultFaultFlow.

8.2 What the export’s Policy folder looks like

 
APIProxy/
├── Policy/
│ ├── defaultRaiseFaultPolicy.xml ← auto-generated (Part 2)
│ ├── VerifyAPIKey.xml ← added in Part 3
│ ├── ValidateOAuthToken.xml ← added in this post
│ ├── JSONThreatProtection.xml ← added in this post
│ ├── SpikeArrest.xml ← added in this post
│ ├── QuotaLimit.xml ← added in this post
│ ├── AddCORSHeaders.xml ← added in this post
│ ├── KVM-GetCredentials.xml ← added in this post
│ └── InjectBasicAuth.xml ← added in this post

8.3 The main proxy XML’s policies section

 
xml
<policies>
<policy type=“RaiseFault”>defaultRaiseFaultPolicy</policy>
<policy type=“VerifyAPIKey”>VerifyAPIKey</policy>
<policy type=“OAuthV2”>ValidateOAuthToken</policy>
<policy type=“JSONThreatProtection”>JSONThreatProtection</policy>
<policy type=“SpikeArrest”>SpikeArrest</policy>
<policy type=“Quota”>QuotaLimit</policy>
<policy type=“AssignMessage”>AddCORSHeaders</policy>
<policy type=“KeyValueMapOperations”>KVM-GetCredentials</policy>
<policy type=“BasicAuthentication”>InjectBasicAuth</policy>
</policies>

💡This is your proxy’s bill of materials. When you export the zip and check it into Git, you can see every policy, every placement, and every configuration. When a colleague asks “what governance does this proxy have?” — this list is the answer.

⚠️Order matters within a PreFlow. The <sequence> numbers determine execution order. If you put SpikeArrest (sequence 4) before VerifyAPIKey (sequence 1), you’d count rate limits for unauthorized requests — wasting your spike budget on junk traffic. Always: authenticate → validate → throttle → route.

9. Update your Policy Template

In Part 3, we created Baseline_Security_Template with just VerifyAPIKey. Let’s expand it to the full baseline:

Step 1: Go to DevelopPolicy Templates → open Baseline_Security_Template.

Step 2: Add these policies in order on ProxyEndpoint PreFlow (Incoming Request):

VerifyAPIKey (already there)JSONThreatProtectionSpikeArrest (Rate: 30ps — more realistic than 12pm)QuotaLimit (with countRef for dynamic Product limits)

Step 3: Add on ProxyEndpoint PostFlow (Outgoing Response): 5. AddCORSHeaders

Step 4: Save.

Now every new proxy gets the full security + traffic + CORS baseline in one click. Five policies, correct placement, sensible defaults.

💡SAP recommends a baseline template for all proxies: Verify API Key + JSON/XML Threat Protection + Regular Expression Protection + Spike Arrest + Quota. Our template covers four of five — add Regular Expression Protection if your APIs accept user input in URLs or headers.

10. Troubleshooting reference

Error HTTP Which policy Fix FailedToResolveAPIKey401VerifyAPIKeyConsumer didn’t send the APIKey headerInvalidApiKey401VerifyAPIKeyWrong key, expired, or not subscribed to the ProductInvalidAccessToken401OAuth v2.0Token is invalid, expired, or from an untrusted issueraccess_token_expired401OAuth v2.0Token has expired — consumer needs to refreshSpikeArrestViolation429SpikeArrestToo many requests too fast — space them out or raise the rateQuotaViolation429QuotaDaily limit hit — upgrade Product tier or wait for midnight resetJSONThreatProtection: Exceeded…400JSONThreatProtectionPayload too deep/large — consumer must simplifyUnresolvedVariable: private.backend.username500KVM-GetCredentialsKVM name mismatch (mapIdentifier doesn’t match the KVM name) or key doesn’t existBasicAuthentication: Unable to Encode500InjectBasicAuthKVM values are empty or variable names don’t match between KVM and BasicAuth policiesInvalid content was found starting with element…(save error)Any policyXML element order is wrong — APIM enforces strict element sequence. Check the policy template for the correct orderClient identifier is required401OauthServiceclient_id and client_secret must be in the body (x-www-form-urlencoded), not in headersUnresolved variable: private.backend.username500KVM-GetCredentialsKVM not created, or mapIdentifier doesn’t match the KVM name. For Northwind (no auth), remove KVM + BasicAuth policies from TargetEndpoint entirely

💡Use the Debug tool (Part 3, section 😎 to pinpoint which policy failed. The trace shows each policy step in sequence — you see exactly where the flow stopped and what variables were set. When KVM-GetCredentials succeeds but InjectBasicAuth fails, it means the variable names don’t match between the two policies.

Quick Reference

Item Value OAuth 2.0ProxyEndpoint PreFlow · <Operation>VerifyAccessToken</Operation>Token Content-Typeapplication/x-www-form-urlencoded (NOT JSON)KVMConfigure → Key Value Maps → Create (Encrypted) · private. prefix for variablesBasic Auth (backend)TargetEndpoint PreFlow · reads from KVM · writes to request.header.AuthorizationSpike ArrestProxyEndpoint PreFlow · <Rate>12pm</Rate> (testing) or <Rate>30ps</Rate> (production)QuotaProxyEndpoint PreFlow · countRef=”apiproduct.developer.quota.limit” for dynamic limitsJSON Threat ProtectionProxyEndpoint PreFlow · limits depth (10), arrays (50), strings (5000)CORSProxyEndpoint PostFlow (Outgoing Response) · AssignMessage with Access-Control-* headersExport structure9 policy files in Policy/ folder after this post

What’s next

In Part 5: Advanced Topics — Routing, Path Removal, Developer Hub, Analytics & MCP Gateway, we tackle the patterns that separate beginners from practitioners. We’ll route requests to different backends using policies, rewrite URL paths for clean consumer-facing URLs, walk through the Developer Hub from a consumer’s perspective, set up Analytics dashboards with custom metrics via Statistics Collector, and close the series with the brand-new MCP Gateway for AI agents.

👉Part 5: Advanced Topics — coming next.

 

​ SAP API Management for Beginners – Part 4: OAuth 2.0, KVM, Traffic Policies, Threat Protection & CORSPart 4 of a 5-part beginner series on SAP API Management within SAP Integration Suite.A quick recapIn Part 1, we toured every APIM feature. In Part 2, we built Northwind_API_V1 — 75 auto-discovered resources, conditional flows, and the defaultRaiseFaultPolicy. In Part 3, we added the VerifyAPIKey policy, created a Product and Application, tested the full subscription flow, explored Debug, versioning, and policy templates.After Part 3, our proxy’s PreFlow has one policy: xml<preFlow>
<name>PreFlow</name>
<request>
<isRequest>true</isRequest>
<steps>
<step>
<policy_name>VerifyAPIKey</policy_name>
<sequence>1</sequence>
</step>
</steps>
</request>
</preFlow>And the <policies> section lists two entries: xml<policies>
<policy type=”RaiseFault”>defaultRaiseFaultPolicy</policy>
<policy type=”VerifyAPIKey”>VerifyAPIKey</policy>
</policies>By the end of this post, both sections will be much bigger. We’ll add six policies — one at a time, each with placement rationale, XML config, and a Postman test.1. Policy execution order — the golden ruleBefore adding anything, let’s establish the execution model. This is the single biggest source of beginner confusion.The golden rule: reject early, transform late.Here’s where each type of policy belongs, and why: ProxyEndpoint PreFlow (Incoming Request) — consumer-facing:
1. VerifyAPIKey ← authenticate first (already done)
2. ValidateOAuthToken ← second auth layer (this post)
3. JSONThreatProtection ← validate payload before processing
4. SpikeArrest ← throttle bursts
5. QuotaLimit ← cap total calls

ProxyEndpoint PostFlow (Outgoing Response) — consumer-facing:
6. AddCORSHeaders ← add Access-Control headers to response

TargetEndpoint PreFlow (Incoming Request) — backend-facing:
7. KVM-GetCredentials ← read backend credentials from secure store
8. InjectBasicAuth ← encode and inject Authorization header💡This is the APIM equivalent of the “credential direction” insight from the Event Mesh series. Consumer-facing policies (who are you? how much can you call? is your payload safe?) go on ProxyEndpoint. Backend-facing policies (what credentials does the backend need?) go on TargetEndpoint. Mixing them up is the #1 reason policies “don’t work.”After this post, the Proxy Endpoint PreFlow will have five steps: xml<preFlow>
<name>PreFlow</name>
<request>
<isRequest>true</isRequest>
<steps>
<step>
<policy_name>VerifyAPIKey</policy_name>
<sequence>1</sequence>
</step>
<step>
<policy_name>ValidateOAuthToken</policy_name>
<sequence>2</sequence>
</step>
<step>
<policy_name>JSONThreatProtection</policy_name>
<sequence>3</sequence>
</step>
<step>
<policy_name>SpikeArrest</policy_name>
<sequence>4</sequence>
</step>
<step>
<policy_name>QuotaLimit</policy_name>
<sequence>5</sequence>
</step>
</steps>
</request>
</preFlow>And the Target Endpoint PreFlow (currently empty) will gain two steps: xml<preFlow>
<name>PreFlow</name>
<request>
<isRequest>true</isRequest>
<steps>
<step>
<policy_name>KVM-GetCredentials</policy_name>
<sequence>1</sequence>
</step>
<step>
<policy_name>InjectBasicAuth</policy_name>
<sequence>2</sequence>
</step>
</steps>
</request>
</preFlow>Let’s build them one at a time.2. OAuth 2.0 — APIM as its own token serverAPI keys are simple but limited — they’re long-lived strings that don’t expire unless you manually revoke them. OAuth 2.0 is the industry standard for token-based authentication, where consumers get short-lived access tokens.Here’s the question most tutorials get wrong: where does the token come from? Most guides tell you to set up SAP IAS, Azure Entra ID, or Okta as an external OAuth provider. That works — but it adds complexity, cost, and another system to manage.APIM can generate OAuth tokens itself. No external provider needed. We’ll build a dedicated OauthService proxy that acts as the token server, and then validate those tokens on our Northwind_API_V1 proxy.2.1 The architecture — two proxies working together Step 1 — Consumer gets a token from APIM:
POST https://<apim-host>/oauth/GenerateToken
Body: grant_type=client_credentials
&client_id=<Application Key>
&client_secret=<Application Secret>
→ APIM generates and returns an access_token

Step 2 — Consumer calls the API with that token:
GET https://<apim-host>/V1/ProxyNorthwindAPI/Customers
Header: Authorization: Bearer <access_token>
→ APIM validates the token → Route to NorthwindTwo proxies:OauthService at /oauth — generates tokens (using GenerateAccessToken)Northwind_API_V1 at /V1/ProxyNorthwindAPI — validates tokens (using VerifyAccessToken)2.2 Build the OauthService proxyThis proxy has no backend — it IS the service. APIM’s built-in OAuth engine handles everything.Step 1: In the API Portal, go to Develop → Create → API.Step 2: This time, select URL (not API Provider):Field Value SelectURLURLhttp://none.com/NameOauthServiceTitleOauthServiceAPI Base Path/oauthService TypeREST💡Why http://none.com/? This proxy never calls a backend. The OAuth policy generates the token and returns it directly to the consumer. The target URL is a placeholder — APIM requires one, but it’s never used.Here’s what the Target Endpoint looks like in the exported XML — notice provider_id=NONE: xml<TargetEndPoint xmlns=”http://www.sap.com/apimgmt”>
<name>default</name>
<url>http://none.com/</url>
<provider_id>NONE</provider_id>

</TargetEndPoint>Step 3: Click Create.Step 4: Go to the Resources tab. Add one resource:Field Value Resource Path/GenerateTokenMethodsAll enabled (GET, POST, PUT, DELETE, etc.)The resource XML: xml<APIResource xmlns=”http://www.sap.com/apimgmt”>
<name>GenerateToken</name>
<canShowGet>true</canShowGet>
<canShowPost>true</canShowPost>
<canShowPut>true</canShowPut>
<canShowDelete>true</canShowDelete>
<canShowHead>true</canShowHead>
<canShowOption>true</canShowOption>
<canShowPatch>true</canShowPatch>
<resource_path>/GenerateToken</resource_path>
</APIResource>⚠️All methods are enabled because the OAuth token endpoint needs to accept POST (the standard) but also supports other methods for flexibility.2.3 Add the GenerateAccessToken policyThis is the key difference from validation. Instead of VerifyAccessToken, we use GenerateAccessToken.Step 1: Open the OauthService proxy → Policies → Edit.Step 2: In the conditional flows on the left, you’ll see the GenerateToken flow. Select it.Step 3: Under Security Policies, click + next to OAuth v2.0.Field Value Policy NameOauthv2StreamIncoming RequestStep 4: Update the XML: xml<OAuthV2 async=”false” continueOnError=”false”
enabled=”true” xmlns=”http://www.sap.com/apimgmt”>
<Operation>GenerateAccessToken</Operation>
<GenerateResponse/>
<SupportedGrantTypes>
<GrantType>client_credentials</GrantType>
</SupportedGrantTypes>
</OAuthV2>Element Value What it does OperationGenerateAccessTokenAPIM generates a token (not validates)GenerateResponse(empty element)Tells APIM to return the token directly in the responseGrantTypeclient_credentialsConsumer authenticates with Application Key + Secret💡Notice the difference: On OauthService, the operation is GenerateAccessToken. On Northwind_API_V1, it will be VerifyAccessToken. Generate vs Verify — one proxy creates tokens, the other checks them.Step 5: The policy is placed on the conditional flow for /GenerateToken, not on the PreFlow. Here’s what the proxy endpoint XML looks like: xml<conditionalFlows>
<conditionalFlow>
<name>GenerateToken</name>
<request>
<isRequest>true</isRequest>
<steps>
<step>
<policy_name>Oauthv2</policy_name>
<sequence>1</sequence>
</step>
</steps>
</request>
<conditions>
(proxy.pathsuffix MatchesPath “/GenerateToken” …)
AND (request.verb = “POST” OR request.verb = “GET” …)
</conditions>
</conditionalFlow>
</conditionalFlows>Step 6: Update, Save, Deploy.The main proxy XML for OauthService: xml<APIProxy xmlns=”http://www.sap.com/apimgmt”>
<name>OauthService</name>
<title>OauthService</title>
<isVersioned>false</isVersioned>
<service_code>REST</service_code>
<APIState>Active</APIState>
<policies>
<policy type=”RaiseFault”>defaultRaiseFaultPolicy</policy>
<policy type=”OAuthV2″>Oauthv2</policy>
</policies>
</APIProxy>2.4 Add the Product to OauthServiceThe consumer’s Application Key and Secret are used as OAuth client_id and client_secret. For this to work, the Application must be subscribed to a Product that includes the OauthService proxy.Step 1: Go to Engage → select Northwind_Demo_Product → Edit.Step 2: Add the OauthService proxy alongside Northwind_API_V1.Step 3: Save and Publish.Now the same Application (Demo_Test_App) is subscribed to both proxies — it can generate tokens through OauthService and use them on Northwind_API_V1.2.5 Test token generation in PostmanStep 1: Create a POST request: POST https://<your-apim-host>/oauth/GenerateToken
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=<your-Application-Key>
&client_secret=<your-Application-Secret>⚠️The Content-Type must be application/x-www-form-urlencoded, NOT application/json. This is the same gotcha from the Event Mesh series with the XSUAA token endpoint. OAuth token endpoints always expect form-encoded bodies.Expected response: json{
“access_token”: “eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…”,
“token_type”: “BearerToken”,
“expires_in”: “3599”,
“scope”: “”
}🖼️ [Screenshot: Postman showing the token generation response with access_token, token_type, and expires_in]💡The client_id and client_secret are your Application Key and Application Secret from Part 3. APIM validates them against the registered Application, confirms it’s subscribed to a Product that includes the OauthService proxy, and generates a token. No external IdP involved.2.6 Add token validation to Northwind_API_V1Now we need the Northwind proxy to accept and validate these tokens.Step 1: Open Northwind_API_V1 → Policies → Edit.Step 2: Select PreFlow under ProxyEndpoint (Incoming Request).Step 3: Under Security Policies, click + next to OAuth v2.0.Field Value Policy NameValidateOAuthTokenStreamIncoming RequestStep 4: Update the XML — notice the operation is VerifyAccessToken: xml<OAuthV2 async=”false” continueOnError=”false”
enabled=”true”
xmlns=”http://www.sap.com/apimgmt”>
<Operation>VerifyAccessToken</Operation>
<SupportedGrantTypes/>
<Tokens/>
</OAuthV2>Step 5: Update, Save, Deploy.2.7 Test the full OAuth flow in PostmanStep 1: Generate a token (from section 2.5). Copy the access_token.Step 2: Call the Northwind proxy with the token: GET https://<apim-host>/V1/ProxyNorthwindAPI/Customers?$top=3&$format=json
Headers:
Authorization: Bearer <your-access-token>Expected: 200 OK with Northwind Customers data.Step 3: Try with a fabricated token: Authorization: Bearer fake-token-12345Expected: 401 Unauthorized — InvalidAccessTokenStep 4: Wait for the token to expire (default 3599 seconds = ~1 hour), then retry:Expected: 401 Unauthorized — access_token_expired💡OAuth vs API Key — when to use which:Approach Use when API Key onlySimple integrations, internal consumers, quick setupOAuth onlyToken-based auth with automatic expiry, more secureBoth togetherAPI key identifies the Application (analytics/quota), OAuth token authenticates the identity (authorization). Best for production2.8 What the export structure looks likeYou now have two proxies: OauthService/
├── APIProxy/
│ ├── OauthService.xml ← service_code: REST, no versioning
│ ├── APIProxyEndPoint/default.xml ← base_path: /oauth
│ ├── APITargetEndPoint/default.xml ← url: http://none.com/, provider_id: NONE
│ ├── Policy/Oauthv2.xml ← GenerateAccessToken + client_credentials
│ └── APIResource/GenerateToken.xml ← /GenerateToken, all methods

Northwind_API_V1/
├── APIProxy/
│ ├── Northwind_API_V1.xml
│ ├── Policy/ValidateOAuthToken.xml ← VerifyAccessToken
│ └── … (75 resources, 9+ policies)💡This pattern scales beautifully. One OauthService proxy serves tokens for ALL your API proxies — Northwind, S/4HANA, CPI endpoints. You create it once, add it to every Product, and every consumer gets OAuth for free.2.9 Alternative: External OAuth providerIf your enterprise requires integration with an existing IdP (SAP IAS, Azure Entra ID, Okta), you can skip the OauthService proxy and configure APIM to trust external tokens instead. In that case:Configure the OAuth Provider in Configure → API Portal Settings (JWKS URI, token endpoint, etc.)The ValidateOAuthToken policy on Northwind_API_V1 validates tokens from the external providerThe consumer gets tokens from the external IdP, not from APIMBoth approaches use the same VerifyAccessToken policy on the API proxy — the only difference is where the token comes from. For this series (and for most beginner setups), the APIM-native OauthService approach is simpler and self-contained.2.10 What changed in the Northwind exportA new file appears in the Policy/ folder: ValidateOAuthToken.xml. The main proxy XML now lists three policies: xml<policies>
<policy type=”RaiseFault”>defaultRaiseFaultPolicy</policy>
<policy type=”VerifyAPIKey”>VerifyAPIKey</policy>
<policy type=”OAuthV2″>ValidateOAuthToken</policy>
</policies>3. Key Value Maps — secure credential storageOur Northwind_API_V1 proxy connects to a public service with no authentication. But in production (S/4HANA, third-party APIs), the backend requires credentials. Key Value Maps (KVM) are APIM’s secure credential store — the equivalent of Security Material in CPI.3.1 Create a KVMStep 1: Go to Configure → Key Value Maps.Step 2: Click Create.Field Value NameBackend_CredentialsEncrypted✅YesStep 3: Add entries:Key Value usernameAPIM_COMM_USERpassword<your-communication-user-password>Step 4: Click Save.🔒Encrypted KVMs mask values after save. You can’t read them back — only overwrite. This is the right way to handle credentials, not hardcoding them in the API Provider or in policy XML.🖼️ [Screenshot: KVM creation with Backend_Credentials name and two encrypted entries]3.2 Read KVM values in a policyWhere: TargetEndpoint → PreFlow → Incoming Request⚠️Why TargetEndpoint? Look at our exported Target Endpoint — it has provider_id=Northwind_API and relativePath=/Northwind/Northwind.svc/. These credentials are for authenticating to that backend, not for the consumer. Consumer auth (API key, OAuth) lives on ProxyEndpoint. Backend auth lives on TargetEndpoint. Same credential direction principle from Event Mesh — who the credentials belong to determines where they go.Step 1: In the Policy Editor, select PreFlow under TargetEndpoint (Incoming Request).Step 2: Under Mediation Policies, click + next to Key Value Map Operations.Field Value Policy NameKVM-GetCredentialsStreamIncoming RequestStep 3: Update the XML: xml<KeyValueMapOperations mapIdentifier=”Backend_Credentials”
async=”true” continueOnError=”false”
enabled=”true”
xmlns=”http://www.sap.com/apimgmt”>
<Get assignTo=”private.backend.username” index=”1″>
<Key><Parameter>username</Parameter></Key>
</Get>
<Get assignTo=”private.backend.password” index=”1″>
<Key><Parameter>password</Parameter></Key>
</Get>
<Scope>environment</Scope>
</KeyValueMapOperations>Element What it does mapIdentifier=”Backend_Credentials”References the KVM we createdassignTo=”private.backend.username”Stores the value in a flow variableScopeenvironment — accessible across all proxies💡The private. prefix is critical. Variables named private.xxx are automatically excluded from debug traces and analytics logs. Without it, your credentials appear in plain text during debugging. Always use private. for sensitive values.3.3 Inject Basic Auth headerAdd a Basic Authentication policy right after the KVM policy on TargetEndpoint PreFlow:Field Value Policy NameInjectBasicAuthStreamIncoming Request xml<BasicAuthentication async=”true” continueOnError=”false”
enabled=”true”
xmlns=”http://www.sap.com/apimgmt”>
<Operation>Encode</Operation>
<IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
<User ref=”private.backend.username”/>
<Password ref=”private.backend.password”/>
<AssignTo createNew=”true”>request.header.Authorization</AssignTo>
</BasicAuthentication>Element What it does Operation: EncodeBase64-encodes username:password into Authorization: Basic xxxUser ref / Password refReads from the flow variables set by the KVM policyAssignToWrites the encoded value into the outbound Authorization headerStep 4: Update, Save, Deploy.💡What this achieves: The consumer sends only an API key (and optionally an OAuth token). APIM internally reads the backend credentials from the KVM and injects the Authorization header before calling the backend. The consumer never sees or handles backend credentials. This decoupling of consumer identity from backend identity is fundamental.⚠️For Northwind specifically: Since Northwind is a public service, these backend credentials aren’t actually needed — Northwind accepts unauthenticated requests. But the pattern is identical for S/4HANA, where the Communication User credentials in the KVM authenticate against the Communication Arrangement. We’re building the pattern here so it’s ready when you swap the backend.🖼️ [Screenshot: TargetEndpoint PreFlow with KVM-GetCredentials and InjectBasicAuth policies]4. Spike Arrest — throttle traffic burstsWhere: ProxyEndpoint → PreFlow → Incoming Request (after authentication policies)Field Value Policy NameSpikeArrestStreamIncoming Request xml<SpikeArrest async=”true” continueOnError=”false”
enabled=”true”
xmlns=”http://www.sap.com/apimgmt”>
<Identifier ref=”request.header.APIKey”/>
<Rate>12pm</Rate>
<UseEffectiveCount>true</UseEffectiveCount>
</SpikeArrest>Element Value What it does Identifierrequest.header.APIKeyThrottle per Application — each consumer gets their own spike limitRate12pm12 per minuteUseEffectiveCounttrueCount across all APIM runtime nodes⚠️Understanding the smoothing: 12pm does NOT mean “allow 12 calls then block until the minute resets.” APIM distributes evenly: 12/min = one every 5 seconds. If two requests arrive within 1 second, the second is rejected — even if only 2 calls have happened the whole minute. This catches spikes, not aggregate overuse. For aggregate limits, use Quota (next section).Testing in Postman:Send 3 rapid requests to /V1/ProxyNorthwindAPI/Customers?$top=3&$format=json (with your API key). Click Send as fast as you can.Request 1: 200 OKRequest 2 or 3: json{
“fault”: {
“faultstring”: “Spike arrest violation. Allowed rate: 12pm”,
“detail”: {
“errorcode”: “policies.ratelimit.SpikeArrestViolation”
}
}
}HTTP Status: 429 Too Many RequestsWait 5 seconds, send again — 200 OK.💡For production, set this higher. 12pm is intentionally low for easy testing. Real-world values: 30ps (30 per second) or 1000pm depending on backend capacity.🖼️ [Screenshot: Postman showing the 429 SpikeArrestViolation response]5. Quota — cap total API callsWhile Spike Arrest handles bursts, Quota controls the total over a longer period.Where: ProxyEndpoint → PreFlow → Incoming Request (after SpikeArrest)Field Value Policy NameQuotaLimitStreamIncoming Request xml<Quota async=”true” continueOnError=”false”
enabled=”true” type=”calendar”
xmlns=”http://www.sap.com/apimgmt”>
<Identifier ref=”request.header.APIKey”/>
<Allow countRef=”apiproduct.developer.quota.limit” count=”1000″/>
<Interval ref=”apiproduct.developer.quota.interval”>1</Interval>
<Distributed>true</Distributed>
<StartTime>2025-01-01 00:00:00</StartTime>
<Synchronous>true</Synchronous>
<TimeUnit ref=”apiproduct.developer.quota.timeunit”>day</TimeUnit>
</Quota>⚠️Element order matters in APIM policy XML. The schema enforces a strict sequence: Allow → Interval → Distributed → StartTime → Synchronous → TimeUnit. If you put TimeUnit before StartTime, you’ll get: Invalid content was found starting with element ‘StartTime’. No child element is expected at this point. This is one of those errors where the XML looks correct but the parser rejects it — the elements are all valid, just in the wrong order.Element What it does countRef=”apiproduct.developer.quota.limit”Reads the limit from the Product’s Quota settings — different Products can have different limitscount=”1000″Fallback if the Product doesn’t define a quotaTimeUnit: dayCounter resets daily at midnightDistributed: trueCount across all APIM runtime nodes💡Spike Arrest vs Quota — the quick rule:Spike Arrest = speed limit (requests per second/minute) — prevents floodsQuota = data plan (total calls per day/month) — prevents overuseYou almost always want both.Testing: Temporarily set count=”5″, redeploy, and send 6 requests:Requests 1–5: 200 OKRequest 6: 429 QuotaViolation⚠️Remember to reset to count=”1000″ after testing.Dynamic Quota from the Product:The countRef attribute makes quotas dynamic. To configure it on the Product side:Go to Engage → select Northwind_Demo_Product → EditSet Calls: 5000, Interval: 1, Time Unit: DaySave and PublishNow a “Free” Product can have 100 calls/day and a “Premium” Product 10,000 — same proxy, same policy, different limits.6. JSON Threat Protection — block malicious payloadsFor APIs that accept POST or PUT (our Northwind_API_V1 supports POST on collections and PUT on single entities), protect against oversized or deeply nested payloads.Where: ProxyEndpoint → PreFlow → Incoming Request (after VerifyAPIKey, before SpikeArrest)Field Value Policy NameJSONThreatProtectionStreamIncoming Request xml<JSONThreatProtection async=”true” continueOnError=”false”
enabled=”true”
xmlns=”http://www.sap.com/apimgmt”>
<Source>request</Source>
<ArrayElementCount>50</ArrayElementCount>
<ContainerDepth>10</ContainerDepth>
<ObjectEntryCount>50</ObjectEntryCount>
<ObjectEntryNameLength>128</ObjectEntryNameLength>
<StringValueLength>5000</StringValueLength>
</JSONThreatProtection>⚠️Element order matters here too. <Source> must come first — before any of the limit elements. If you put it last, you’ll get: Invalid content was found starting with element ‘StringValueLength’. One of ‘Source’ is expected. This is the same strict-ordering pattern we saw with the Quota policy — always check the policy template for the correct element sequence.Element Limit Blocks ArrayElementCount50Arrays with 50+ items (memory-bomb payloads)ContainerDepth10Nesting deeper than 10 levels (stack overflow attacks)ObjectEntryCount50Objects with 50+ keysObjectEntryNameLength128Key names longer than 128 charactersStringValueLength5000Strings longer than 5,000 characters (content injection)SourcerequestApply to the incoming request body onlyTesting in Postman:Create a POST request to /V1/ProxyNorthwindAPI/Customers with an 11-level deep JSON body: json{
“a”: { “b”: { “c”: { “d”: { “e”: { “f”: { “g”: { “h”: { “i”: { “j”: { “k”: “too deep” } } } } } } } } } }
}Expected: 400 Bad Request json{
“fault”: {
“faultstring”: “JSONThreatProtection[JSONThreatProtection]: Exceeded container depth…”,
“detail”: {
“errorcode”: “steps.jsonthreatprotection.ExecutionFailed”
}
}
}💡This works together with the conditional flows from Part 2. The conditional flow for Customers (collection) allows POST. If the POST passes the conditional flow check but the JSON body is malicious, JSONThreatProtection catches it. Two layers: method enforcement + payload validation.🖼️ [Screenshot: Postman showing the 400 JSONThreatProtection violation]7. CORS — enable browser-based consumersIf a web application (SAP Build Apps, Fiori, React) calls your /V1/ProxyNorthwindAPI endpoint from a browser, it will fail with a CORS error. Browsers enforce the Same-Origin Policy — they block requests to different domains unless the response includes Access-Control-* headers.Where: ProxyEndpoint → PostFlow → Outgoing Response⚠️Why PostFlow Outgoing Response? CORS headers go on the response, not the request. And on the ProxyEndpoint (consumer-facing), because the browser’s CORS check happens between the consumer and APIM — not between APIM and Northwind.Field Value Policy NameAddCORSHeadersStreamOutgoing Response xml<AssignMessage async=”false” continueOnError=”false”
enabled=”true”
xmlns=”http://www.sap.com/apimgmt”>
<Set>
<Headers>
<Header name=”Access-Control-Allow-Origin”>*</Header>
<Header name=”Access-Control-Allow-Methods”>GET, POST, PUT, DELETE, OPTIONS</Header>
<Header name=”Access-Control-Allow-Headers”>APIKey, Content-Type, Authorization</Header>
<Header name=”Access-Control-Max-Age”>3600</Header>
</Headers>
</Set>
<IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
<AssignTo createNew=”false” type=”response”/>
</AssignMessage>Header Why it matters Allow-Origin: *Allow any domain. In production, restrict to specific origins like https://myapp.launchpad.cfapps.us10.hana.ondemand.comAllow-Headers: APIKey, …APIKey must be listed here — otherwise the browser strips it from the requestMax-Age: 3600Cache the preflight response for 1 hour⚠️OPTIONS preflight: Browsers send a preflight OPTIONS request before the real call. If VerifyAPIKey runs on OPTIONS too, it’ll reject the preflight (which has no API key). Use a conditional flow to skip key verification for OPTIONS requests — add the condition (request.verb != “OPTIONS”) around the VerifyAPIKey policy step.Step 5: Update, Save, Deploy.🖼️ [Screenshot: Policy Editor showing AddCORSHeaders on ProxyEndpoint PostFlow (Outgoing Response)]8. The complete policy lineup — what the export looks like nowAfter this post, your proxy has eight policies. Here’s the full picture:8.1 Policy summary table# Policy Location Stream Purpose 1VerifyAPIKeyProxyEndpoint PreFlowIncoming RequestIdentify the consumer (Application)2ValidateOAuthTokenProxyEndpoint PreFlowIncoming RequestAuthenticate the consumer (identity)3JSONThreatProtectionProxyEndpoint PreFlowIncoming RequestBlock malicious payloads4SpikeArrestProxyEndpoint PreFlowIncoming RequestThrottle traffic bursts5QuotaLimitProxyEndpoint PreFlowIncoming RequestCap daily API calls6AddCORSHeadersProxyEndpoint PostFlowOutgoing ResponseEnable browser access7KVM-GetCredentialsTargetEndpoint PreFlowIncoming RequestRead backend credentials8InjectBasicAuthTargetEndpoint PreFlowIncoming RequestAdd Authorization header to backend callPlus the auto-generated defaultRaiseFaultPolicy on the DefaultFaultFlow.8.2 What the export’s Policy folder looks like APIProxy/
├── Policy/
│ ├── defaultRaiseFaultPolicy.xml ← auto-generated (Part 2)
│ ├── VerifyAPIKey.xml ← added in Part 3
│ ├── ValidateOAuthToken.xml ← added in this post
│ ├── JSONThreatProtection.xml ← added in this post
│ ├── SpikeArrest.xml ← added in this post
│ ├── QuotaLimit.xml ← added in this post
│ ├── AddCORSHeaders.xml ← added in this post
│ ├── KVM-GetCredentials.xml ← added in this post
│ └── InjectBasicAuth.xml ← added in this post8.3 The main proxy XML’s policies section xml<policies>
<policy type=”RaiseFault”>defaultRaiseFaultPolicy</policy>
<policy type=”VerifyAPIKey”>VerifyAPIKey</policy>
<policy type=”OAuthV2″>ValidateOAuthToken</policy>
<policy type=”JSONThreatProtection”>JSONThreatProtection</policy>
<policy type=”SpikeArrest”>SpikeArrest</policy>
<policy type=”Quota”>QuotaLimit</policy>
<policy type=”AssignMessage”>AddCORSHeaders</policy>
<policy type=”KeyValueMapOperations”>KVM-GetCredentials</policy>
<policy type=”BasicAuthentication”>InjectBasicAuth</policy>
</policies>💡This is your proxy’s bill of materials. When you export the zip and check it into Git, you can see every policy, every placement, and every configuration. When a colleague asks “what governance does this proxy have?” — this list is the answer.⚠️Order matters within a PreFlow. The <sequence> numbers determine execution order. If you put SpikeArrest (sequence 4) before VerifyAPIKey (sequence 1), you’d count rate limits for unauthorized requests — wasting your spike budget on junk traffic. Always: authenticate → validate → throttle → route.9. Update your Policy TemplateIn Part 3, we created Baseline_Security_Template with just VerifyAPIKey. Let’s expand it to the full baseline:Step 1: Go to Develop → Policy Templates → open Baseline_Security_Template.Step 2: Add these policies in order on ProxyEndpoint PreFlow (Incoming Request):VerifyAPIKey (already there)JSONThreatProtectionSpikeArrest (Rate: 30ps — more realistic than 12pm)QuotaLimit (with countRef for dynamic Product limits)Step 3: Add on ProxyEndpoint PostFlow (Outgoing Response): 5. AddCORSHeadersStep 4: Save.Now every new proxy gets the full security + traffic + CORS baseline in one click. Five policies, correct placement, sensible defaults.💡SAP recommends a baseline template for all proxies: Verify API Key + JSON/XML Threat Protection + Regular Expression Protection + Spike Arrest + Quota. Our template covers four of five — add Regular Expression Protection if your APIs accept user input in URLs or headers.10. Troubleshooting referenceError HTTP Which policy Fix FailedToResolveAPIKey401VerifyAPIKeyConsumer didn’t send the APIKey headerInvalidApiKey401VerifyAPIKeyWrong key, expired, or not subscribed to the ProductInvalidAccessToken401OAuth v2.0Token is invalid, expired, or from an untrusted issueraccess_token_expired401OAuth v2.0Token has expired — consumer needs to refreshSpikeArrestViolation429SpikeArrestToo many requests too fast — space them out or raise the rateQuotaViolation429QuotaDaily limit hit — upgrade Product tier or wait for midnight resetJSONThreatProtection: Exceeded…400JSONThreatProtectionPayload too deep/large — consumer must simplifyUnresolvedVariable: private.backend.username500KVM-GetCredentialsKVM name mismatch (mapIdentifier doesn’t match the KVM name) or key doesn’t existBasicAuthentication: Unable to Encode500InjectBasicAuthKVM values are empty or variable names don’t match between KVM and BasicAuth policiesInvalid content was found starting with element…(save error)Any policyXML element order is wrong — APIM enforces strict element sequence. Check the policy template for the correct orderClient identifier is required401OauthServiceclient_id and client_secret must be in the body (x-www-form-urlencoded), not in headersUnresolved variable: private.backend.username500KVM-GetCredentialsKVM not created, or mapIdentifier doesn’t match the KVM name. For Northwind (no auth), remove KVM + BasicAuth policies from TargetEndpoint entirely💡Use the Debug tool (Part 3, section 😎 to pinpoint which policy failed. The trace shows each policy step in sequence — you see exactly where the flow stopped and what variables were set. When KVM-GetCredentials succeeds but InjectBasicAuth fails, it means the variable names don’t match between the two policies.Quick ReferenceItem Value OAuth 2.0ProxyEndpoint PreFlow · <Operation>VerifyAccessToken</Operation>Token Content-Typeapplication/x-www-form-urlencoded (NOT JSON)KVMConfigure → Key Value Maps → Create (Encrypted) · private. prefix for variablesBasic Auth (backend)TargetEndpoint PreFlow · reads from KVM · writes to request.header.AuthorizationSpike ArrestProxyEndpoint PreFlow · <Rate>12pm</Rate> (testing) or <Rate>30ps</Rate> (production)QuotaProxyEndpoint PreFlow · countRef=”apiproduct.developer.quota.limit” for dynamic limitsJSON Threat ProtectionProxyEndpoint PreFlow · limits depth (10), arrays (50), strings (5000)CORSProxyEndpoint PostFlow (Outgoing Response) · AssignMessage with Access-Control-* headersExport structure9 policy files in Policy/ folder after this postWhat’s nextIn Part 5: Advanced Topics — Routing, Path Removal, Developer Hub, Analytics & MCP Gateway, we tackle the patterns that separate beginners from practitioners. We’ll route requests to different backends using policies, rewrite URL paths for clean consumer-facing URLs, walk through the Developer Hub from a consumer’s perspective, set up Analytics dashboards with custom metrics via Statistics Collector, and close the series with the brand-new MCP Gateway for AI agents.👉Part 5: Advanced Topics — coming next.   Read More Technology Blog Posts by Members articles 

#SAP

#SAPTechnologyblog

You May Also Like

More From Author