Calling endpoints externally
This page is the wire-protocol guide for invoking a Workspace custom endpoint from outside the workspace process. It focuses on the one detail that trips most clients up: HTTP 202 Accepted means "still working — poll again," not "queued for later." Everything else is plain JSON over HTTPS.
For the conceptual map of API surfaces, see API Overview. For tokens, see Token scopes. For writing the endpoint itself, see Custom Endpoints.
The request
There are two routes; pick the one that matches your token type:
POST /api/endpoints/run/{name} # session JWT or API token
POST /api/endpoints/token/run/{name} # endpoint token
Headers:
Authorization: Bearer <token>
Content-Type: application/json
Accept: application/json
The body is whatever your endpoint's Body.FromJson<T>() expects.
The response — three cases
The server makes one decision per request: did the endpoint finish inside its short server-side wait window?
| Status | Meaning | Body |
|---|---|---|
200 OK |
The endpoint completed. | The endpoint's return value (JSON, text, or a file payload). |
202 Accepted |
Still running. Poll again by re-POSTing to the same URL. | Empty. |
500 Internal Server Error |
The endpoint threw. | Exception message as plain text. See Error codes. |
No separate polling endpoint
There is no separate polling endpoint, no job id, no Location header, no Retry-After. The pollable identity of the in-flight task is (endpoint, body, caller). The server hashes that into a cache key and returns it on every response as X-MSK-ENDPOINT-KEY. On a subsequent poll you may either:
- Re-POST the same body — the hash matches and you hit the same task. Simplest path; this is what the
Curiosity.LibrarySDK does. - Echo the cache key, drop the body — send
X-MSK-ENDPOINT-KEY: <key>as a request header and POST with an empty body. The server short-circuits to the cached task. This is what the Workspace front-end does; it saves bandwidth when bodies are large.
Either way, keep the same URL and Authorization header for the duration of the loop.
Response headers on 202
| Header | Purpose |
|---|---|
X-MSK-ENDPOINT-KEY |
Cache key for the in-flight task. Capture it and echo it back as a request header on subsequent polls to skip resending the body. Also useful in support requests alongside the trace id. |
CalculationProgress |
Optional free-form progress string set by the endpoint (e.g., "42 / 1000 records processed"). May be empty. Surface it to end-users for UX. |
The polling loop
Keep polling
Keep polling. If the server stops seeing requests for a given cache key for ~5 seconds it pauses the task; after ~1 minute of silence it cancels it. Stopping your poll loop is the cancellation mechanism — there is no explicit cancel call.
The minimum viable loop:
- POST the request.
- If
200, parse the body, done. - If
202, capture theX-MSK-ENDPOINT-KEYresponse header (and surfaceCalculationProgressif present), sleep 1 second, then poll again. Either re-POST the same body, or echo the captured key as a request header and POST with an empty body. - If
500(or any other non-2xx), surface the failure — the body is the exception message.
Optional — letting the server wait longer
Set X-MSK-RETRY: <integer> on each poll. The server uses it to scale how long it blocks before giving up and returning 202 (1 s at retry 0, scaling up to ~15 s on later retries). This reduces the number of round-trips when your endpoint takes seconds-to-minutes. It is optional; clients that don't send it get the 1 s default.
C# — Curiosity.Library SDK (recommended)
The SDK ships a client that handles the 202 loop, cancellation, and JSON (de)serialization for you. Add the package:
dotnet add package Curiosity.Library
Then:
using System.Threading;
using Curiosity.Library;
var client = new EndpointsClient(
baseUrl: "https://workspace.example.com",
endpointToken: Environment.GetEnvironmentVariable("WORKSPACE_ENDPOINT_TOKEN"));
var request = new {
query = "screen flicker after firmware update",
productSku = "PRO-14",
limit = 5,
};
// Generic body + generic response — the SDK serializes and deserializes for you.
var results = await client.CallAsync<object, Ticket[]>(
endpoint: "similar-tickets",
body: request,
cancellationToken: CancellationToken.None);
foreach (var t in results) Console.WriteLine(t.Subject);
Available overloads:
| Signature | When to use |
|---|---|
CallAsync<TBody, TResponse>(endpoint, body, ct) |
Typed request + typed response. |
CallAsync<TResponse>(endpoint, ct) |
No body, typed response. |
CallAsync<TBody>(endpoint, body, ct) |
Typed request, fire-and-wait (no response value). |
CallAsync(endpoint, ct) |
No body, no response. |
What the SDK does for you under the hood:
- Targets
POST /api/cce/token/run/{endpoint}(an alias of/api/endpoints/token/run/{endpoint}). - Attaches the
Authorization: Bearer …header on every request. - Loops on
202with a 1-second delay, re-sending the same body each time, until the server returns200or throws. - Throws
HttpRequestExceptionon500/non-success. Cancel viaCancellationToken.
The SDK uses the "re-send the body" poll mode. For most workloads that's fine; if your request bodies are large and your endpoint is slow, the raw-HttpClient example below shows the X-MSK-ENDPOINT-KEY optimization that skips resending the body on every poll.
No built-in deadline
The SDK loops indefinitely while the server returns 202. If you need a deadline, pass a CancellationTokenSource with CancelAfter(...):
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(2));
var results = await client.CallAsync<object, Ticket[]>("similar-tickets", request, cts.Token);
C# — HttpClient directly (no SDK)
Use this when you can't add the Curiosity.Library dependency, or you want full control of timeouts, headers, and retries.
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
static async Task<T> CallEndpointAsync<T>(
HttpClient http,
string baseUrl,
string endpointName,
object body,
string token,
CancellationToken ct)
{
var url = $"{baseUrl.TrimEnd('/')}/api/endpoints/token/run/{endpointName}";
var retry = 0;
string? cacheKey = null; // captured from X-MSK-ENDPOINT-KEY on the first 202
while (true)
{
using var req = new HttpRequestMessage(HttpMethod.Post, url);
req.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
req.Headers.TryAddWithoutValidation("X-MSK-RETRY", retry.ToString()); // optional
if (cacheKey is null)
{
// First request — send the body. The server hashes it into a cache key.
req.Content = JsonContent.Create(body);
}
else
{
// Subsequent polls — echo the cache key, omit the body.
req.Headers.TryAddWithoutValidation("X-MSK-ENDPOINT-KEY", cacheKey);
}
var response = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct);
if (response.StatusCode == HttpStatusCode.Accepted)
{
// Capture the cache key so subsequent polls can skip the body.
if (response.Headers.TryGetValues("X-MSK-ENDPOINT-KEY", out var keys))
cacheKey = keys.FirstOrDefault();
if (response.Headers.TryGetValues("CalculationProgress", out var progress))
Console.WriteLine($"progress: {string.Join(" ", progress)}");
await Task.Delay(TimeSpan.FromSeconds(1), ct);
retry++;
continue;
}
response.EnsureSuccessStatusCode(); // throws on 4xx/5xx
return (await response.Content.ReadFromJsonAsync<T>(cancellationToken: ct))!;
}
}
Call it the same way:
using var http = new HttpClient();
var results = await CallEndpointAsync<Ticket[]>(
http, "https://workspace.example.com", "similar-tickets",
new { query = "screen flicker", limit = 5 },
Environment.GetEnvironmentVariable("WORKSPACE_ENDPOINT_TOKEN")!,
CancellationToken.None);
Other languages
The protocol is the same in every language: POST, branch on the status code, sleep on 202, poll. The C# and Python samples below demonstrate the X-MSK-ENDPOINT-KEY optimization (capture the key on 202, echo it back on subsequent polls, drop the body). The other languages re-POST the body each time to keep the samples short; applying the same optimization is a two-line change.
import os, time, requests
BASE = "https://workspace.example.com"
TOKEN = os.environ["WORKSPACE_ENDPOINT_TOKEN"]
def call_endpoint(name: str, body: dict, timeout_s: float = 120) -> dict:
url = f"{BASE}/api/endpoints/token/run/{name}"
base_hdrs = {
"Authorization": f"Bearer {TOKEN}",
"Accept": "application/json",
}
deadline = time.monotonic() + timeout_s
retry = 0
cache_key = None # captured from X-MSK-ENDPOINT-KEY on the first 202
while True:
if time.monotonic() > deadline:
raise TimeoutError(f"endpoint {name} did not finish in {timeout_s}s")
headers = dict(base_hdrs)
headers["X-MSK-RETRY"] = str(retry) # optional
if cache_key is None:
# First request — send the body.
headers["Content-Type"] = "application/json"
r = requests.post(url, headers=headers, json=body, timeout=30)
else:
# Subsequent polls — echo the cache key, omit the body.
headers["X-MSK-ENDPOINT-KEY"] = cache_key
r = requests.post(url, headers=headers, timeout=30)
if r.status_code == 202:
cache_key = r.headers.get("X-MSK-ENDPOINT-KEY") or cache_key
progress = r.headers.get("CalculationProgress")
if progress:
print(f"progress: {progress}")
time.sleep(1)
retry += 1
continue
r.raise_for_status()
return r.json()
results = call_endpoint("similar-tickets", {
"query": "screen flicker after firmware update",
"productSku": "PRO-14",
"limit": 5,
})
Implementation checklist
- Use the same URL and
Authorizationheader on every poll. Either keep sending the same body, or captureX-MSK-ENDPOINT-KEYfrom the first202and echo it back on subsequent polls with an empty body — pick one and stick with it for the duration of the loop. Changing the body without sending the cache key starts a new task. - Sleep ~1 second between polls. Going faster does not speed up the work and will hit rate limits.
- Keep polling. Stopping for >5 seconds may pause the task; >1 minute cancels it. This is also how you cancel: drop the loop.
- Wrap the loop in a caller-side deadline. The protocol itself has no upper bound.
- Treat
500as final and surface the response body — it is the endpoint's exception message. - Log the
X-MSK-ENDPOINT-KEYalongside thetraceIdwhen filing support requests. - If the endpoint sets
CalculationProgress, surface it to the user — for long-running jobs this is the only feedback they get.
Related pages
- API Overview — surfaces, tokens, decision guide.
- API Usage and Tokens — token creation and quick examples.
- Custom Endpoints — write the server side.
- REST API reference — exact request/response shapes.
- Error codes — every status code and how to react.
- Token scopes — pick the right token for each caller.