Orchestrate

SAP BTP foundations for AI builders

Understand how SAP BTP accounts, runtimes, service keys, destinations and roles fit together, and read your trial's destinations from Python the way an AI app does.

Updated Sep 30, 2026Foundational 8 minDeep 40 min
Foundational layer · 8 min read

The 60-second version

SAP Business Technology Platform (BTP) is where SAP customers build and run their own extensions and AI services. Almost every SAP AI project touches it, even when the AI itself is bought, not built.

Four ideas explain most of what happens in a BTP project:

  1. Accounts. A global account is the contract with SAP. Subaccounts inside it are the rooms where work happens, each in one region.
  2. Runtimes. A subaccount can run apps in Cloud Foundry, in Kyma (Kubernetes) or in the ABAP environment.
  3. Services and keys. Teams switch on services such as SAP AI Core. Each service hands out credentials, called a service key or a binding, that an app uses to prove who it is.
  4. Connections and roles. Destinations store where a backend system lives and how to log on to it. The Cloud Connector reaches systems inside the company network. Role collections decide what each person may do.

An AI assistant that explains blocked sales orders needs all four. It runs in a subaccount, calls an AI service with a key, reads S/4HANA through a destination, and must respect the user's roles.

Why it matters to the business

BTP decisions end up in the budget, the risk register and the timeline:

  • Cost. In a paid BTP account, what a subaccount is entitled to use drives the bill. A clean split into subaccounts (for example development, test and production) makes costs traceable to a project.
  • Risk. Service keys are passwords for machines. A key pasted into a chat, a slide or a code repository gives anyone who finds it access to that service. It is an avoidable risk.
  • Speed. AI pilots can stall for weeks on plumbing, not on the model. Typical blockers are "we have no destination to the S/4HANA system", "the Cloud Connector isn't installed" and "nobody owns the role collections". Asking early saves the timeline.
  • Data residency. Each subaccount lives in one region. Where the AI service runs, and where the data flows, is decided when the subaccount is created.

Example: a team wants an assistant that tells order-to-cash clerks why a sales order is blocked for credit. The model is the easy part. The team also needs a subaccount in the right region, an AI service entitlement, a destination to S/4HANA through the Cloud Connector, and a rule that the assistant only shows orders the clerk may already see.

How SAP does it

As of September 2026, SAP describes BTP with these building blocks:

  • Global account, directories and subaccounts. The global account is "the realization of a contract" with SAP. Directories optionally group subaccounts. Each subaccount is tied to one region.
  • Entitlements and quotas. An entitlement is the right to use a service plan; the quota is how much. Both are bought for the global account and handed down to subaccounts.
  • Runtime environments. Cloud Foundry for new business apps and services, Kyma for containers and serverless functions on managed Kubernetes, and the ABAP environment for ABAP-based extensions. The older Neo environment is only for existing customers.
  • Security. The SAP Authorization and Trust Management service (often called XSUAA) issues tokens and groups roles into role collections. It trusts an identity provider, such as SAP Cloud Identity Services, to log users on.
  • Connectivity. The Destination service stores connection details. The Connectivity service and the SAP Cloud Connector reach systems in the company network.
  • AI services. SAP AI Core, including the generative AI hub, is a BTP service. Apps call it with credentials from a service key, like any other service. SAP offers a 30-day basic trial of the generative AI hub; after that it needs Pay-As-You-Go or an enterprise agreement. Unit 5 covers it in depth.

The free 90-day BTP trial from the Unit 3 setup has the same structure, which is why this course uses it.

Who owns what in a BTP AI project

Most confusion in BTP projects comes from unclear ownership. Use this table in the kick-off meeting.

BTP piece What it is Typical owner What goes wrong if nobody owns it
Global account The contract with SAP Procurement and the BTP platform team Nobody knows what has been bought
Subaccounts and regions Where apps and AI services run Platform team, with the architect Production data in a test room, or the wrong region
Entitlements Which services each subaccount may use Platform team, approved by budget owner Surprise bills, or a pilot blocked for weeks
Service keys Machine credentials for a service The app team, under security rules Keys leak into repositories and chats
Destinations Where a backend lives and how to log on Integration team The AI app can't reach S/4HANA
Cloud Connector Secure link into the company network Basis or infrastructure team No access to on-premise systems at all
Role collections What each person may do Security and authorization team Users see data they shouldn't

Questions to ask

  • Which subaccount will this AI use case run in, in which region, and who approved that?
  • Do we have the entitlements for the AI services we need, or do they have to be bought first?
  • Where will service keys be stored, and who can see them? How often are they replaced?
  • Is there a destination to the S/4HANA system, and does it use the user's identity or a shared technical user?
  • Is the Cloud Connector installed and maintained, and who owns it?
  • Which role collections will users get, and who reviews them?
  • Are development, test and production separate subaccounts?

