Programmatic Instance Management
This page documents the correct HTTP contract for managing OSC service instances from scripts, pipelines, and applications without using the web console or MCP tools.
Retired Endpoint (api.osaas.io/service/*)
POST https://api.osaas.io/service/<serviceId> is not a supported write endpoint and must not be used in new integrations. The api.osaas.io host became a cache-only CloudFront distribution and rejects all write methods (POST, DELETE, PATCH) with a CloudFront 403 error. Existing pipelines that target this host will break.
Use the per-service apiUrl contract described below instead.
Correct Contract
The authoritative implementation is @osaas/client-core. The following describes the HTTP calls it makes, so you can replicate the same behaviour in any language.
Step 1: Obtain Your Personal Access Token (PAT)
Generate a long-lived PAT from the OSC console:
- Sign in to app.osaas.io.
- Go to Settings in the left sidebar.
- Click the { } API tab.
- Copy the personal access token.
Store it in an environment variable. Never commit it to source control.
export OSC_ACCESS_TOKEN=<your-pat>
Step 2: Get the Per-Service apiUrl from Your Subscriptions
Each service you have subscribed to exposes its own orchestrator base URL (apiUrl). Fetch your subscriptions list from the catalog:
curl -s https://catalog.svc.prod.osaas.io/mysubscriptions \
-H "x-pat-jwt: Bearer $OSC_ACCESS_TOKEN" \
-H "Content-Type: application/json"
Response is an array of service entries:
[
{
"serviceId": "eyevinn-test-adserver",
"apiUrl": "https://eyevinn-test-adserver.auto.prod.osaas.io",
"serviceType": "instance"
}
]
Find the entry whose serviceId matches the service you want to manage. The apiUrl is your base URL for all create/get/list/delete operations on that service.
If the service is not in the list, you have not subscribed to it yet. Subscribe by sending a POST to
/mysubscriptions:
bash curl -s -X POST https://catalog.svc.prod.osaas.io/mysubscriptions \ -H "x-pat-jwt: Bearer $OSC_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"services":["eyevinn-test-adserver"]}'
Step 3: Exchange the PAT for a Service Access Token (SAT)
All service orchestrator endpoints are protected by a SAT scoped to the specific service. Exchange your PAT for a SAT at the token service:
curl -s -X POST https://token.svc.prod.osaas.io/servicetoken \
-H "x-pat-jwt: Bearer $OSC_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"serviceId":"eyevinn-test-adserver"}'
Response:
{
"serviceId": "eyevinn-test-adserver",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiry": 1757123456
}
The token field is your SAT. The expiry field is a Unix timestamp. SATs expire after one hour. Refresh by repeating this step.
Step 4: Manage Instances Using apiUrl + x-jwt
Use the SAT in the x-jwt header (the service orchestrator uses x-jwt, not Authorization) for all instance operations.
Create an instance
SAT=<token-from-step-3>
API_URL=<apiUrl-from-step-2>
curl -s -X POST "$API_URL" \
-H "x-jwt: Bearer $SAT" \
-H "Content-Type: application/json" \
-d '{"name":"myadserver"}'
Response includes the instance URL and any service-specific fields.
Instance names must be lowercase alphanumeric only (
a-z,0-9) — no hyphens, underscores, spaces, or uppercase letters.
List instances
curl -s "$API_URL" \
-H "x-jwt: Bearer $SAT" \
-H "Content-Type: application/json"
Get a specific instance
curl -s "$API_URL/myadserver" \
-H "x-jwt: Bearer $SAT" \
-H "Content-Type: application/json"
Returns 404 if the instance does not exist.
Delete an instance
curl -s -X DELETE "$API_URL/myadserver" \
-H "x-jwt: Bearer $SAT"
TypeScript SDK
@osaas/client-core implements this contract. Use it if you are working in Node.js:
import { Context, createInstance, getInstance, listInstances, removeInstance } from "@osaas/client-core";
// OSC_ACCESS_TOKEN env var is read automatically
const ctx = new Context();
// Step 3: get a SAT (Steps 1+2 are handled internally)
const sat = await ctx.getServiceAccessToken("eyevinn-test-adserver");
// Create
const instance = await createInstance(ctx, "eyevinn-test-adserver", sat, {
name: "myadserver"
});
console.log(instance.url);
// Get
const found = await getInstance(ctx, "eyevinn-test-adserver", "myadserver", sat);
// List
const all = await listInstances(ctx, "eyevinn-test-adserver", sat);
// Delete
const result = await removeInstance(ctx, "eyevinn-test-adserver", "myadserver", sat);
// result === 'success' | 'alreadyAbsent'
My Apps: Runner Token to PAT Refresh
My Apps (custom code deployed via web-runner, python-runner, wasm-runner, or golang-runner) receive a runner refresh token, not a PAT, in the OSC_ACCESS_TOKEN environment variable. To call a catalog service instance from inside a My App, exchange the runner token for a PAT first.
Step A: Exchange the runner refresh token for a PAT
# Inside My App code, OSC_ACCESS_TOKEN holds a runner refresh token
curl -s -X POST https://token.svc.prod.osaas.io/runner-token/refresh \
-H "Content-Type: application/json" \
-d "{\"token\":\"$OSC_ACCESS_TOKEN\"}"
Response:
{
"token": "<short-lived-PAT>",
"expiresIn": 3600
}
Step B: Exchange the PAT for a SAT (same as Step 3 above)
PAT=<token-from-step-A>
curl -s -X POST https://token.svc.prod.osaas.io/servicetoken \
-H "x-pat-jwt: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{"serviceId":"eyevinn-app-config-svc"}'
Step C: Call the service with the SAT
curl -s "$API_URL/myinstance" \
-H "x-jwt: Bearer $SAT"
OSC_ACCESS_TOKENinside a My App is a runner refresh token, NOT a PAT. Passing it directly to/servicetokenreturns a 401. Always exchange it via/runner-token/refreshfirst.
Node.js example (My App)
// Exchange runner token for a PAT
const patRes = await fetch("https://token.svc.prod.osaas.io/runner-token/refresh", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token: process.env.OSC_ACCESS_TOKEN })
});
const { token: pat } = await patRes.json();
// Exchange PAT for a SAT
const satRes = await fetch("https://token.svc.prod.osaas.io/servicetoken", {
method: "POST",
headers: {
"x-pat-jwt": `Bearer ${pat}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ serviceId: "eyevinn-app-config-svc" })
});
const { token: sat } = await satRes.json();
// Call the service
const data = await fetch(`${apiUrl}/myinstance`, {
headers: { "x-jwt": `Bearer ${sat}` }
});
Token Expiry and Refresh
| Token | Lifetime | How to refresh |
|---|---|---|
| PAT (console-generated) | Long-lived | Rotate manually in Settings / API |
| PAT (from runner-token/refresh) | 3600 seconds | Repeat Step A |
| SAT | ~1 hour | Repeat Step 3 (or Steps A+B from My Apps) |
A practical strategy for long-running processes is to refresh every 50 minutes or to catch any 401 response from the service and retry once with a fresh SAT.
Reference Implementation
The complete TypeScript source is in the @osaas/client-core package:
packages/core/src/context.ts—Context.getServiceAccessToken(),activateService()packages/core/src/core.ts—createInstance(),getInstance(),listInstances(),removeInstance()packages/core/src/myapp.ts— My App management via deploy-manager
See also:
- Developer Guide: Service Access Tokens — SAT mechanics and expiry details
- Developer Guide: Service-to-Service Integration — My App calling catalog services
- Developer Guide: OSC CLI in CI Pipelines — using the CLI in automated pipelines