End-to-end: a secured API from gateway to portal¶
This tutorial connects three pieces: the Platform API control plane, a gateway, and the API Portal. You'll publish an API that requires both a subscription token and an API key on every call. Then you'll generate both credentials in the portal and watch the gateway accept them.
The point of the exercise is the seam in the middle. The portal never talks to the gateway. When a developer subscribes or generates a key, the portal fires a signed webhook to the Platform API, which persists the credential and pushes it to every gateway where the API is deployed. Getting that seam right is most of the work, and most of this tutorial.
What you'll build¶
API Portal ──signed webhook──▶ Platform API ──control plane──▶ Gateway
(issues the (verifies, decrypts, (enforces
credentials) persists, broadcasts) on each call)
▲
consumer's request ───────────┘
API-Key + Subscription-Key
Prerequisites¶
- Docker with the Compose plugin,
curl,unzip,jq, andopenssl - Free ports: 9543 (portal), 9243 (Platform API), 9090 and 9094 (gateway management and admin), 8081 (gateway API listener)
Step 1: Start the control plane and portal¶
The API Portal distribution ships the Platform API alongside it, so one compose file gives you both.
curl -sLO https://github.com/wso2/api-platform/releases/download/api-portal%2Fv1.0.0-rc/wso2apip-api-portal-1.0.0-rc.zip
unzip wso2apip-api-portal-1.0.0-rc.zip
cd wso2apip-api-portal-1.0.0
./scripts/setup.sh
docker compose up -d
setup.sh provisions the TLS certificate, encryption keys, the RS256 JWT keypair the two services share, and your admin credentials. Copy the admin password it prints—it's shown once.
Confirm both are up:
curl -fsk https://localhost:9243/health && echo " platform-api ok"
curl -fsk -o /dev/null https://localhost:9543/default/views/default && echo "api-portal ok"
Now get a Platform API token and create a project to hold the API. Every Platform API call below uses this token, and the portal accepts the same one—it verifies it against the shared public key.
export ADMIN_USERNAME=admin
export ADMIN_PASSWORD='<the password setup.sh printed>'
export TOKEN=$(curl -sk -X POST https://localhost:9243/api/portal/v0.9/auth/login \
-d "username=$ADMIN_USERNAME&password=$ADMIN_PASSWORD" | jq -r .token)
export PROJECT_ID=$(curl -sk -X POST https://localhost:9243/api/v0.9/projects \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"id":"e2e","displayName":"e2e","description":"End-to-end tutorial"}' | jq -r .id)
echo "project: $PROJECT_ID"
Step 2: Register the gateway, then start it¶
A gateway has to be registered with the Platform API before it can join. Registration returns an id; a second call mints the token the gateway authenticates with.
export GW_NAME=e2e-gateway
curl -sk -X POST https://localhost:9243/api/v0.9/gateways \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d "{\"id\":\"$GW_NAME\",\"displayName\":\"$GW_NAME\",\"endpoints\":[\"http://localhost:8081\"],\"functionalityType\":\"regular\"}" | jq -r .id
export GW_TOKEN=$(curl -sk -X POST https://localhost:9243/api/v0.9/gateways/$GW_NAME/tokens \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{}' | jq -r .token)
Download the gateway distribution and run its setup:
cd ..
curl -sLO https://github.com/wso2/api-platform/releases/download/gateway%2Fv1.2.0-rc/wso2apip-api-gateway-1.2.0-rc.zip
unzip wso2apip-api-gateway-1.2.0-rc.zip
cd wso2apip-api-gateway-1.2.0
./scripts/setup.sh
Point the controller at the control plane by adding these to api-platform.env, then start it:
cat >> api-platform.env <<EOF
APIP_GW_CONTROLLER_CONTROLPLANE_HOST=https://host.docker.internal:9243
APIP_GW_CONTROLLER_CONTROLPLANE_TOKEN=$GW_TOKEN
APIP_GW_CONTROLLER_CONTROLPLANE_GATEWAY_NAME=$GW_NAME
APIP_GW_CONTROLLER_CONTROLPLANE_INSECURE_SKIP_VERIFY=true
EOF
docker compose up -d
curl -fs http://localhost:9094/api/admin/v1/health && echo " gateway ok"
GATEWAY_NAME must match the id you registered, and INSECURE_SKIP_VERIFY is needed here only because setup.sh generated a self-signed certificate.
Note
host.docker.internal lets the gateway containers reach the Platform API published on your host. On Linux without that alias, put both stacks on one Docker network and use the service name instead.
Step 3: Connect the portal's webhooks to the Platform API¶
This is the seam. Two things have to be true before a portal-issued credential can reach the gateway.
First, link the portal's organization to the control plane. The Platform API resolves each incoming event's organization by handle, read from org.ref_id, which comes from the portal organization's cpRefId:
curl -sk -X PUT https://localhost:9543/api/v0.9/organizations/default \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"id":"default","displayName":"Default","cpRefId":"default"}'
Second, register the Platform API as a webhook subscriber. The secret here does double duty: it signs each delivery and derives the key that encrypts the credential fields. It must equal APIP_CP_WEBHOOK_SECRET on the Platform API, or signature verification and decryption both fail.
export WEBHOOK_SECRET=$(openssl rand -hex 32)
curl -sk -X POST https://localhost:9543/api/v0.9/webhook-subscribers \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d "{\"id\":\"platform-api\",\"displayName\":\"Platform API\",
\"targetUrl\":\"https://platform-api:9243/api/portal/v0.9/webhooks/events\",
\"secret\":\"$WEBHOOK_SECRET\",
\"events\":[\"apikey.*\",\"subscription.*\"],\"enabled\":true}"
Set the same value as APIP_CP_WEBHOOK_SECRET in the portal distribution's api-platform.env and restart the Platform API so it picks it up:
cd ../wso2apip-api-portal-1.0.0
echo "APIP_CP_WEBHOOK_SECRET=$WEBHOOK_SECRET" >> api-platform.env
chmod 600 api-platform.env
docker compose up -d platform-api
Warning
api-platform.env now holds a live shared secret, alongside the admin password hash. Keep it readable only by its owner, and never commit it to source control.
targetUrl uses the container name because the portal reaches the Platform API across the Docker network, not through your host's published port.
Step 4: Create the secured API¶
Create a subscription plan on the Platform API:
export PLAN=e2e-gold
curl -sk -X POST https://localhost:9243/api/v0.9/subscription-plans \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d "{\"id\":\"$PLAN\",\"displayName\":\"$PLAN\",\"status\":\"ACTIVE\",
\"limits\":[{\"limitType\":\"REQUEST_COUNT\",\"timeUnit\":\"HOUR\",\"limitCount\":10000}]}"
Give each plan a unique display name
The gateway stores plans keyed by gateway and display name. Two plans sharing one display name collide, and the second one—along with its subscriptions—silently fails to sync.
Now the API. Two policies do the enforcing: api-key-auth reads the key from a header you name, and subscription-validation reads the subscription token from another.
export API_ID=$(curl -sk -X POST https://localhost:9243/api/v0.9/rest-apis \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d "{\"displayName\":\"Reading List API\",\"context\":\"/reading-list\",\"version\":\"v1\",
\"projectId\":\"$PROJECT_ID\",\"lifeCycleStatus\":\"PUBLISHED\",
\"subscriptionPlans\":[\"$PLAN\"],
\"upstream\":{\"main\":{\"url\":\"https://apis.bijira.dev/samples/reading-list-api-service/v1.0\"}},
\"policies\":[
{\"name\":\"api-key-auth\",\"version\":\"v1\",
\"params\":{\"key\":\"API-Key\",\"in\":\"header\"}},
{\"name\":\"subscription-validation\",\"version\":\"v1\",
\"params\":{\"subscriptionKeyHeader\":\"Subscription-Key\"}}
]}" | jq -r .id)
The header names are the gateway's, set here—API-Key and Subscription-Key are the policy defaults, and changing these params changes what consumers must send.
Deploy it to the gateway, then confirm the route is live and enforcing:
curl -sk -X POST https://localhost:9243/api/v0.9/rest-apis/$API_ID/deployments \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d "{\"gatewayId\":\"$GW_NAME\"}"
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8081/reading-list/v1/books
Expect 401 or 403. A 404 means the route isn't programmed yet—wait a few seconds and retry.
Deploy before issuing credentials
Do not create the subscription or key first. The Platform API only broadcasts credential events to gateways where the API is already deployed, and issuing an API key while no gateway is connected returns 503. Once the API is deployed, credentials propagate live over the control-plane connection—no restart needed.
Step 5: Mirror the API and plan into the portal¶
The Platform API resolves each event's API and plan by handle, so the portal's copies have to carry matching references. Together with the organization link from step 3, that's three linkages that must line up:
| Portal field | Must equal |
|---|---|
Organization cpRefId |
The Platform API organization handle (default) |
API referenceId |
The Platform API API handle |
Plan refId |
The Platform API plan handle |
Sync the plan:
curl -sk -X PUT https://localhost:9543/api/v0.9/subscription-plans \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d "[{\"id\":\"$PLAN\",\"displayName\":\"$PLAN\",\"refId\":\"$PLAN\",
\"limits\":[{\"limitType\":\"REQUEST_COUNT\",\"limitCount\":10000,
\"timeUnit\":\"HOUR\",\"timeAmount\":1}]}]"
Then publish the API to the portal with referenceId set to the Platform API handle, and its specification attached. Write api.yaml and definition.yaml as shown in Getting Started, setting referenceId to the value of $API_ID and listing $PLAN under subscriptionPlans, then:
curl -sk -X POST https://localhost:9543/api/v0.9/apis \
-H "Authorization: Bearer $TOKEN" \
-F "[email protected];type=application/yaml" \
-F "[email protected];type=application/yaml"
Open https://localhost:9543/default/views/default/apis and the API is in the catalog with its plan showing.
Step 6: Get both credentials in the portal¶
Do this part in the portal UI—it's what a developer would actually do. Sign in with the admin credentials.
- Open the API and click Subscribe on the plan. Copy the subscription token from the dialog.
- Click API Keys, then Generate API key. Give it a name and copy the key.
Each action fires a webhook the Platform API turns into gateway state. For the equivalent REST calls, see Subscriptions and API Keys.
Step 7: Invoke through the gateway¶
Send both credentials, in the headers the policies named:
export API_KEY='<the key from the portal>'
export SUB_TOKEN='<the subscription token from the portal>'
curl -i http://localhost:8081/reading-list/v1/books \
-H "API-Key: $API_KEY" \
-H "Subscription-Key: $SUB_TOKEN"
A 200 means the whole chain worked: the portal issued the credentials, signed and encrypted them into a webhook, the Platform API verified and decrypted them, and the gateway is now enforcing them on live traffic.
Propagation takes a moment. If you get a 401 or 403 immediately after generating the credentials, retry after a few seconds.
Confirm each credential is really being checked¶
Drop one header at a time—each should be rejected:
curl -s -o /dev/null -w 'no credentials: %{http_code}\n' http://localhost:8081/reading-list/v1/books
curl -s -o /dev/null -w 'key only: %{http_code}\n' http://localhost:8081/reading-list/v1/books -H "API-Key: $API_KEY"
curl -s -o /dev/null -w 'subscription only: %{http_code}\n' http://localhost:8081/reading-list/v1/books -H "Subscription-Key: $SUB_TOKEN"
Step 8: Watch a lifecycle change propagate¶
Credential changes travel the same path. In the portal, revoke the API key, then call again with it:
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8081/reading-list/v1/books \
-H "API-Key: $API_KEY" -H "Subscription-Key: $SUB_TOKEN"
Once the webhook lands you get 401—the key is gone from the gateway without anyone touching the gateway. Generate a new key and the call succeeds again.
The same holds for the subscription side, where a rejection reads as 403 rather than 401: suspending the subscription blocks calls, resuming restores them, and regenerating the token invalidates the old one while the new one works. See Manage Subscriptions.
Troubleshooting¶
| Symptom | Likely cause |
|---|---|
404 at the gateway |
The route isn't programmed yet, or the API wasn't deployed to this gateway |
503 when generating an API key |
No gateway is connected for that API—deploy it first |
| Credentials never start working | The webhook secret differs between the portal subscriber and APIP_CP_WEBHOOK_SECRET, so signatures fail and credential fields can't be decrypted |
Only the subscription fails (403) |
The portal plan's refId doesn't match the Platform API plan handle, or two plans share a display name |
| Nothing arrives at all | The organization's cpRefId doesn't match the Platform API organization handle |
| Deliveries fail once and stop | Webhook delivery is attempted exactly once with no retry—check delivery history, see Webhook Events |
Related¶
- Webhook Event Catalog: the events this flow depends on, their payloads, and the signing and encryption scheme
- Webhook Integration: registering a subscriber from the Settings UI
- Consume an API: which credentials an API expects, and how to tell
- Manage Subscriptions and Manage API Keys: the consumer-side lifecycles
- Getting Started: the portal on its own, without a gateway