Common misconceptions

  • "BTP is one product." It is a platform of many services, each with its own plans and prices. Two teams can both "use BTP" and share nothing.
  • "The AI model decides what users can see." It doesn't. Authorizations, destinations and role collections decide. The model only sees what the app hands it.
  • "A service key is just configuration." It is a password for a machine. Treat it like one.
  • "Our S/4HANA is on-premise, so BTP can't reach it." It can, through the Cloud Connector, which opens the link from inside the company network.
  • "The trial is a small production account." SAP rules out production and team use for trials, and deletes them after 90 days.

Key terms

  • Global account: the contract with SAP; holds subaccounts, entitlements and quotas.
  • Subaccount: a space in one region where apps run and services are used.
  • Entitlement and quota: the right to use a service plan, and how much of it.
  • Runtime environment: where apps run: Cloud Foundry, Kyma or ABAP.
  • Service instance: one copy of a service, created in a subaccount.
  • Service key / binding: credentials for a service instance; a key for outside callers, a binding for a deployed app.
  • Destination: a stored connection to another system: URL, proxy type and logon method.
  • Cloud Connector: software inside the company network that links on-premise systems to BTP.
  • Role collection: a bundle of roles assigned to users or groups.

Check yourself

Pick one answer for each question. The explanation appears after you choose.
  1. 1In BTP terms, what is a global account?

    Answer: B. SAP describes the global account as the realization of a contract. Subaccounts, entitlements and quotas hang below it; the work itself happens in subaccounts.
  2. 2Why does the choice of subaccount matter for an AI use case?

    Answer: C. Each subaccount is tied to one region and receives its own entitlements. A clean split keeps data residency, costs and test versus production under control.
  3. 3A developer pastes a service key into a team chat. What is the risk?

    Answer: D. A service key holds the client ID and secret an app uses to get tokens. It works like a password for a machine, so it belongs in a secure store, not in chats, slides or code.
  4. 4Your S/4HANA runs in your own data center. How does a BTP AI app reach it?

    Answer: B. A destination with an on-premise proxy type routes calls through the Connectivity service and the Cloud Connector, which runs inside the company network. The common belief that on-premise systems are out of reach is wrong.
  5. 5What decides which sales orders an AI assistant may show a clerk?

    Answer: C. The model only sees what the app hands it. Role collections in BTP and authorizations in S/4HANA decide what a user may see; ask whether the destination uses the user's identity or a shared technical user.
  6. 6A pilot has been stuck for three weeks. Which question most likely finds the blocker?

    Answer: D. Pilots can stall on plumbing, not the model: a missing entitlement, no destination to S/4HANA, or no Cloud Connector. Owners for each BTP piece prevent that.
  7. 7How can a team try SAP's generative AI hub before buying?

    Answer: A. As of September 2026, SAP offers a 30-day basic trial of the generative AI hub. Continued use needs Pay-As-You-Go for SAP BTP or an enterprise agreement.
Deep layer · 40 min read

Mental model: every call is "who are you, where are you going, what may you do"

Strip away the product names and every call an AI app makes on BTP answers three questions:

  1. Who are you? The app proves its identity with credentials from a service key or binding and gets a short-lived token.
  2. Where are you going? A destination says which system to call, at which URL, through which network path.
  3. What may you do? The token carries scopes, and the target system checks the user's authorizations.

The account model (global account, subaccounts, entitlements) decides where all of this is allowed to exist. Once you see those three questions, the Destination service, SAP AI Core and your own apps all look the same: get a token, call a URL, get checked.

flowchart LR
  K[Service key<br/>client ID + secret] --> A[Authorization server<br/>XSUAA]
  A -->|token| APP[Your AI app]
  APP -->|Bearer token| D[Destination service]
  D -->|URL + logon info| APP
  APP --> S[S/4HANA, via<br/>Cloud Connector]
  APP --> AI[SAP AI Core]

How it works

The account model, from the builder's side

You met the structure in the Unit 3 setup. Here is what each level means when you build:

Level What you do there Builder's rule of thumb
Global account Nothing day to day; it holds the contract, entitlements and quotas You rarely have admin rights here in a customer landscape
Directory (optional) Groups subaccounts, up to 7 levels deep, and can hand out entitlements Often one per business unit or project
Subaccount Create service instances, deploy apps, create destinations, assign role collections Your daily workplace; one region each
Cloud Foundry org and space Deploy apps and create service instances One org per subaccount; spaces such as dev separate work

Entitlements are the bridge between contract and code. If a service doesn't appear in the Service Marketplace, or a plan is greyed out, the subaccount usually lacks the entitlement. In a customer account, you ask the platform team; in your trial, you can add it yourself under Entitlements.

SAP also lets admins attach labels to subaccounts, directories, subscriptions and service instances, for example cost-center or use-case. They make an AI landscape easier to filter and audit.

Runtime environments

Environment What SAP says it is for When an AI builder picks it
Cloud Foundry New business apps and services, many languages through buildpacks Python or Node.js APIs, CAP apps; the default in this course
Kyma Managed Kubernetes for containerized microservices and serverless functions Teams already on containers, or needing custom images
ABAP environment Extensions for ABAP-based products; cloud-enabled, runs within Cloud Foundry ABAP teams extending S/4HANA the clean-core way
Neo Older environment, only for existing customers Never for new work

Unit 6 deploys an AI API to Cloud Foundry.

Service instances, bindings and service keys

A service in the marketplace is a template. When you create an instance, you get your own copy with its own credentials. There are two ways to hand those credentials out:

  • Binding: you bind an instance to an app deployed on BTP. The platform passes the credentials to the app through its environment. No human copies anything.
  • Service key: you create credentials by hand, for a caller outside that app: your laptop, a test tool, another system. The Cloud Foundry documentation says service keys are for "manually configuring consumers" of marketplace services.

A service key is a small JSON document. For the Destination service, SAP's documentation names four values you need:

Field What it is
clientid The app's user name at the authorization server
clientsecret The app's password; secret
url The authorization server that issues tokens
uri The address of the Destination service itself

Other services use the same pattern with different extra fields. For SAP AI Core, SAP's Python SDK maps clientid, clientsecret, url and serviceurls.AI_API_URL from the service key to its settings.

Tokens: OAuth 2.0 client credentials

BTP services don't accept the secret on every call. The app first swaps it for a token, using the OAuth 2.0 client credentials flow:

sequenceDiagram
  participant App as Your script
  participant UAA as Authorization server (url)
  participant Dest as Destination service (uri)
  App->>UAA: POST /oauth/token (client_credentials, client ID, secret)
  UAA-->>App: access_token (a JWT), expires_in
  App->>Dest: GET /destination-configuration/v1/subaccountDestinations<br/>Authorization: Bearer token
  Dest-->>App: JSON list of destinations

The token is a JWT (JSON Web Token): three parts separated by dots. The middle part holds claims, such as which client it was issued to, when it expires and which scopes it grants. The authorization server signs it, so services can check it hasn't been changed. Anyone holding the token can use it until it expires, so treat it like the secret.

The authorization server here is the SAP Authorization and Trust Management service (XSUAA). SAP's documentation describes how it separates two kinds of callers:

  • Technical clients, like your script, get credentials directly from the service. That is the flow above.
  • Business users log on through a trusted identity provider, such as SAP Cloud Identity Services. Their token carries their own scopes, which come from the roles in their role collections.

For AI apps this difference is the whole security story. A technical client sees whatever its credentials allow. A token that carries the user's identity lets the backend apply that user's authorizations. Unit 11 builds on this.

Destinations

A destination is a named connection stored in the subaccount. It holds at least a name, a type (usually HTTP), a URL, a proxy type and an authentication method. Apps ask the Destination service for a destination by name, so URLs and passwords stay out of the code and can differ between test and production.

The proxy type decides the network path:

Proxy type Meaning
Internet The target is on the public internet, for example an SAP cloud API
OnPremise The target is inside the company network; calls go through the Connectivity service and the SAP Cloud Connector
PrivateLink The target is reached over a private link; the flow is the same as Internet

The authentication method decides how the destination logs on. NoAuthentication is the simplest. Customer landscapes use methods such as OAuth flows or principal propagation, which passes the user's identity through to S/4HANA so its authorizations apply.

The SAP Cloud Connector is software that runs inside the customer's network. For OnPremise destinations, the Connectivity service routes the call through it to the internal system. This is how an AI app on BTP reads blocked sales orders from an on-premise S/4HANA.

Tools you will meet

You can do everything in this topic in the cockpit. Teams automate it with two command-line tools:

  • The btp CLI manages accounts, for example btp list accounts/subaccount after btp login --sso.
  • The Cloud Foundry CLI works inside a space, for example cf create-service-key MY-SERVICE MY-KEY.

Later units install them when they're needed.

Build it yourself: read your trial's destinations from Python

You will create a Destination service instance in your trial, make a service key, and create one destination. Then a Python script swaps the key for a token and asks the Destination service for your destinations. It is the same pattern your AI apps will use for SAP AI Core in Unit 5.

Before you start: complete Set up your computer for this course and Set up for Unit 3. They give you the orchestrate-course folder with its .venv, requests, python-dotenv, a .env file listed in .gitignore, and your SAP BTP trial with unit03/btp_trial_notes.md.

flowchart LR
  E[.env<br/>4 values from<br/>the service key] --> P[btp_destinations.py]
  P -->|1. get token| U[Trial authorization server]
  P -->|2. list destinations| D[Destination service]
  D --> O[Table on screen]

What you need

  • Your SAP BTP trial account from the Unit 3 setup. If it has expired or been suspended, create a new one (Unit 3 setup, Step 7).
  • Your course folder and .venv.
  • About 45 minutes.
  • Cost: free. The trial has no charges.
  • No account? Run the script with --sample in Step 6 and read along. You will see the same output shape with made-up data.

