Getting Started with OpenAI Responses API

Introduction & Goals

Welcome to your first lesson in building effective agents with GPT-5! Whether you are new to the OpenAI API or have some experience, this lesson will provide you with a solid foundation in structuring requests and interpreting responses — skills you will reuse for agent workflows throughout this course.

In this lesson, you will learn how to send messages using the Responses API and understand the complete response structure. By the end, you will be able to create a script that communicates with a chat-capable model and inspects the full JSON response. This is important because, throughout the course, we will work with different parts of Responses API outputs — from basic text to reasoning summaries and conversation flow control.

The same patterns extend to more complex workflows later in the path, regardless of which compatible model you choose.

Environment and Setup

To communicate with GPT-5, you will need two things: the OpenAI Ruby gem and an API key from OpenAI. The gem handles all the technical details of making API requests; you would normally install it by adding gem "openai", "~> 0.59.0" to your Gemfile and running bundle install, or directly with gem install openai -v 0.59.0. At the top of your script, you bring it in with require "openai". The API key authenticates your requests, and OpenAI::Client.new automatically reads it from the OPENAI_API_KEY environment variable.

In CodeSignal, we have already configured everything for you — the gem is pre-installed, and your API key is set up, so you can focus on learning the core concepts without worrying about setup details.

How the Responses API Handles Messaging

Every interaction follows a structured conversation pattern. In the Ruby client, the Responses API uses role-based messaging, where every message is a Ruby hash with a role key and a content array of typed content blocks:

  • The developer role sets behavior and context for the model — the model's "job description" for the conversation. This message is passed directly inside the input array alongside all other messages, not as a separate field.
  • The user role represents messages from you or your end users.
  • The assistant role represents model responses.

A user or developer message follows this shape:

{
  role: "user",
  content: [
    { type: "input_text", text: "Your message here" }
  ]
}

The content field is an array of typed blocks rather than a plain string. The type: "input_text" block is the standard way to pass text to the model. This typed structure gives the API flexibility to support multiple content types within a single message.

Content Type and Role

There is one important subtlety: the content type depends on the role. Messages that go into the model — user and developer — use input_text. But assistant messages, which represent the model's own output, must use output_text:

{
  role: "assistant",
  content: [
    { type: "output_text", text: "The assistant's previous reply" }
  ]
}

This distinction matters the moment you start replaying the model's previous answers back into a conversation. If you send an assistant message using input_text, the API rejects it with an error like Invalid value: 'input_text'. Supported values are: 'output_text' and 'refusal'. We will account for this directly in our helper method.

When you make a request, you provide a model and an input array that contains all messages in order — including the developer message at the front. Optional parameters like store control persistence. The API returns a structured response object containing the model's output and metadata.

Setting Up the Client and Configuration

Let's build our first interaction by initializing the client and defining a model and developer prompt:

require "json"
require "openai"

# Initialize the OpenAI client
client = OpenAI::Client.new

# Choose a model to use
model = "gpt-5"

# Short developer prompt starting with "You are"
developer_prompt = "You are a helpful assistant. Answer questions very briefly."

OpenAI::Client.new automatically reads your API key from the OPENAI_API_KEY environment variable. The developer_prompt influences how the model responds throughout the conversation and is central to defining behavior you will use later for tools and agent workflows.

Creating Your First Message

To avoid repeating the message hash structure throughout your code, we define a small helper method that also handles the role-based content type for us:

def text_message(role, text)
  # Assistant messages must use "output_text"; user/developer use "input_text"
  content_type = role == "assistant" ? "output_text" : "input_text"

  {
    role: role,
    content: [
      { type: content_type, text: text }
    ]
  }
end

This helper takes a role and a text string and returns a properly formatted message hash with a typed content block. The single conditional line is what keeps it correct across all three roles: when the role is "assistant", it uses output_text; for user and developer, it uses input_text. This means you can call text_message with any role and always get a valid message — including when you replay the model's previous answers into a multi-turn conversation later in this lesson.

Now we can create the messages array representing the conversation so far:

# Create an array of messages to send to GPT-5
messages = [
  text_message("user", "What is the main difference between cats and dogs as pets")
]

Each message produced by text_message has role and content fields, where content holds an array with a single typed block. Even for a single turn, using an array makes it natural to grow into multi-turn conversations.

Sending the Message with the Responses API

With our message prepared, we can now send it to GPT-5:

# Send the messages to GPT-5
response = client.responses.create(
  model: model,
  input: [
    text_message("developer", developer_prompt),
    *messages
  ],
  store: false
)

client.responses.create sends an HTTP request to OpenAI's servers, where GPT-5 processes your messages and returns a structured response.

Notice that the developer prompt is passed as a developer-role message directly inside the input array, using the same text_message helper. The splat operator (*messages) expands the messages array inline, so the full input array contains all messages in the correct order.

The store parameter controls whether OpenAI persists this conversation for later retrieval. Setting store: false tells OpenAI not to save the conversation — it processes your request and returns a response, but the conversation will not be retrievable later. Throughout this course, we will manage the entire context window ourselves by maintaining conversation history in our code, so we do not need OpenAI to store anything. This gives us complete control over what is sent in each request and how conversations are structured.

For simplicity, these examples do not set an explicit output-length limit. The Responses API does support controls such as max_output_tokens, but we omit them here to keep the first examples focused on message structure and response handling.

Examining the Complete Response Structure

To understand what GPT-5 returns, let's examine the complete response structure:

# Print the whole response as JSON
puts JSON.pretty_generate(response.to_h)

response.to_h converts the response object into a plain Ruby hash, and JSON.pretty_generate formats it as readable, indented JSON. You will see output similar to the following:

{
  "id": "resp_uh4yvkTPmabXvBEEz5K7_...",
  "created_at": 1777049283,
  "error": null,
  "instructions": null,
  "metadata": {},
  "model": "gpt-5",
  "object": "response",
  "output": [
    {
      "id": "rs_07b7b16e75a597c70169eb9ec3e73881...",
      "summary": [],
      "type": "reasoning",
      "status": null
    },
    {
      "id": "msg_07b7b16e75a597c70169eb9ec5eab481...",
      "content": [
        {
          "annotations": [],
          "text": "- Cats: More independent, low-maintenance, use a litter box, need less training/exercise.\n- Dogs: More social and trainable, need daily walks, time, and attention.",
          "type": "output_text",
          "logprobs": []
        }
      ],
      "role": "assistant",
      "status": "completed",
      "type": "message"
    }
  ],
  "status": "completed",
  "usage": {
    "input_tokens": 32,
    "output_tokens": 231,
    "output_tokens_details": {
      "reasoning_tokens": 128
    },
    "total_tokens": 263
  },
  "store": false
}

This structure contains everything you need to see how the API processed your request and what it returned. Notice that the assistant message block in output uses type: "output_text" — this is exactly why our helper must produce output_text for assistant messages when we replay them back into the conversation.

Understanding Response Fields

Understanding this response structure is essential for the rest of the course. Key fields include:

  • id — Unique identifier for logging and debugging
  • status — Whether the response completed successfully
  • output — Array of output blocks (e.g., reasoning blocks and message blocks)
  • usage — Token consumption details, which matter for monitoring and cost control

In output, you typically see a reasoning block (optionally with a summary if enabled) and a message block (the final answer). Inside message blocks, the actual text lives in content items of type "output_text".

It is important to note that GPT-5 requests include reasoning with medium effort by default — even when you do not explicitly specify the reasoning parameter, the model allocates tokens to internal reasoning processes. You will see this reflected in the usage section, where reasoning_tokens are counted separately from regular output tokens. This default behavior ensures thoughtful, well-considered responses while balancing quality and speed. This structure allows the API to include both intermediate reasoning summaries and final outputs when supported.

Extracting the Text Response

While the full hash from to_h shows you everything about the response, most of the time you will want just GPT-5's text reply. The Ruby client provides a convenient method for this:

# Print the text response
puts response.output_text

output_text automatically extracts the text content from the response structure. It scans through the output array for assistant message blocks, finds content items where type is "output_text", and collects their text values. This produces the clean text output:

- Cats: More independent, low-maintenance, use a litter box, need less training/exercise.
- Dogs: More social and trainable, need daily walks, time, and attention.

This is the easiest approach for simple interactions, as the client handles navigating the nested structure for you. Note that output_text does not appear in the raw hash returned by to_h — it is a convenience method computed by the Ruby client based on the content structure.

Building Multi-Turn Conversations

To continue a conversation, maintain the history by appending the model's response as an assistant message, then add your follow-up:

# Append GPT-5's response to messages
messages << text_message("assistant", response.output_text)

# Append a new user message
messages << text_message("user", "Which one is easier to train?")

# Send the second request
second_response = client.responses.create(
  model: model,
  input: [
    text_message("developer", developer_prompt),
    *messages
  ],
  store: false
)

We use the << operator to append new messages to the array. Notice how we construct the assistant message using text_message("assistant", response.output_text) — this adds GPT-5's previous reply back into the conversation history. Because we pass the "assistant" role, our helper automatically wraps the text in an output_text block, which is exactly what the API requires for model-generated messages. This is the moment where the role-based content type in our helper pays off: if it had hard-coded input_text, this line would trigger the Invalid value: 'input_text' error.

The developer message is always prepended fresh at the front of input, while *messages expands the full conversation history behind it. This preserves the conversation flow and allows GPT-5 to understand the context of our follow-up question. Now let's see GPT-5's response:

puts second_response.output_text

This produces output like:

Dogs. They're more people-oriented and reward-driven; cats can be trained but usually take more patience and different incentives.

The conversation continues naturally because GPT-5 can see the full context of our previous exchange, allowing it to provide a focused answer about training specifically.

Enabling Enhanced Reasoning

Although GPT-5 includes reasoning with medium effort by default, we can explicitly control the reasoning behavior to suit our needs. Let's continue the conversation using custom reasoning settings to see how summaries can appear:

# Append GPT-5's second response to messages
messages << text_message("assistant", second_response.output_text)

# Append a third user message
messages << text_message("user", "What about grooming requirements?")

# Send the third request with enhanced reasoning
third_response = client.responses.create(
  model: model,
  input: [
    text_message("developer", developer_prompt),
    *messages
  ],
  reasoning: {
    effort: "low",
    summary: "auto"
  },
  store: false
)

The reasoning parameter allows us to override the default behavior. Setting effort to "low" reduces the computational effort GPT-5 invests in thinking through the problem compared to the default medium level, while summary set to "auto" tells GPT-5 to automatically generate a summary of its reasoning process when appropriate. This gives us fine-grained control over the balance between response speed, depth of reasoning, and visibility into the model's reasoning summary.

Examining Reasoning Responses

Let's examine the full response structure to see both a reasoning summary and the final answer:

# Print the whole response as JSON first
puts JSON.pretty_generate(third_response.to_h)

You will see output that includes both a reasoning block with a summary and a message block with the final answer:

{
  "id": "resp_059f964efaf0ab3a...",
  "created_at": 1760037917,
  "model": "gpt-5",
  "object": "response",
  "output": [
    {
      "id": "rs_059f964efaf0ab3a...",
      "summary": [
        {
          "text": "**Comparing Pet Grooming**\n\nI want to keep it concise while comparing grooming for cats and dogs. Cats usually self-groom, but they benefit from weekly brushing, especially long-haired ones, along with occasional nail trims and rare baths. For dogs, grooming needs vary by breed: regular baths and brushing are essential, and some breeds might require professional grooming every 4–8 weeks.",
          "type": "summary_text"
        }
      ],
      "type": "reasoning"
    },
    {
      "id": "msg_059f964efaf0ab3a...",
      "content": [
        {
          "annotations": [],
          "text": "- Cats: Mostly self-groom. Brush weekly (long-haired: daily), trim nails, occasional baths, dental care. Generally lower maintenance.\n- Dogs: Need regular brushing and baths; nail trims and ear cleaning. Many breeds require professional grooming every 4–8 weeks. Shedding and upkeep vary widely by breed.",
          "type": "output_text",
          "logprobs": []
        }
      ],
      "role": "assistant",
      "status": "completed",
      "type": "message"
    }
  ],
  "status": "completed",
  "usage": {
    "input_tokens": 117,
    "output_tokens": 200,
    "output_tokens_details": {
      "reasoning_tokens": 128
    },
    "total_tokens": 317
  },
  "store": false
}

Notice how the response now contains two output blocks: a reasoning block showing GPT-5's internal thought process as a summary, and a message block with the polished final answer. The usage section also breaks down token consumption, showing that 128 tokens were used for reasoning.

When reasoning is enabled, you can access the summary through the hash returned by to_h, but output_text still gives you just the final answer:

# Print the response content
puts third_response.output_text

This produces the clean final output:

- Cats: Mostly self-groom. Brush weekly (long-haired: daily), trim nails, occasional baths, dental care. Generally lower maintenance.
- Dogs: Need regular brushing and baths; nail trims and ear cleaning. Many breeds require professional grooming every 4–8 weeks. Shedding and upkeep vary widely by breed.

The reasoning summary provides insight into how GPT-5 approached the problem, which can be valuable for debugging and understanding the model's decision-making process, especially when building complex agent workflows.

Summary & Next Steps

You have now learned how to interact with the OpenAI Responses API in Ruby: building typed message hashes with the text_message helper (which selects input_text or output_text based on the role), passing a developer message directly inside the input array alongside conversation history, sending requests with client.responses.create, inspecting full response data via response.to_h and JSON.pretty_generate, extracting just the reply text with output_text, maintaining multi-turn history with messages <<, and tuning reasoning behavior with the reasoning parameter.

These core patterns — role-aware typed content blocks, the developer message as part of input, hash-based response inspection, and optional reasoning summaries — form the foundation for the agent workflows you will build in the rest of this course.

In the upcoming practices, you will gain hands-on experience building upon these concepts and exploring different ways to interact with GPT-5. This foundation will serve you well as we progress through more advanced topics in the course!

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal