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.
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.
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 tabWhat 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
{
"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.
candidatesis an array because the model can return several alternative replies. You almost always want the first one,candidates[0].partsis an array because a response can mix things — text, a function call, inline data. Plain text answers land inparts[0].text.finishReasontells you why it stopped.STOPis normal.MAX_TOKENSmeans the answer was cut off.SAFETYmeans it was blocked.usageMetadatacounts 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.