Step 1: Open your course folder and turn on the environment

  1. Open VS Code, choose File > Open Folder, and open orchestrate-course.

  2. Open a terminal: Terminal > New Terminal.

  3. If the prompt doesn't start with (.venv), turn it on:

    • Windows (PowerShell):

      .venv\Scripts\Activate.ps1
    • macOS / Linux:

      source .venv/bin/activate
  4. Check the two libraries are there:

    pip install requests python-dotenv

    Lines saying Requirement already satisfied are fine.

Step 2: Save the script

  1. In VS Code's file list, right-click unit03, choose New File, and name it btp_destinations.py.
  2. Paste the code below and save.
"""Read the destinations in your SAP BTP trial subaccount, the way an AI app does it.

The flow: service key values -> OAuth token from your subaccount's authorization server
-> call the Destination service REST API with that token -> print what it returns.

How to run (from the unit03 folder, with the course .venv turned on):
    python btp_destinations.py --sample              # no account needed: made-up data, no network
    python btp_destinations.py                       # your trial: reads the four DEST_ values from .env
    python btp_destinations.py --name Northwind      # details of one destination
    python btp_destinations.py --name Northwind --call "/V2/Northwind/Northwind.svc/Customers?$top=2&$format=json"
                                                     # also call the target system through that destination

Secrets stay in .env. The script never prints the client secret or the full token.
"""
import argparse
import base64
import json
import os
import sys
import time

SECRET_WORDS = ("password", "secret", "apikey", "token")  # property names we never print (URLs are fine)

SAMPLE_DESTINATIONS = [  # made-up, shaped like the Destination service's response
    {"Name": "Northwind", "Type": "HTTP", "URL": "https://services.odata.org",
     "ProxyType": "Internet", "Authentication": "NoAuthentication",
     "Description": "Northwind OData services"},
    {"Name": "S4_SALES_ORDERS", "Type": "HTTP", "URL": "http://s4h-virtual:443",
     "ProxyType": "OnPremise", "Authentication": "PrincipalPropagation",
     "Description": "Made-up example: S/4HANA behind a Cloud Connector"},
    {"Name": "AI_CORE", "Type": "HTTP", "URL": "https://api.ai.example.invalid",
     "ProxyType": "Internet", "Authentication": "OAuth2ClientCredentials",
     "clientId": "sb-made-up", "clientSecret": "never-printed",
     "tokenServiceURL": "https://example.invalid/oauth/token",
     "Description": "Made-up example: an AI service called with its own client"},
]


def fake_jwt(claims: dict) -> str:
    """Build an unsigned token with the same three-part shape as a real one (sample mode only)."""
    def part(obj: dict) -> str:
        raw = json.dumps(obj).encode()
        return base64.urlsafe_b64encode(raw).decode().rstrip("=")
    return f"{part({'alg': 'none', 'typ': 'JWT'})}.{part(claims)}.sample-signature"


def read_claims(token: str) -> dict:
    """Decode the middle part of a JWT to read its claims. This does NOT verify the signature."""
    try:
        payload = token.split(".")[1]
        payload += "=" * (-len(payload) % 4)  # base64 needs padding to a multiple of 4
        return json.loads(base64.urlsafe_b64decode(payload))
    except (IndexError, ValueError):
        return {}


def settings_from_env() -> dict:
    """Read the four service key values from .env (or the environment)."""
    try:
        from dotenv import load_dotenv
        load_dotenv()
    except ImportError:
        pass  # python-dotenv missing: fall back to real environment variables
    names = ["DEST_CLIENT_ID", "DEST_CLIENT_SECRET", "DEST_AUTH_URL", "DEST_SERVICE_URI"]
    values = {n: os.getenv(n, "").strip() for n in names}
    missing = [n for n, v in values.items() if not v]
    if missing:
        sys.exit("Missing in .env: " + ", ".join(missing)
                 + "\nCopy them from your service key (Step 4), or run with --sample.")
    return values


def get_token(session, s: dict) -> dict:
    """Ask the subaccount's authorization server for a token (OAuth 2.0 client credentials)."""
    url = s["DEST_AUTH_URL"].rstrip("/") + "/oauth/token"
    form = {"grant_type": "client_credentials",
            "client_id": s["DEST_CLIENT_ID"], "client_secret": s["DEST_CLIENT_SECRET"]}
    reply = session.post(url, data=form, timeout=30)
    if reply.status_code == 401:
        sys.exit("401 from the token endpoint: client ID or secret is wrong. Copy them again (Step 4).")
    reply.raise_for_status()
    return reply.json()


def list_destinations(session, s: dict, token: str) -> list:
    """GET <uri>/destination-configuration/v1/subaccountDestinations with the token."""
    url = s["DEST_SERVICE_URI"].rstrip("/") + "/destination-configuration/v1/subaccountDestinations"
    reply = session.get(url, headers={"Authorization": f"Bearer {token}"}, timeout=30)
    if reply.status_code in (401, 403):
        sys.exit(f"{reply.status_code} from the Destination service: the token was refused. "
                 "Check that all four DEST_ values come from the same service key.")
    reply.raise_for_status()
    return reply.json()


