Home / Docs / Quickstart

Quickstart

Point the OpenAI or Anthropic SDK you already use at the router and make a first call.

Use this page to send your first request through the router with the SDK you already have. The router speaks three wire formats: the OpenAI chat completions format, the OpenAI responses format and the Anthropic messages format, so you change only the base URL and the key.

Get a relay key

A relay key is the API key the router issues to your workspace; every request carries it as a bearer token. Today a signed-in workspace creates one on the Keys page of the console. Signing up and receiving a key in one flow is not available yet: join the waiting list and we send you access. See Authentication for scopes and key management.

Set the base URL

Use https://api.massrelay.ai/v1 for production. Staging is https://relay-stag.massrelay.ai/v1.

Make your first request

The model field is optional. When it is absent, the key's routing picks the model. To choose one yourself, list the models your key may use with GET /v1/models (see Models).

curl
curl https://api.massrelay.ai/v1/chat/completions \
  -H "Authorization: Bearer $RELAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"Say hello in one sentence."}]}'
Python
from openai import OpenAI

client = OpenAI(base_url="https://api.massrelay.ai/v1", api_key="YOUR_RELAY_KEY")
reply = client.chat.completions.create(
    model="MODEL_ID_FROM_GET_V1_MODELS",
    messages=[{"role": "user", "content": "Say hello in one sentence."}],
)
print(reply.choices[0].message.content)
TypeScript
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.massrelay.ai/v1",
  apiKey: process.env.RELAY_KEY,
});
const reply = await client.chat.completions.create({
  model: "MODEL_ID_FROM_GET_V1_MODELS",
  messages: [{ role: "user", content: "Say hello in one sentence." }],
});
console.log(reply.choices[0].message.content);

Every answer carries an id of the form chatcmpl-<decisionId>. Keep it: the explain endpoint reads it back (see Routing).

Tip The same base URL serves POST /v1/responses for code written against the responses format.

Use the Anthropic SDK

The Anthropic SDK appends /v1/messages itself, so its base URL has no /v1.

Python
import anthropic

client = anthropic.Anthropic(base_url="https://api.massrelay.ai", api_key="YOUR_RELAY_KEY")
msg = client.messages.create(
    model="MODEL_ID_FROM_GET_V1_MODELS",
    max_tokens=256,
    messages=[{"role": "user", "content": "Say hello in one sentence."}],
)
print(msg.content[0].text)
TypeScript
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({
  baseURL: "https://api.massrelay.ai",
  apiKey: process.env.RELAY_KEY,
});
const msg = await client.messages.create({
  model: "MODEL_ID_FROM_GET_V1_MODELS",
  max_tokens: 256,
  messages: [{ role: "user", content: "Say hello in one sentence." }],
});
console.log(msg.content[0].type === "text" ? msg.content[0].text : "");

Stream the answer

Set stream to true on any of the three endpoints. The answer is then a server-sent event stream in the format of that endpoint.

Before you ship

  • Check your key with GET /v1/key: it reads back the key's scopes and spend. See the reference.
  • Handle errors. Every error has the same JSON shape; a 429 carries Retry-After, so wait that long and retry. See Errors and Limits.
  • Watch for changes. Changes to the API are listed in the Changelog.

API reference

POST /v1/chat/completions

Authentication relay key (bearer)

Request body

FieldTypeDescription
modelstringA model id or an alias from GET /v1/models. Optional: the key's routing decides when absent.
messages requiredarray of objectRequired and non-empty; an empty or missing list is a 400.
messages[].rolestringWho wrote the message: system, user, assistant or tool.
messages[].contentstring or arrayText; the gateway reads the text of a message.
max_completion_tokensnumberUpper bound on the generated tokens, the newer OpenAI name for max_tokens. Send one of the two.
max_tokensnumberUpper bound on the tokens the model may generate for this answer. Send this or max_completion_tokens, not both.
temperaturenumberSampling temperature: lower is more deterministic, higher more varied. Usually between 0 and 2.
streambooleanDefault false.
toolsarrayCarried and forwarded verbatim upstream; never validated here.
response_formatobjectCarried and forwarded verbatim upstream.
reasoning_effortstringThe o-series field; read as the request's effort.

Example request

curl
curl -X POST https://api.massrelay.ai/v1/chat/completions \
  -H "Authorization: Bearer $RELAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"Say hello in one sentence."}]}'
Python
import os, requests

r = requests.post(
    "https://api.massrelay.ai/v1/chat/completions",
    headers={"Authorization": f"Bearer {os.environ['RELAY_KEY']}"},
    json={
        "messages": [
            {
                "role": "user",
                "content": "Say hello in one sentence."
            }
        ]
    },
)
r.raise_for_status()
print(r.json())
TypeScript
const r = await fetch("https://api.massrelay.ai/v1/chat/completions", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.RELAY_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({"messages":[{"role":"user","content":"Say hello in one sentence."}]}),
});
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());

