Developer API
Guide · Video · Responses · Messages · Billing & Ink
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.
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.
| Field | Notes | ||||||
|---|---|---|---|---|---|---|---|
model | "gpt-6-astra" or "astra". Required (unknown_model). | ||||||
input | A string, or an array of items: message items with input_text / output_text parts, function_call, function_call_output. Required (missing_input). | ||||||
instructions | System-level instructions. | ||||||
tools | Function 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_calls | boolean. | ||||||
reasoning | `{ effort: "minimal" \ | "low" \ | "medium" \ | "high" \ | "xhigh", summary: "auto" \ | "concise" \ | "detailed" }`. |
text | { format: … } for structured output (json_schema, json_object, text). | ||||||
temperature, top_p | As in the protocol. | ||||||
truncation | "auto" or "disabled". | ||||||
max_output_tokens | Default 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. | ||||||
metadata | Up to 16 string keys, returned unchanged. | ||||||
include | ["reasoning.encrypted_content"] only. | ||||||
user | Passed through. | ||||||
store | Only false (the default). true answers unsupported_parameter. | ||||||
stream | true 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.
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.
| Field | Type | Notes | ||
|---|---|---|---|---|
id | string | resp_… | ||
object | "response" | |||
created_at | integer | Unix seconds, as in the protocol. | ||
status | "completed" \ | "incomplete" \ | "failed" | |
model | "gpt-6-astra" | |||
output | item[] | message items with output_text parts; function_call items when the model calls a tool; reasoning items when requested. | ||
usage | object | input_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. | |
store | false | Always. | ||
metadata | object | As 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.
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.
Beyond the shared error types, this endpoint answers:
| HTTP | type | code | When |
|---|---|---|---|
| 400 | invalid_request_error | invalid_body, missing_input, invalid_type, invalid_enum, too_large | A field is missing or fails validation; param names it. |
| 400 | invalid_request_error | unsupported_parameter | A rejected field (listed above), a non-function tool, a media part, an include other than reasoning.encrypted_content, or an unknown top-level field. |
| 400 | invalid_request_error | unknown_model | model is not gpt-6-astra / astra. |
| 400 | invalid_request_error | upstream_rejected | The 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. |
| 402 | insufficient_ink_error | insufficient_ink | The wallet cannot cover the prompt plus the output cap. |
| 402 | insufficient_ink_error | key_ink_limit_reached | The key's Ink cap would be crossed. |
| 413 | invalid_request_error | payload_too_large | The prompt exceeds the 922,000-token envelope, or the body exceeds 4 MiB. |
| 429 | rate_limit_error | rate_limit_exceeded | More than 60 requests in a minute on this key. |
| 502 | provider_error | provider_error | The vendor failed after the request was sent, or answered without a usable usage receipt; the hold is kept until it reconciles. |
| 503 | provider_error | provider_unavailable | Astra 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.
loopling.ai · grows with you, grows itself
hello@loopling.ai · community · pricing · Enterprise · API · privacy · terms