def show_token(token: str, expires_in) -> None:
    claims = read_claims(token)
    scopes = claims.get("scope", [])
    print(f"  token: {token[:12]}... ({len(token)} characters, valid {expires_in} seconds)")
    print(f"  issued to client: {claims.get('client_id', '?')}")
    print(f"  scopes: {len(scopes)} ({', '.join(scopes[:3])}{', ...' if len(scopes) > 3 else ''})")


def show_table(destinations: list) -> None:
    if not destinations:
        print("  No destinations yet. Create one in the cockpit (Step 5), then run again.")
        return
    print(f"  {'Name':<18} {'Proxy':<10} {'Authentication':<26} URL")
    for d in destinations:
        print(f"  {d.get('Name', '?'):<18} {d.get('ProxyType', '?'):<10} "
              f"{d.get('Authentication', '?'):<26} {d.get('URL', '?')}")


def show_one(destination: dict) -> None:
    for prop, value in destination.items():
        hidden = any(w in prop.lower() for w in SECRET_WORDS) and not prop.lower().endswith("url")
        print(f"  {prop:<18} {'(hidden)' if hidden else value}")


def call_target(session, destination: dict, path: str) -> None:
    """Call the target system through a destination. Only simple Internet destinations."""
    if destination.get("ProxyType") != "Internet" or destination.get("Authentication") != "NoAuthentication":
        print("  --call only handles Internet destinations with NoAuthentication. Others need the "
              "Connectivity service or a token flow, which later units cover.")
        return
    url = destination["URL"].rstrip("/") + path
    print(f"  GET {url}")
    try:
        reply = session.get(url, timeout=30)
    except Exception as err:  # network, proxy or DNS problem on the target side
        print(f"  Could not reach the target ({type(err).__name__}). Try another network.")
        return
    print(f"  status {reply.status_code}, {len(reply.content)} bytes")
    print("  " + reply.text[:300].replace("\n", " "))


def main() -> None:
    parser = argparse.ArgumentParser(description="Read SAP BTP destinations with a service key.")
    parser.add_argument("--sample", action="store_true", help="use made-up data, no account or network")
    parser.add_argument("--name", help="show the details of one destination")
    parser.add_argument("--call", metavar="PATH", help="with --name: GET this path on the destination's URL")
    args = parser.parse_args()

    if args.sample:
        print("SAMPLE MODE: made-up data, nothing leaves your computer.")
        print("\n1. Token (OAuth 2.0 client credentials)")
        claims = {"client_id": "sb-made-up-client-id",
                  "scope": ["made-up.scope"], "exp": int(time.time()) + 43199}
        show_token(fake_jwt(claims), 43199)
        destinations = SAMPLE_DESTINATIONS
        session = None
    else:
        try:
            import requests
        except ImportError:
            sys.exit("The requests library is missing. Run: pip install requests python-dotenv")
        s = settings_from_env()
        session = requests.Session()
        try:
            print("1. Token (OAuth 2.0 client credentials)")
            reply = get_token(session, s)
            token = reply["access_token"]
            show_token(token, reply.get("expires_in", "?"))
            destinations = list_destinations(session, s, token)
        except requests.exceptions.ConnectionError as err:
            sys.exit(f"Could not connect ({type(err).__name__}). Check the URLs in .env and your "
                     "network or proxy. --sample still works.")
        except requests.exceptions.HTTPError as err:
            sys.exit(f"The server answered with an error: {err}")

    print(f"\n2. Destinations in the subaccount: {len(destinations)}")
    show_table(destinations)

    if args.name:
        match = [d for d in destinations if d.get("Name", "").lower() == args.name.lower()]
        if not match:
            sys.exit(f"\nNo destination named {args.name}. Names are listed above.")
        print(f"\n3. Details of {match[0]['Name']}")
        show_one(match[0])
        if args.call:
            print("\n4. Calling the target system")
            if session is None:
                print("  Skipped in sample mode (no network).")
            else:
                call_target(session, match[0], args.call)


if __name__ == "__main__":
    main()

Step 3: Run it with sample data first

This proves the script works before you touch the cockpit.

  1. Go into the unit03 folder:

    cd unit03
  2. Run:

    python btp_destinations.py --sample

What success looks like:

SAMPLE MODE: made-up data, nothing leaves your computer.

1. Token (OAuth 2.0 client credentials)
  token: eyJhbGciOiAi... (169 characters, valid 43199 seconds)
  issued to client: sb-made-up-client-id
  scopes: 1 (made-up.scope)

