{"course":{"slug":"python-for-ai","title":"Python, from Zero, for AI","url":"https://addaly.com/learn/python-for-ai","level":"beginner","track":"build"},"lesson":{"slug":"first-api-call","title":"Calling an API for the first time","url":"https://addaly.com/learn/python-for-ai/first-api-call","position":49,"of":89,"minutes":9,"answer":"The status code tells you the request and reply worked; only the body tells you what the answer says.","keyIdea":"The status code tells you the request and reply worked; only the body tells you what the answer says."},"body":{"format":"markdown","text":"## An API is a front desk for a program\n\nWhen you open a web page, your browser sends a request to a server and gets a page back. An API is the same conversation, but the reply is data meant for a program rather than a layout meant for eyes. You send a request. You get a response. The response has a status code and a body, and the body is usually JSON, which lesson eight showed you how to turn into a dict.\n\nThat is the entire idea. Everything else is detail.\n\n## A call you can run right now\n\nInside an activated virtual environment with `requests` installed:\n\n```python\nimport requests\n\nr = requests.get(\"https://api.github.com/repos/python/cpython\", timeout=10)\n\nprint(r.status_code)\ndata = r.json()\nprint(data[\"full_name\"])\nprint(data[\"stargazers_count\"])\nprint(data[\"language\"])\n```\n\nNo account, no key. Four things to notice.\n\n`requests.get` sends the request and waits. `timeout=10` says give up after ten seconds; without it a program can hang indefinitely, and a hang is harder to debug than a failure. Always pass a timeout.\n\n`r.status_code` is the server's one-word summary of how the conversation went:\n\n- `200`: fine\n- `401`: your credentials are missing or wrong\n- `403`: recognised, but not allowed\n- `404`: no such thing at that address\n- `429`: too many requests, slow down\n- `500` and up: the server broke, not you\n\n`r.json()` parses the response body into Python dicts and lists. It fails if the body is not JSON, which is what happens when an error page comes back as HTML. If you are unsure what arrived, print `r.text` first.\n\nAdd `r.raise_for_status()` after the call and any 4xx or 5xx becomes an exception instead of quietly flowing into code that expects data.\n\n## Sending data, and sending a key\n\nAI APIs need two more things: you send a body with `POST` instead of asking with `GET`, and you identify yourself with a key.\n\n```python\nimport os\nimport requests\n\nAPI_KEY = os.environ[\"ANTHROPIC_API_KEY\"]\nMODEL = \"claude-sonnet-4-5\"   # model ids change; check the provider's docs\n\nr = requests.post(\n    \"https://api.anthropic.com/v1/messages\",\n    headers={\n        \"x-api-key\": API_KEY,\n        \"anthropic-version\": \"2023-06-01\",\n        \"content-type\": \"application/json\",\n    },\n    json={\n        \"model\": MODEL,\n        \"max_tokens\": 300,\n        \"messages\": [\n            {\"role\": \"user\", \"content\": \"Explain gravity in one sentence.\"}\n        ],\n    },\n    timeout=60,\n)\n\nprint(r.status_code)\nreply = r.json()\nprint(reply[\"content\"][0][\"text\"])\n```\n\nYou have seen every piece of that before. `headers` is a dict. `json=` takes a dict and `requests` converts it for you. The `messages` value is a list of dicts, exactly the structure you built in lessons four and six. The reply is a dict, and `reply[\"content\"][0][\"text\"]` is a lookup, a position, and another lookup.\n\nThis one costs money and needs an account, so it may not be the one you run today. Every provider's shape differs slightly; read their docs for the exact keys. The pattern does not differ.\n\n## Keys\n\nA key is a password that spends your money. Three rules.\n\nNever type it into your code. Put it in the environment instead:\n\n```bash\nexport ANTHROPIC_API_KEY=\"sk-...\"     # macOS or Linux\n$env:ANTHROPIC_API_KEY = \"sk-...\"     # Windows PowerShell\n```\n\nNever commit it. If a key has ever been in a file you pushed anywhere public, treat it as leaked and replace it at the provider. Deleting the line in a later commit does not help; the old commit is still in the history, and automated scanners find published keys within minutes.\n\nNever paste it into a chat, a screenshot, or a support ticket.\n\n## What the status code does and does not tell you\n\nA `200` means the request was well formed and the server answered. It says nothing about whether the answer is true, complete, or sensible.\n\nAn AI model can return a confident, well-written, wrong answer with a `200`. It can also stop mid-sentence because it hit your `max_tokens` limit, and that is still a `200`, with a field in the response body such as `stop_reason` telling you why it ended. The status code describes the delivery. The body is where the content, and any problem with the content, lives.\n\n## Try this now\n\nRun the GitHub call. Change `cpython` to something that does not exist and print the status code. Then wrap it:\n\n```python\nimport requests\n\ndef repo_stars(full_name):\n    r = requests.get(f\"https://api.github.com/repos/{full_name}\", timeout=10)\n    if r.status_code != 200:\n        return None\n    return r.json()[\"stargazers_count\"]\n\nfor name in [\"python/cpython\", \"pallets/flask\", \"no/such-repo\"]:\n    print(name, repo_stars(name))\n```\n\nA function, a loop, a dict, a list, a conversion, and an API call. That is the whole course in nine lines."},"check":{"question":"You call an AI API. `r.status_code` is 200, and the text in the response contains a confident claim that turns out to be false. What does the 200 actually tell you?","options":[{"text":"That the answer arrived whole, since an answer cut off partway through would come back with a different status code.","correct":false,"tempting":"Truncation feels like a failure, so it seems it ought to be reported at the status level rather than buried in a field in the body."},{"text":"That the model was confident in what it said, because a low-confidence answer would come back as a 4xx.","correct":false,"tempting":"Status codes are the API's way of flagging problems, so it is easy to assume an uncertain answer counts as a problem worth flagging."},{"text":"That the request was well formed and the server replied; it says nothing at all about whether the content is correct.","correct":true},{"text":"That the provider checked the response before sending it, since a server would not return success for output it had not validated.","correct":false,"tempting":"Success is a strong word, and it is reassuring to imagine something on the other end is vouching for what it sends you."}]},"prev":{"slug":"secrets-and-environment","title":"API keys: out of the code, out of the repository","url":"https://addaly.com/learn/python-for-ai/secrets-and-environment"},"next":{"slug":"anatomy-of-a-request","title":"What a request is made of, and how to see the one you actually sent","url":"https://addaly.com/learn/python-for-ai/anatomy-of-a-request"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Calling an API for the first time', in Python, from Zero, for AI. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/python-for-ai/first-api-call (Accessed: 31 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Calling an API for the first time","courseTitle":"Python, from Zero, for AI","module":{"index":6,"slug":"talking-to-a-service","title":"Talking to a service: HTTP from Python, done properly"},"answer":null,"keyIdea":"The status code tells you the request and reply worked; only the body tells you what the answer says.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"an-api-is-a-front-desk-for-a-program","runs":[{"text":"An API is a front desk for a program"}]},{"t":"p","runs":[{"text":"When you open a web page, your browser sends a request to a server and gets a page back. An API is the same conversation, but the reply is data meant for a program rather than a layout meant for eyes. You send a request. You get a response. The response has a status code and a body, and the body is usually JSON, which lesson eight showed you how to turn into a dict."}]},{"t":"p","runs":[{"text":"That is the entire idea. Everything else is detail."}]},{"t":"h2","id":"a-call-you-can-run-right-now","runs":[{"text":"A call you can run right now"}]},{"t":"p","runs":[{"text":"Inside an activated virtual environment with "},{"text":"requests","code":true},{"text":" installed:"}]},{"t":"code","lang":"python","label":"Python","text":"import requests\n\nr = requests.get(\"https://api.github.com/repos/python/cpython\", timeout=10)\n\nprint(r.status_code)\ndata = r.json()\nprint(data[\"full_name\"])\nprint(data[\"stargazers_count\"])\nprint(data[\"language\"])"},{"t":"p","runs":[{"text":"No account, no key. Four things to notice."}]},{"t":"p","runs":[{"text":"requests.get","code":true},{"text":" sends the request and waits. "},{"text":"timeout=10","code":true},{"text":" says give up after ten seconds; without it a program can hang indefinitely, and a hang is harder to debug than a failure. Always pass a timeout."}]},{"t":"p","runs":[{"text":"r.status_code","code":true},{"text":" is the server’s one-word summary of how the conversation went:"}]},{"t":"ul","items":[[{"text":"200","code":true},{"text":": fine"}],[{"text":"401","code":true},{"text":": your credentials are missing or wrong"}],[{"text":"403","code":true},{"text":": recognised, but not allowed"}],[{"text":"404","code":true},{"text":": no such thing at that address"}],[{"text":"429","code":true},{"text":": too many requests, slow down"}],[{"text":"500","code":true},{"text":" and up: the server broke, not you"}]]},{"t":"p","runs":[{"text":"r.json()","code":true},{"text":" parses the response body into Python dicts and lists. It fails if the body is not JSON, which is what happens when an error page comes back as HTML. If you are unsure what arrived, print "},{"text":"r.text","code":true},{"text":" first."}]},{"t":"p","runs":[{"text":"Add "},{"text":"r.raise_for_status()","code":true},{"text":" after the call and any 4xx or 5xx becomes an exception instead of quietly flowing into code that expects data."}]},{"t":"h2","id":"sending-data-and-sending-a-key","runs":[{"text":"Sending data, and sending a key"}]},{"t":"p","runs":[{"text":"AI APIs need two more things: you send a body with "},{"text":"POST","code":true},{"text":" instead of asking with "},{"text":"GET","code":true},{"text":", and you identify yourself with a key."}]},{"t":"code","lang":"python","label":"Python","text":"import os\nimport requests\n\nAPI_KEY = os.environ[\"ANTHROPIC_API_KEY\"]\nMODEL = \"claude-sonnet-4-5\"   # model ids change; check the provider's docs\n\nr = requests.post(\n    \"https://api.anthropic.com/v1/messages\",\n    headers={\n        \"x-api-key\": API_KEY,\n        \"anthropic-version\": \"2023-06-01\",\n        \"content-type\": \"application/json\",\n    },\n    json={\n        \"model\": MODEL,\n        \"max_tokens\": 300,\n        \"messages\": [\n            {\"role\": \"user\", \"content\": \"Explain gravity in one sentence.\"}\n        ],\n    },\n    timeout=60,\n)\n\nprint(r.status_code)\nreply = r.json()\nprint(reply[\"content\"][0][\"text\"])"},{"t":"p","runs":[{"text":"You have seen every piece of that before. "},{"text":"headers","code":true},{"text":" is a dict. "},{"text":"json=","code":true},{"text":" takes a dict and "},{"text":"requests","code":true},{"text":" converts it for you. The "},{"text":"messages","code":true},{"text":" value is a list of dicts, exactly the structure you built in lessons four and six. The reply is a dict, and "},{"text":"reply[\"content\"][0][\"text\"]","code":true},{"text":" is a lookup, a position, and another lookup."}]},{"t":"p","runs":[{"text":"This one costs money and needs an account, so it may not be the one you run today. Every provider’s shape differs slightly; read their docs for the exact keys. The pattern does not differ."}]},{"t":"h2","id":"keys","runs":[{"text":"Keys"}]},{"t":"p","runs":[{"text":"A key is a password that spends your money. Three rules."}]},{"t":"p","runs":[{"text":"Never type it into your code. Put it in the environment instead:"}]},{"t":"code","lang":"bash","label":"Bash","text":"export ANTHROPIC_API_KEY=\"sk-...\"     # macOS or Linux\n$env:ANTHROPIC_API_KEY = \"sk-...\"     # Windows PowerShell"},{"t":"p","runs":[{"text":"Never commit it. If a key has ever been in a file you pushed anywhere public, treat it as leaked and replace it at the provider. Deleting the line in a later commit does not help; the old commit is still in the history, and automated scanners find published keys within minutes."}]},{"t":"p","runs":[{"text":"Never paste it into a chat, a screenshot, or a support ticket."}]},{"t":"h2","id":"what-the-status-code-does-and-does-not-tell-you","runs":[{"text":"What the status code does and does not tell you"}]},{"t":"p","runs":[{"text":"A "},{"text":"200","code":true},{"text":" means the request was well formed and the server answered. It says nothing about whether the answer is true, complete, or sensible."}]},{"t":"p","runs":[{"text":"An AI model can return a confident, well-written, wrong answer with a "},{"text":"200","code":true},{"text":". It can also stop mid-sentence because it hit your "},{"text":"max_tokens","code":true},{"text":" limit, and that is still a "},{"text":"200","code":true},{"text":", with a field in the response body such as "},{"text":"stop_reason","code":true},{"text":" telling you why it ended. The status code describes the delivery. The body is where the content, and any problem with the content, lives."}]},{"t":"h2","id":"try-this-now","runs":[{"text":"Try this now"}],"role":"try"},{"t":"p","runs":[{"text":"Run the GitHub call. Change "},{"text":"cpython","code":true},{"text":" to something that does not exist and print the status code. Then wrap it:"}]},{"t":"code","lang":"python","label":"Python","text":"import requests\n\ndef repo_stars(full_name):\n    r = requests.get(f\"https://api.github.com/repos/{full_name}\", timeout=10)\n    if r.status_code != 200:\n        return None\n    return r.json()[\"stargazers_count\"]\n\nfor name in [\"python/cpython\", \"pallets/flask\", \"no/such-repo\"]:\n    print(name, repo_stars(name))"},{"t":"p","runs":[{"text":"A function, a loop, a dict, a list, a conversion, and an API call. That is the whole course in nine lines."}]}],"check":{"question":[{"text":"You call an AI API. "},{"text":"r.status_code","code":true},{"text":" is 200, and the text in the response contains a confident claim that turns out to be false. What does the 200 actually tell you?"}],"options":[{"text":[{"text":"That the answer arrived whole, since an answer cut off partway through would come back with a different status code."}],"correct":false,"tempting":[{"text":"Truncation feels like a failure, so it seems it ought to be reported at the status level rather than buried in a field in the body."}]},{"text":[{"text":"That the model was confident in what it said, because a low-confidence answer would come back as a 4xx."}],"correct":false,"tempting":[{"text":"Status codes are the API’s way of flagging problems, so it is easy to assume an uncertain answer counts as a problem worth flagging."}]},{"text":[{"text":"That the request was well formed and the server replied; it says nothing at all about whether the content is correct."}],"correct":true,"tempting":null},{"text":[{"text":"That the provider checked the response before sending it, since a server would not return success for output it had not validated."}],"correct":false,"tempting":[{"text":"Success is a strong word, and it is reassuring to imagine something on the other end is vouching for what it sends you."}]}],"order":[0,1,2,3],"why":null},"prev":{"slug":"secrets-and-environment","title":"API keys: out of the code, out of the repository","minutes":8},"next":{"slug":"anatomy-of-a-request","title":"What a request is made of, and how to see the one you actually sent","minutes":8}}}