Purrx

Your first Gemini API call

Send a prompt to Gemini, read the response envelope, and pull the text out of it. The one call every other lesson builds on.

10 min+25 XPHands-on

Time to make the model say something. The you will use for almost everything is generateContent: you send it your prompt, and it sends back the model's reply.

The playground's gemini helper

Inside every Purrx playground you have a gemini object. It is a thin wrapper around Gemini's . It is deliberately thin, so the requests you write here look exactly like the real ones you would send from your own app.

available in every exercise
await gemini.generateContent({ contents: "Hello" })   // the main one
await gemini.countTokens({ contents: "Hello" })       // token counting
await gemini.embedContent({ contents: "Hello" })      // embeddings

gemini.text(response)          // shortcut: pull text out of a response
gemini.cosineSimilarity(a, b)  // used in the embeddings lesson
console.log(...)               // shows up in the Output tab

What comes back

The response is not a string. It is an : an object with the text buried inside, plus extra information. Learning its shape now saves confusion in every later lesson:

response

candidates[0] · the first (usually only) reply

content.parts[0]

text: "A large language model is…"

← the answer you want

finishReason: "STOP"

← why it stopped. Anything else means the answer is incomplete.

usageMetadata: { totalTokenCount: 39 }

← tokens used, which is what you pay for

The response is nested boxes. Your text sits four levels deep, next to why the model stopped and what it cost.
the same response as raw JSON (trimmed)
{
  "candidates": [
    {
      "content": {
        "role": "model",
        "parts": [ { "text": "A large language model is…" } ]
      },
      "finishReason": "STOP"
    }
  ],
  "usageMetadata": {
    "promptTokenCount": 11,
    "candidatesTokenCount": 28,
    "totalTokenCount": 39
  }
}
Under the hood — Why is it called an "envelope"?

Nothing Gemini-specific — it is a general API design convention. Instead of returning the payload on its own, the response wraps it in an outer object alongside metadata: status, usage, alternatives, why it stopped.

You have seen it before. A Stripe response wraps a charge in an object with an id and a livemode flag; a GitHub list response wraps items in an object with pagination. The benefit is room to grow: fields can be added to the wrapper without breaking anyone parsing the payload inside it. The cost is that you always dig one or two levels to reach what you actually asked for.

  • candidates is an array because the model can return several alternative replies. You almost always want the first one, candidates[0].
  • parts is an array because a response can mix things — text, a function call, inline data. Plain text answers land in parts[0].text.
  • finishReason tells you why it stopped. STOP is normal. MAX_TOKENS means the answer was cut off. SAFETY means it was blocked.
  • usageMetadata counts the the prompt and answer used. That is what you are billed for.

Your turn

Complete ask() below so it sends the prompt and returns just the text. Run it and watch the Calls tab — you will see the exact request that went out and the full response that came back.

Key takeaways

  • generateContent takes your prompt and returns a response object, not a plain string.
  • The text is at candidates[0].content.parts[0].text, but check each step exists before reading it.
  • finishReason says why the model stopped. Anything other than STOP means the answer is incomplete.

Sign in to run the exercise

Reading is free. Writing code here needs an account so we have somewhere to keep your Gemini key and the +25 XP you are about to earn.