2. Destinations in the subaccount: 3
  Name               Proxy      Authentication             URL
  Northwind          Internet   NoAuthentication           https://services.odata.org
  S4_SALES_ORDERS    OnPremise  PrincipalPropagation       http://s4h-virtual:443
  AI_CORE            Internet   OAuth2ClientCredentials    https://api.ai.example.invalid

The token length may differ slightly. The three destinations show the three situations you will meet: a public API, an on-premise S/4HANA behind a Cloud Connector, and an AI service with its own credentials. All three are made up.

  1. Look at one destination in detail:

    python btp_destinations.py --sample --name AI_CORE

    The last lines show clientSecret (hidden). The script never prints properties with password, secret, apikey or token in their names, except URLs.

Step 4: Create a Destination service instance and a service key

  1. Open your trial cockpit (your bookmark from the Unit 3 setup) and click the trial subaccount tile.
  2. In the left menu, choose Services > Service Marketplace.
  3. Type Destination in the search box and click the Destination Service tile.
  4. Click Create (some screens say Create Instance).
  5. Keep the default service and plan. If the dialog asks for a Runtime Environment, choose Cloud Foundry and the space dev.
  6. For Instance Name, type course-destination, then click Create.
  7. In the confirmation dialog, click View Instance. You are now in Instances and Subscriptions.
  8. Click the three dots (...) at the end of the course-destination row and choose Create Service Key.
  9. For the key name, type course-destination-key, then click Create.
  10. Click the three dots again next to the key (or the key name itself) and choose View. You see JSON.
  1. In VS Code, open .env in your course folder (not in unit03) and add four lines. Copy each value from the JSON, without the quotes:

    DEST_CLIENT_ID=value of "clientid"
    DEST_CLIENT_SECRET=value of "clientsecret"
    DEST_AUTH_URL=value of "url"
    DEST_SERVICE_URI=value of "uri"
  2. Save .env.

Step 5: Create a destination in the cockpit

You will create a destination to Northwind, a public demo OData service that SAP's own tutorials use.

  1. In the trial subaccount's left menu, open Connectivity > Destinations (in some layouts just Destinations).

  2. Click Create Destination (older screens: New Destination). If asked, choose From Scratch.

  3. Fill in the form:

    Field Value
    Name Northwind
    Type HTTP
    Description Northwind OData services
    URL https://services.odata.org
    Proxy Type Internet
    Authentication NoAuthentication
  4. Click Save.

  5. Click Check Connection. A message that the connection works means BTP can reach Northwind.

Step 6: Read your destinations from Python

  1. Make sure you are in unit03 and (.venv) shows.

  2. Run:

    python btp_destinations.py

What success looks like:

1. Token (OAuth 2.0 client credentials)
  token: eyJ... (... characters, valid ... seconds)
  issued to client: sb-...
  scopes: ... (...)

2. Destinations in the subaccount: 1
  Name               Proxy      Authentication             URL
  Northwind          Internet   NoAuthentication           https://services.odata.org

Your token length, client name, validity and scopes will differ. What matters: a token arrives, and the table lists the destination you created. If you see No destinations yet, the token worked but the subaccount has no destinations; go back to Step 5.

  1. Show the details and call Northwind through the destination:

    python btp_destinations.py --name Northwind --call '/V2/Northwind/Northwind.svc/Customers?$top=2&$format=json'

    The command is the same in PowerShell and on macOS/Linux. The single quotes stop the terminal from treating $top as a variable.

What success looks like (the end of the output):

4. Calling the target system
  GET https://services.odata.org/V2/Northwind/Northwind.svc/Customers?$top=2&$format=json
  status 200, ... bytes
  {"d" : {"results": [ ...

Your laptop called Northwind directly, using the URL the destination stored. A BTP app does the same, but the Destination service can also hand it logon details or route it through the Cloud Connector.

What each part of the script does

Part What it does
settings_from_env() Reads the four DEST_ values from .env and stops with a clear message if one is missing
get_token() Posts to <url>/oauth/token with grant_type=client_credentials, client ID and secret; returns the token
read_claims() Decodes the middle part of the JWT to show who it was issued to and its scopes. It doesn't check the signature; services do that
list_destinations() Calls <uri>/destination-configuration/v1/subaccountDestinations with Authorization: Bearer <token>
show_one() Prints one destination's properties and hides anything that looks secret
call_target() For Internet destinations with NoAuthentication only, calls the stored URL plus your path
--sample Uses a made-up token and destinations; no account or network
SECRET_WORDS The property-name parts the script refuses to print

Step 7: Update your notes and save your work

  1. Open unit03/btp_trial_notes.md and add:

    ## Destination service
    - Instance name: course-destination
    - Service key name: course-destination-key (values in .env only)
    - Destinations: Northwind (Internet, NoAuthentication)
    - Token valid for (seconds):
  2. Fill in the token validity from Step 6.

  3. From the course folder, check Git won't pick up your secrets, then commit:

    cd ..
    git status

    .env must not appear in the list. If it does, stop and add .env to .gitignore first.

    git add unit03/btp_destinations.py unit03/btp_trial_notes.md
    git commit -m "Read BTP destinations with a service key"

If something goes wrong

What you see What it means What to do
python is not recognized, or command not found Python isn't installed, or the terminal can't find it Windows: repeat Unit 1, Step 1, then open a new terminal. macOS/Linux: use python3 until .venv is active
The requests library is missing or ModuleNotFoundError A library isn't installed in this Python Check (.venv) shows, then run pip install requests python-dotenv
Missing in .env: DEST_... A value is missing, or you ran the script from a folder where .env isn't found Check .env is in the course folder and has all four lines. load_dotenv() searches upward from unit03, so it should find it
401 from the token endpoint Client ID or secret is wrong, or has extra quotes or spaces Copy them again from the service key, without quotes
401 or 403 from the Destination service The token doesn't fit this service Make sure all four values come from the same service key, and DEST_SERVICE_URI is the uri value
Could not connect (ConnectionError) A URL is mistyped, or your network or proxy blocks it Check the URLs; try another network. --sample still works
No destinations yet The call worked, but there are no destinations Create one (Step 5)
No destination named ... The name doesn't match Names are listed in the table above the message; spelling counts, case doesn't
--call prints Could not reach the target Your network blocks services.odata.org Try another network. The destination steps still worked
The cockpit says the trial is suspended No sign-in for 30 days Create a new trial and repeat Steps 4 and 5
Windows: Activate.ps1 cannot be loaded PowerShell blocks scripts Run Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser, answer Y, and try again

The SAP way

What you just did by hand is what SAP's frameworks do for you in a deployed app.

In a deployed app: bindings, not keys

An app deployed to Cloud Foundry is bound to its Destination, XSUAA and AI Core instances. The platform passes the credentials through the environment, so there is no .env and nobody copies a secret. SAP's SDKs read those credentials, fetch tokens and look up destinations by name. The AI Core Python SDK, for example, can read its settings from a service key, from individual variables such as AICORE_CLIENT_ID and AICORE_BASE_URL, or from the bound environment (VCAP_SERVICES).

Reaching S/4HANA

For an S/4HANA system in the company network, the pattern is:

  1. The customer runs the SAP Cloud Connector in its network and connects it to the subaccount.
  2. A destination with proxy type OnPremise points to the S/4HANA system.
  3. The app calls through the Connectivity service. With principal propagation, the user's identity reaches S/4HANA, and S/4HANA's own authorizations decide what comes back.

A cloud system reachable on the internet needs no Cloud Connector; its destination uses proxy type Internet. Unit 7 covers grounding AI on SAP data with authorizations.

SAP AI Core

SAP AI Core, with the generative AI hub, is a BTP service like the Destination service: you get an entitlement, create an instance and a service key, then swap clientid and clientsecret for a token at url. The difference is the extra serviceurls.AI_API_URL, where the AI API lives. As of September 2026 SAP offers a 30-day basic trial of the generative AI hub on sap.com. Unit 5 covers plans, setup and the orchestration service.

Security and authorizations

  • XSUAA issues tokens and holds roles; admins group roles into role collections and assign them to users or identity-provider groups.
  • SAP Cloud Identity Services can act as the identity provider that logs users on; XSUAA trusts it.
  • Your script used a technical client. A user-facing AI app should, where possible, act with the user's identity so the backend's authorizations apply.

Build vs. SAP

Need Do it yourself (like today's script) Use SAP's tooling
Try an API from a laptop Service key in .env, requests, client credentials Overkill
App deployed on BTP Possible, but you reimplement token caching and destination lookup Bindings plus SAP Cloud SDK or CAP; the platform handles credentials
Reach on-premise S/4HANA Not realistic; the Connectivity proxy and Cloud Connector need BTP runtime support Destination with OnPremise, Connectivity service, Cloud Connector
User-level authorizations Hard and risky to build XSUAA with an identity provider, principal propagation
Call SAP AI Core Fine for learning: token plus HTTP SAP AI SDK reads the key or binding for you
Account setup at scale Clicking in the cockpit btp CLI or other automation, reviewed by the platform team

The rule: hand-rolled code is for learning and quick tests. Anything deployed uses bindings and SAP's libraries, so secrets never pass through people.

Production concerns

  • Secrets. Service keys are for people and outside systems; bindings are for deployed apps. Keep keys out of Git, chats, tickets and AI prompts. Delete keys you no longer use, and replace any key that may have leaked.
  • Least privilege. One service key per consumer, so you can revoke one without breaking others. Give technical clients only the scopes they need.
  • User identity. A shared technical user in a destination means the AI app sees everything that user sees. Prefer propagating the user's identity to S/4HANA, and log which user asked for what.
  • Separation. Use separate subaccounts for development, test and production, with separate destinations and keys. Never point a test app at production S/4HANA "just for a demo".
  • Region and data flow. Check the subaccount's region and where each AI service processes data before any real data moves.
  • Cost. Entitlements and quotas cap what a subaccount can consume. Labels such as use-case make AI costs traceable.
  • Clean core. BTP side-by-side extensions call S/4HANA through released APIs, instead of modifying the core system. Destinations and the Cloud Connector are how they get there. Unit 6 covers CAP and side-by-side extensions.
  • Operations. Tokens expire; code must fetch a new one, not store one. Trials stop apps daily; production doesn't, but certificates, keys and Cloud Connector versions need an owner.

Pitfalls

  • Committing .env or the service key JSON. Check git status before every commit.
  • Mixing values from two keys. Client ID from one key and URI from another gives 401 or 403 errors that look mysterious.
  • Printing the token "for debugging". Logs live long. Print a prefix and length, as the script does.
  • Treating decoded claims as verified. Decoding a JWT is not checking it. Only the signature check proves it is genuine.
  • Hard-coding backend URLs. Use destinations, so test and production differ by configuration, not code.
  • Assuming the model filters data. Authorizations must be applied before data reaches the model.
  • Building in the trial for a customer. Trials are for learning, not team or production use.

Exercise: design the BTP landscape for the blocked-orders assistant

You will turn what you learned into a one-page landscape design for the running example: an assistant that explains why sales orders are blocked for credit. It feeds the solution design document in Unit 6.

  1. In VS Code, create unit03/btp_landscape.md.

  2. Add a heading ## Subaccounts and list three subaccounts (development, test, production), each with a region you would pick and one sentence on why.

  3. Add ## Entitlements and list the services each subaccount needs: at least Destination service, SAP AI Core, and SAP Authorization and Trust Management service. Mark any you are not sure about with (check).

  4. Add ## Destinations and describe one destination for S/4HANA in each subaccount: proxy type (OnPremise or Internet), and whether it uses the user's identity or a technical user. Write one sentence on why.

  5. Add ## Keys and bindings and say which consumers get service keys (for example, a developer laptop in development only) and which use bindings.

  6. Add ## Roles and name two role collections you would create, for example one for clerks and one for admins, and what each may do.

  7. Run your script once more and paste its table (not the token) under ## Evidence:

    python btp_destinations.py

    No trial? Run it with --sample and paste that table instead, marked "sample".

  8. Save your work:

    git add unit03/btp_landscape.md
    git commit -m "BTP landscape for the blocked-orders assistant"

Done when: btp_landscape.md has all six sections filled in, the evidence table shows at least one destination, git status doesn't list .env, and git log shows the commit.

Check yourself

Pick one answer for each question. The explanation appears after you choose.
  1. 1What is the difference between a binding and a service key?

    Answer: B. Binding an instance to a deployed app hands the credentials over through the app's environment. A service key creates credentials by hand for consumers outside that app, such as your laptop script.
  2. 2In btp_destinations.py, what does get_token() send to <url>/oauth/token?

    Answer: C. The client credentials flow swaps the client ID and secret for a short-lived access token. The script then sends only that token, as a Bearer header, to the Destination service.
  3. 3What does read_claims() prove about the token?

    Answer: D. Decoding a JWT just reads its claims. Only checking the signature proves it is genuine, and the services do that, not the script.
  4. 4Your script gets a token but the Destination service answers 401. What is the likeliest cause?

    Answer: A. A token from one key doesn't fit a service URI from another. No destinations gives an empty list, not a 401, and Northwind only matters for --call.
  5. 5An AI app on BTP must read sales orders from an on-premise S/4HANA. What do you set up?

    Answer: B. Calls to on-premise systems go through the Connectivity service and the SAP Cloud Connector, which the customer installs in its own network. The destination's proxy type is OnPremise.
  6. 6Why prefer principal propagation over a shared technical user for an AI assistant?

    Answer: C. With a technical user, the assistant sees everything that user can see. Propagating the user's identity lets S/4HANA's authorizations decide what reaches the model.
  7. 7A colleague's commit contains .env with a service key. What do you do first?

    Answer: D. Once a secret is in Git history, assume it is known. Deleting the service key retires those credentials; then fix .gitignore and store the new key's values only in .env.
  8. 8How does calling SAP AI Core differ from calling the Destination service?

    Answer: D. Both are BTP services with service keys. You swap clientid and clientsecret for a token at url; AI Core adds the address of its AI API in serviceurls.AI_API_URL.
  9. 9The Destination Service tile is missing from your trial's Service Marketplace. What is the likely fix?

    Answer: B. Services and plans a subaccount isn't entitled to don't show up or can't be used. In a trial you add the plan yourself under Entitlements; in a customer account you ask the platform team.

Sources

Sign in to track your progress

We'll email you a one-time sign-in link. No password needed.

or

Tell us a little about you

Optional, every field. It helps us pitch answers to your questions at the right level and decide which topics to write next. It is never shown publicly, and you can change or clear it anytime from the account menu.

SAP areas you work in