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:

  1. Sign in to app.osaas.io.
  2. Go to Settings in the left sidebar.
  3. Click the { } API tab.
  4. 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_TOKEN inside a My App is a runner refresh token, NOT a PAT. Passing it directly to /servicetoken returns a 401. Always exchange it via /runner-token/refresh first.

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.tsContext.getServiceAccessToken(), activateService()
  • packages/core/src/core.tscreateInstance(), getInstance(), listInstances(), removeInstance()
  • packages/core/src/myapp.ts — My App management via deploy-manager

See also: