loopling.ai

Developer API

Responses API (Astra) · loopling

Guide · Video · Responses · Messages · Billing & Ink

Overview

POST /v1/responses speaks the OpenAI Responses protocol and serves GPT 6 Astra (gpt-6-astra, alias astra). The request and response shapes are the ones the OpenAI SDK already produces, so pointing the SDK at Loopling is a two-line change — see OpenAI SDK.

v1 is text only: text input items and text output, function tools, structured output, reasoning, streaming. Image, file and audio parts answer 400 unsupported_parameter.

The Astra lane opens after its vendor certification. Until then its catalogue card reports available: false and calls answer 503 provider_unavailable; GET /v1/models/astra says where a deployment stands.

Pricing is per million tokens — 375 Ink input, 1,875 Ink output, with cache reads at 38 and cache writes at 469; above 272,000 input tokens the long-context tier applies. A 10,000-in / 2,000-out call is 8 Ink. The full table is on Billing & Ink.

Request

curl

curl https://loopling.ai/v1/responses \
  -H "Authorization: Bearer $LOOPLING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-6-astra",
    "instructions": "You are a concise studio assistant.",
    "input": "Give me three shot ideas for a night harbour scene.",
    "max_output_tokens": 2048
  }'

Node

const res = await fetch('https://loopling.ai/v1/responses', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LOOPLING_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    model: 'gpt-6-astra',
    instructions: 'You are a concise studio assistant.',
    input: 'Give me three shot ideas for a night harbour scene.',
    max_output_tokens: 2048,
  }),
})
const response = await res.json()
const text = response.output
  .flatMap((item) => item.content ?? [])
  .filter((part) => part.type === 'output_text')
  .map((part) => part.text)
  .join('')

Python

res = requests.post(
    "https://loopling.ai/v1/responses",
    headers={"Authorization": f"Bearer {os.environ['LOOPLING_API_KEY']}"},
    json={
        "model": "gpt-6-astra",
        "instructions": "You are a concise studio assistant.",
        "input": "Give me three shot ideas for a night harbour scene.",
        "max_output_tokens": 2048,
    },
)
response = res.json()
text = "".join(
    part["text"]
    for item in response["output"]
    for part in item.get("content", [])
    if part["type"] == "output_text"
)

OpenAI SDK

from openai import OpenAI

client = OpenAI(
    base_url="https://loopling.ai/v1",
    api_key=os.environ["LOOPLING_API_KEY"],
)

response = client.responses.create(
    model="gpt-6-astra",
    instructions="You are a concise studio assistant.",
    input="Give me three shot ideas for a night harbour scene.",
    max_output_tokens=2048,
)
print(response.output_text)

The response is the standard response object:

{
  "id": "resp_01J9X6K3M8Q2",
  "object": "response",
  "created_at": 1789805564,
  "status": "completed",
  "model": "gpt-6-astra",
  "output": [
    {
      "id": "msg_01J9X6K3M8Q3",
      "type": "message",
      "role": "assistant",
      "status": "completed",
      "content": [
        { "type": "output_text", "text": "1. A lone lantern …", "annotations": [] }
      ]
    }
  ],
  "usage": {
    "input_tokens": 41,
    "input_tokens_details": { "cached_tokens": 0 },
    "output_tokens": 118,
    "output_tokens_details": { "reasoning_tokens": 0 },
    "total_tokens": 159
  },
  "incomplete_details": null,
  "store": false
}

usage is the vendor's own count and is what Loopling settles on. A non-streaming response also carries two headers: X-Loopling-Ink-Charged (the settled Ink for this call) and X-Loopling-Request-Id (the id of the request record in Settings → API). A streaming response reports the same in its trailing loopling.usage event.

Supported parameters

FieldNotes
model"gpt-6-astra" or "astra". Required (unknown_model).
inputA string, or an array of items: message items with input_text / output_text parts, function_call, function_call_output. Required (missing_input).
instructionsSystem-level instructions.
toolsFunction tools only (type: "function"). Hosted tools (web search, file search, computer use) answer unsupported_parameter.
tool_choice"auto", "none", "required", or a named function.
parallel_tool_callsboolean.
reasoning`{ effort: "minimal" \"low" \"medium" \"high" \"xhigh", summary: "auto" \"concise" \"detailed" }`.
text{ format: … } for structured output (json_schema, json_object, text).
temperature, top_pAs in the protocol.
truncation"auto" or "disabled".
max_output_tokensDefault 32,768; maximum 128,000 (too_large above it). The Ink hold is sized for at least 32,768 output tokens whatever you set, so a small cap lowers the charge on settlement, not the hold.
metadataUp to 16 string keys, returned unchanged.
include["reasoning.encrypted_content"] only.
userPassed through.
storeOnly false (the default). true answers unsupported_parameter.
streamtrue for SSE — see Streaming.

Not supported in v1, answered with 400 unsupported_parameter and param naming the field: previous_response_id, conversation, background, prompt, service_tier, max_tool_calls, safety_identifier, prompt_cache_key, store: true, non-function tools, image / file / audio parts in input, and any top-level field not listed above. Loopling keeps no conversation state, so carry the transcript in input yourself.

The input envelope is 922,000 tokens; a prompt above it answers 413 payload_too_large, and a body above 4 MiB does the same. Each request holds Ink for the prompt plus the output cap and settles to the vendor's usage when the response completes — see Reserve, then settle.

Streaming

With stream: true the response is text/event-stream, and the event stream is the protocol's own — response.created, response.output_text.delta, response.output_item.done, response.completed, and so on — followed by one Loopling frame after the terminal event:

event: loopling.usage
data: {"type": "loopling.usage", "ink_charged": 3, "usd_payg_charged": 0.03, "request_id": "req_01J9X6K3M8Q2"}

ink_charged is the settled figure for this call; usd_payg_charged is the same at $1 = 100 Ink; request_id is the id of the request record in Settings → API. SDKs that do not know the event ignore it.

curl

curl -N https://loopling.ai/v1/responses \
  -H "Authorization: Bearer $LOOPLING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-6-astra",
    "input": "Write a two-line logline for a harbour film.",
    "stream": true
  }'
# … event: response.output_text.delta frames, then:
# event: response.completed
# event: loopling.usage
# data: {"ink_charged": 3, "usd_payg_charged": 0.03, "request_id": "req_…"}

Node

const res = await fetch('https://loopling.ai/v1/responses', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LOOPLING_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    model: 'gpt-6-astra',
    input: 'Write a two-line logline for a harbour film.',
    stream: true,
  }),
})
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffered = ''
for (;;) {
  const { value, done } = await reader.read()
  if (done) break
  buffered += decoder.decode(value, { stream: true })
  const frames = buffered.split('\n\n')
  buffered = frames.pop() ?? ''
  for (const frame of frames) {
    const event = /^event: (.+)$/m.exec(frame)?.[1]
    const data = /^data: (.+)$/m.exec(frame)?.[1]
    if (event === 'response.output_text.delta') process.stdout.write(JSON.parse(data).delta)
    if (event === 'loopling.usage') console.log('\n', JSON.parse(data)) // { ink_charged, … }
  }
}

Python

with requests.post(
    "https://loopling.ai/v1/responses",
    headers={"Authorization": f"Bearer {os.environ['LOOPLING_API_KEY']}"},
    json={"model": "gpt-6-astra", "input": "Write a two-line logline for a harbour film.", "stream": True},
    stream=True,
) as res:
    event = None
    for line in res.iter_lines(decode_unicode=True):
        if line.startswith("event: "):
            event = line[7:]
        elif line.startswith("data: "):
            data = json.loads(line[6:])
            if event == "response.output_text.delta":
                print(data["delta"], end="", flush=True)
            elif event == "loopling.usage":
                print("\n", data)  # {"ink_charged": …, "usd_payg_charged": …, "request_id": …}

OpenAI SDK

from openai import OpenAI

client = OpenAI(base_url="https://loopling.ai/v1", api_key=os.environ["LOOPLING_API_KEY"])

stream = client.responses.create(
    model="gpt-6-astra",
    input="Write a two-line logline for a harbour film.",
    stream=True,
)
for event in stream:
    if event.type == "response.output_text.delta":
        print(event.delta, end="", flush=True)
# The trailing loopling.usage frame is ignored by the SDK; read Ink in Settings → API
# or from GET /v1/me.

If the visible output reaches max_output_tokens, Loopling stops the vendor, the stream ends with response.incomplete and incomplete_details.reason is "max_output_tokens"; because the vendor's own count is not available for a stopped run, the call is billed at its hold. A client that disconnects mid-stream is still charged for what the vendor produced, because Loopling does not abort the vendor when the client leaves. Non-streaming calls receive whitespace heartbeats inside the JSON body every 15 seconds while the vendor works, so long replies do not trip idle timeouts.

If the vendor's stream ends without a usage receipt, the stream ends with an error frame — code: "usage_unknown" — and the Ink hold is kept until the vendor's invoice reconciles it; it is never silently charged. See Refunds and holds.

Response schema

FieldTypeNotes
idstringresp_…
object"response"
created_atintegerUnix seconds, as in the protocol.
status"completed" \"incomplete" \"failed"
model"gpt-6-astra"
outputitem[]message items with output_text parts; function_call items when the model calls a tool; reasoning items when requested.
usageobjectinput_tokens, input_tokens_details.cached_tokens, output_tokens, output_tokens_details.reasoning_tokens, total_tokens.
incomplete_details{ reason } \null"max_output_tokens" when the cap was reached.
storefalseAlways.
metadataobjectAs submitted.

Function calls arrive as function_call items with call_id, name and arguments (a JSON string). Run the function and send the result back as a function_call_output item in the next request's input, together with the earlier items — Loopling does not remember the previous turn.

OpenAI SDK

Set base_url to Loopling and api_key to your key; everything else is the SDK you already use.

from openai import OpenAI

client = OpenAI(base_url="https://loopling.ai/v1", api_key=os.environ["LOOPLING_API_KEY"])
import OpenAI from 'openai'

const client = new OpenAI({ baseURL: 'https://loopling.ai/v1', apiKey: process.env.LOOPLING_API_KEY })
const response = await client.responses.create({
  model: 'gpt-6-astra',
  input: 'Give me three shot ideas for a night harbour scene.',
})
console.log(response.output_text)

The SDK's client.chat.completions is not served — /v1/chat/completions is not part of v1. Use client.responses.

Errors

Beyond the shared error types, this endpoint answers:

HTTPtypecodeWhen
400invalid_request_errorinvalid_body, missing_input, invalid_type, invalid_enum, too_largeA field is missing or fails validation; param names it.
400invalid_request_errorunsupported_parameterA rejected field (listed above), a non-function tool, a media part, an include other than reasoning.encrypted_content, or an unknown top-level field.
400invalid_request_errorunknown_modelmodel is not gpt-6-astra / astra.
400invalid_request_errorupstream_rejectedThe vendor's gateway refused the request before running the model (its 400 / 404 / 413 / 415 / 422); the message is the vendor's; the hold is released.
402insufficient_ink_errorinsufficient_inkThe wallet cannot cover the prompt plus the output cap.
402insufficient_ink_errorkey_ink_limit_reachedThe key's Ink cap would be crossed.
413invalid_request_errorpayload_too_largeThe prompt exceeds the 922,000-token envelope, or the body exceeds 4 MiB.
429rate_limit_errorrate_limit_exceededMore than 60 requests in a minute on this key.
502provider_errorprovider_errorThe vendor failed after the request was sent, or answered without a usable usage receipt; the hold is kept until it reconciles.
503provider_errorprovider_unavailableAstra is not enabled or not yet certified on this deployment, the vendor is at capacity, or the vendor is rate-limiting; retry after Retry-After.

On a stream, a vendor failure after dispatch arrives as a final event: error frame instead — code: "usage_unknown" when no usage receipt arrived (hold kept), code: "incomplete_stream" when the stream ended without a terminal response.

API overview

loopling.ai · grows with you, grows itself

hello@loopling.ai · community · pricing · Enterprise · API · privacy · terms