Response

FieldTypeDescription
id requiredstringchatcmpl-<decisionId>; the decision id is the key of GET /v1/route/explain/{decisionId}.
object requiredstringThe kind of object returned, for example chat.completion. Always "chat.completion".
created requiredintegerWhen the answer was created, in Unix seconds.
model requiredstringThe model id. In a request it is optional: without it the key's routing chooses.
choices requiredarray of objectThe generated answers; one unless n asks for more.
choices[].indexintegerThe position of this choice in the list.
choices[].messageobjectThe generated message: its role and content.
choices[].message.rolestringWho wrote the message: system, user, assistant or tool. Always "assistant".
choices[].message.contentstringThe text of the message.
choices[].finish_reasonstringWhy generation stopped, for example stop or length.
usage requiredobjectTokens counted for this call.
usage.prompt_tokensnumberTokens in the input.
usage.completion_tokensnumberTokens generated.
usage.total_tokensnumberInput plus generated tokens.
Example response (shape)
{
  "id": "chatcmpl-1f2e3d4c",
  "object": "chat.completion",
  "created": 1761400000,
  "model": "provider/model-id",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Hello there."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 12,
    "completion_tokens": 6,
    "total_tokens": 18
  }
}

Common errors

  • 401 invalid_api_key
  • 400 the body is not usable
  • 402 out of credit or over a budget
  • 413 the body is over the limit
  • 403 insufficient_scope
  • 429 over the request rate; read Retry-After

Every error has the same JSON shape; see Errors and Limits.

Full schema (JSON)

Request schema

{
  "description": "Every other field is accepted and not read (for example n, stop, top_p, seed are ignored). Fields the gateway does not read are not documented as honoured.",
  "type": "object",
  "required": [
    "messages"
  ],
  "additionalProperties": true,
  "properties": {
    "model": {
      "type": "string",
      "description": "A model id or an alias from GET /v1/models. Optional: the key's routing decides when absent."
    },
    "messages": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "role": {
            "type": "string"
          },
          "content": {
            "description": "Text; the gateway reads the text of a message.",
            "type": [
              "string",
              "array"
            ]
          }
        }
      },
      "description": "Required and non-empty; an empty or missing list is a 400."
    },
    "max_completion_tokens": {
      "type": "number"
    },
    "max_tokens": {
      "type": "number"
    },
    "temperature": {
      "type": "number"
    },
    "stream": {
      "type": "boolean",
      "description": "Default false."
    },
    "tools": {
      "description": "Carried and forwarded verbatim upstream; never validated here.",
      "type": "array"
    },
    "response_format": {
      "description": "Carried and forwarded verbatim upstream.",
      "type": "object",
      "additionalProperties": true
    },
    "reasoning_effort": {
      "type": "string",
      "description": "The o-series field; read as the request's effort."
    }
  }
}

Response schema

{
  "description": "Every other field is accepted and not read (streaming responses are server-sent events and are not described here). Fields the gateway does not read are not documented as honoured.",
  "type": "object",
  "additionalProperties": true,
  "required": [
    "id",
    "object",
    "created",
    "model",
    "choices",
    "usage"
  ],
  "properties": {
    "id": {
      "type": "string",
      "description": "chatcmpl-<decisionId>; the decision id is the key of GET /v1/route/explain/{decisionId}."
    },
    "object": {
      "const": "chat.completion"
    },
    "created": {
      "type": "integer"
    },
    "model": {
      "type": "string"
    },
    "choices": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer"
          },
          "message": {
            "type": "object",
            "properties": {
              "role": {
                "const": "assistant"
              },
              "content": {
                "type": "string"
              }
            }
          },
          "finish_reason": {
            "type": "string"
          }
        }
      }
    },
    "usage": {
      "type": "object",
      "properties": {
        "prompt_tokens": {
          "type": "number"
        },
        "completion_tokens": {
          "type": "number"
        },
        "total_tokens": {
          "type": "number"
        }
      }
    }
  }
}

Every answer carries a decision id; the explain endpoint returns why that model was chosen.

POST /v1/responses

Authentication relay key (bearer)

Request body

FieldTypeDescription
modelstringThe model id. In a request it is optional: without it the key's routing chooses.
input requiredstring or arrayRequired: a string or a list of {role?, content} items (role defaults to user).
instructionsstringBecomes a leading system message; blank text contributes nothing.
max_output_tokensnumber
temperaturenumberSampling temperature: lower is more deterministic, higher more varied. Usually between 0 and 2.
streamboolean
reasoningobjectOnly reasoning.effort is read.
reasoning.effortstring

Example request

curl
curl -X POST https://api.massrelay.ai/v1/responses \
  -H "Authorization: Bearer $RELAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"input":"Say hello in one sentence."}'
Python
import os, requests

r = requests.post(
    "https://api.massrelay.ai/v1/responses",
    headers={"Authorization": f"Bearer {os.environ['RELAY_KEY']}"},
    json={
        "input": "Say hello in one sentence."
    },
)
r.raise_for_status()
print(r.json())
TypeScript
const r = await fetch("https://api.massrelay.ai/v1/responses", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.RELAY_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({"input":"Say hello in one sentence."}),
});
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());

Response

FieldTypeDescription
id requiredstringresp_<decisionId>
object requiredstringThe kind of object returned, for example chat.completion. Always "response".
created_atinteger
modelstringThe model id. In a request it is optional: without it the key's routing chooses.
status requiredstring Always "completed".
output requiredarray of object
output[].typestring Always "message".
output[].idstringThe answer's id.
output[].statusstring
output[].rolestringWho wrote the message: system, user, assistant or tool. Always "assistant".
output[].contentarray of objectThe text of the message.
output[].content[].typestring Always "output_text".
output[].content[].textstring
output[].content[].annotationsarray
output_text requiredstring
usage requiredobjectTokens counted for this call.
usage.input_tokensnumberTokens in the input.
usage.output_tokensnumberTokens generated.
usage.total_tokensnumberInput plus generated tokens.
Example response (shape)
{
  "id": "resp_1f2e3d4c",
  "object": "response",
  "created_at": 1761400000,
  "model": "provider/model-id",
  "status": "completed",
  "output": [
    {
      "type": "message",
      "role": "assistant",
      "content": [
        {
          "type": "output_text",
          "text": "Hello there."
        }
      ]
    }
  ],
  "output_text": "Hello there.",
  "usage": {
    "input_tokens": 12,
    "output_tokens": 6,
    "total_tokens": 18
  }
}

Common errors

  • 401 invalid_api_key
  • 400 the body is not usable
  • 402 out of credit or over a budget
  • 413 the body is over the limit
  • 403 insufficient_scope
  • 429 over the request rate; read Retry-After

Every error has the same JSON shape; see Errors and Limits.

Full schema (JSON)

Request schema

{
  "description": "Every other field is accepted and not read (tools and previous_response_id are not read on this surface). Fields the gateway does not read are not documented as honoured.",
  "type": "object",
  "required": [
    "input"
  ],
  "additionalProperties": true,
  "properties": {
    "model": {
      "type": "string"
    },
    "input": {
      "description": "Required: a string or a list of {role?, content} items (role defaults to user).",
      "type": [
        "string",
        "array"
      ]
    },
    "instructions": {
      "type": "string",
      "description": "Becomes a leading system message; blank text contributes nothing."
    },
    "max_output_tokens": {
      "type": "number"
    },
    "temperature": {
      "type": "number"
    },
    "stream": {
      "type": "boolean"
    },
    "reasoning": {
      "type": "object",
      "properties": {
        "effort": {
          "type": "string"
        }
      },
      "additionalProperties": true,
      "description": "Only reasoning.effort is read."
    }
  }
}

Response schema

{
  "description": "Every other field is accepted and not read (streaming is not described here). Fields the gateway does not read are not documented as honoured.",
  "type": "object",
  "additionalProperties": true,
  "required": [
    "id",
    "object",
    "status",
    "output",
    "output_text",
    "usage"
  ],
  "properties": {
    "id": {
      "type": "string",
      "description": "resp_<decisionId>"
    },
    "object": {
      "const": "response"
    },
    "created_at": {
      "type": "integer"
    },
    "model": {
      "type": "string"
    },
    "status": {
      "const": "completed"
    },
    "output": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "type": {
            "const": "message"
          },
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "role": {
            "const": "assistant"
          },
          "content": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "type": {
                  "const": "output_text"
                },
                "text": {
                  "type": "string"
                },
                "annotations": {
                  "type": "array"
                }
              }
            }
          }
        }
      }
    },
    "output_text": {
      "type": "string"
    },
    "usage": {
      "type": "object",
      "properties": {
        "input_tokens": {
          "type": "number"
        },
        "output_tokens": {
          "type": "number"
        },
        "total_tokens": {
          "type": "number"
        }
      }
    }
  }
}

Every answer carries a decision id; the explain endpoint returns why that model was chosen.

POST /v1/messages

Authentication relay key (bearer)

Request body

FieldTypeDescription
modelstringThe model id. In a request it is optional: without it the key's routing chooses.
messages requiredarray of object
messages[].rolestringWho wrote the message: system, user, assistant or tool.
messages[].contentstring or arrayThe text of the message.
systemstring or arrayA string, or a list of {type:text,text} blocks (joined). An empty list contributes nothing.
max_tokensnumberUpper bound on the tokens the model may generate for this answer. Send this or max_completion_tokens, not both.
temperaturenumberSampling temperature: lower is more deterministic, higher more varied. Usually between 0 and 2.
streamboolean
thinkingstring or objectThis gateway reads a plain effort string, or an object with an effort field; it does not translate Anthropic's budget_tokens block.

Example request

curl
curl -X POST https://api.massrelay.ai/v1/messages \
  -H "Authorization: Bearer $RELAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"Say hello in one sentence."}],"max_tokens":256}'
Python
import os, requests

r = requests.post(
    "https://api.massrelay.ai/v1/messages",
    headers={"Authorization": f"Bearer {os.environ['RELAY_KEY']}"},
    json={
        "messages": [
            {
                "role": "user",
                "content": "Say hello in one sentence."
            }
        ],
        "max_tokens": 256
    },
)
r.raise_for_status()
print(r.json())
TypeScript
const r = await fetch("https://api.massrelay.ai/v1/messages", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.RELAY_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({"messages":[{"role":"user","content":"Say hello in one sentence."}],"max_tokens":256}),
});
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
console.log(await r.json());

Response

FieldTypeDescription
id requiredstringmsg_<decisionId>
type requiredstring Always "message".
role requiredstringWho wrote the message: system, user, assistant or tool. Always "assistant".
model requiredstringThe model id. In a request it is optional: without it the key's routing chooses.
content requiredarray of objectThe text of the message.
content[].typestring Always "text".
content[].textstring
stop_reason requiredstring
stop_sequencenull
usage requiredobjectTokens counted for this call.
usage.input_tokensnumberTokens in the input.
usage.output_tokensnumberTokens generated.
Example response (shape)
{
  "id": "msg_1f2e3d4c",
  "type": "message",
  "role": "assistant",
  "model": "provider/model-id",
  "content": [
    {
      "type": "text",
      "text": "Hello there."
    }
  ],
  "stop_reason": "end_turn",
  "stop_sequence": null,
  "usage": {
    "input_tokens": 12,
    "output_tokens": 6
  }
}

Common errors

  • 401 invalid_api_key
  • 400 the body is not usable
  • 402 out of credit or over a budget
  • 413 the body is over the limit
  • 403 insufficient_scope
  • 429 over the request rate; read Retry-After

Every error has the same JSON shape; see Errors and Limits.

Full schema (JSON)

Request schema

{
  "description": "Every other field is accepted and not read (for example top_p, top_k, stop_sequences, tools are not read on this surface). Fields the gateway does not read are not documented as honoured.",
  "type": "object",
  "required": [
    "messages"
  ],
  "additionalProperties": true,
  "properties": {
    "model": {
      "type": "string"
    },
    "messages": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "role": {
            "type": "string"
          },
          "content": {
            "type": [
              "string",
              "array"
            ]
          }
        }
      }
    },
    "system": {
      "description": "A string, or a list of {type:text,text} blocks (joined). An empty list contributes nothing.",
      "type": [
        "string",
        "array"
      ]
    },
    "max_tokens": {
      "type": "number"
    },
    "temperature": {
      "type": "number"
    },
    "stream": {
      "type": "boolean"
    },
    "thinking": {
      "description": "This gateway reads a plain effort string, or an object with an effort field; it does not translate Anthropic's budget_tokens block.",
      "type": [
        "string",
        "object"
      ]
    }
  }
}

Response schema

{
  "description": "Every other field is accepted and not read (streaming is not described here). Fields the gateway does not read are not documented as honoured.",
  "type": "object",
  "additionalProperties": true,
  "required": [
    "id",
    "type",
    "role",
    "model",
    "content",
    "stop_reason",
    "usage"
  ],
  "properties": {
    "id": {
      "type": "string",
      "description": "msg_<decisionId>"
    },
    "type": {
      "const": "message"
    },
    "role": {
      "const": "assistant"
    },
    "model": {
      "type": "string"
    },
    "content": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "type": {
            "const": "text"
          },
          "text": {
            "type": "string"
          }
        }
      }
    },
    "stop_reason": {
      "type": "string"
    },
    "stop_sequence": {
      "type": "null"
    },
    "usage": {
      "type": "object",
      "properties": {
        "input_tokens": {
          "type": "number"
        },
        "output_tokens": {
          "type": "number"
        }
      }
    }
  }
}

Every answer carries a decision id; the explain endpoint returns why that model was chosen.