Custom Commands homeCustom Commands
  • Blog
  • Premium
  • Templates
  • Support
Back to blog
// blog

How to call an API from a Discord custom command using fetch

Use fetch inside a custom command to call any web API from Discord — GET and POST, headers, JSON bodies, query parameters, and the per-run request limits.

Md Shahriyar Alam

an hour ago

·7 min read

fetch asks a website for something and gives you the answer, so your command can reply with live information — a weather forecast, a game's player count, a row from your own API.

Everything below goes in the code box of a slash command or a text command.


Quickstart

python
data = await fetch("https://api.github.com/repos/hikari-py/hikari")

response = f"⭐ {data['stargazers_count']} stars"

That is the whole thing. fetch returns the parsed JSON, and you use it like any other value.

Always await it. Without the await you get a promise of an answer rather than the answer.


What you get back

to decides the shape of the result. It is "json" unless you say otherwise.

python
data = await fetch(url)                  # a dict or list, parsed from JSON
text = await fetch(url, to="text")       # a plain string
raw  = await fetch(url, to="bytes")      # raw bytes, for files
toYou getUse it for
"json"dict or listAPIs
"text"stringplain text, HTML, CSV
"bytes"bytesimages and other files

When something goes wrong you get None

fetch returns None rather than stopping your command when:

  • the site answers with an error (404, 500, anything 400 and above)
  • the answer was not valid JSON but you asked for "json"
  • the site could not be reached

So check before you use it:

python
data = await fetch("https://api.example.com/thing")

if not data:
    set_error("That service is not answering right now.")

response = data["title"]

Skipping the check is the most common way these commands break — the site has a bad five minutes and your command throws instead of saying something useful.


Methods

python
await fetch(url)                       # GET, the default
await fetch(url, method="POST")
await fetch(url, method="PUT")
await fetch(url, method="PATCH")
await fetch(url, method="DELETE")
await fetch(url, method="HEAD")
await fetch(url, method="OPTIONS")

Anything else is an error.


Headers, query strings and bodies

Extra options are passed straight through to the request.

Headers — for API keys

python
data = await fetch(
    "https://api.example.com/me",
    headers={"Authorization": f"Bearer {server_var('api_key')}"},
)

Keep the key in a server variable, not typed into the command. Anyone who can see the command can see what is written in it.

Query parameters

python
data = await fetch(
    "https://api.example.com/search",
    params={"q": "pikachu", "limit": 5},
)

Cleaner than gluing the URL together yourself, and it escapes odd characters for you.

Sending JSON

python
await fetch(
    "https://api.example.com/events",
    method="POST",
    json={"user": user.id, "action": "joined"},
)

Sending a form

python
await fetch(url, method="POST", data={"name": "value"})

Asking for the same thing twice is free

Inside one command run, the same GET is only sent once. Ask for it a thousand times in a loop and the site is contacted once; every later call is answered from memory.

python
for name in ["pikachu", "pikachu", "pikachu"]:
    data = await fetch(f"https://pokeapi.co/api/v2/pokemon/{name}")
    # one real request, not three

This lasts for that one run only. The next time the command is used, it asks the site again — so your data is never stale.

POST, PUT, PATCH and DELETE are never reused. Those change something on the other end, so running one twice is what you asked for.


When you want a fresh answer every time

Some links answer differently every time you ask — a random quote, a live score, a dice roll. Reusing the first answer would break those, so tell fetch not to:

python
for _ in range(3):
    quote = await fetch("https://api.example.com/random-quote", dynamic=True)
    # three real requests, three different quotes

dynamic=True never reads from memory and never writes to it, so every call really goes out — and every call counts against your requests for that run. Five on a free server, twenty on premium.

Use it only where the answer genuinely changes. On a link that returns the same thing, it just spends your requests faster.

What happens
(nothing)the same GET is reused within the run — free
dynamic=Trueevery call goes out, every call counts

Limits

LimitFree serverPremium server
Different requests per run520
Response size5 MB100 MB
Time per request20 seconds120 seconds
Redirects followed44

Only different requests count. Repeating one you already made in that run is free, so a loop over the same link costs a single slot.

Go over and you get a plain message:

code
Maximum requests made (5). Only different requests count -- asking for the
same one again is free.

A response bigger than the size cap stops with a message too. The time limit is scaled to the size limit, so a large download has room to finish; a site that never answers at all gives up after five seconds rather than hanging your command.


What is not allowed

Only http and https links.

Addresses inside our own network are refused, and redirects are checked at every hop, so a link that bounces towards one is stopped as well. You will see:

code
That address is not reachable from here.

This is not about your server — it stops the bot being used to reach things it can see and you should not.


Examples

A weather command

python
data = await fetch(
    "https://api.open-meteo.com/v1/forecast",
    params={"latitude": 51.5, "longitude": -0.12, "current_weather": True},
)

if not data:
    set_error("The weather service is not answering.")

now = data["current_weather"]
response = f"🌡️ {now['temperature']}°C, wind {now['windspeed']} km/h"

Posting to your own API

python
result = await fetch(
    "https://my-site.example/api/tickets",
    method="POST",
    headers={"Authorization": f"Bearer {server_var('api_key')}"},
    json={"opened_by": str(user.id), "subject": subject},
)

response = "Ticket opened ✅" if result else "Could not open the ticket."

Downloading a picture and posting it

python
picture = await fetch("https://picsum.photos/400/300.jpg", to="bytes")

if not picture:
    set_error("Could not get a picture.")

add_attachment(Attachment(file=picture, filename="random.jpg"))

Several APIs in one command

python
sources = {
    "Cats": "https://api.thecatapi.com/v1/images/search",
    "Dogs": "https://dog.ceo/api/breeds/image/random",
}

lines = []

for name, url in sources.items():
    data = await fetch(url)
    lines.append(f"{name}: {'ok' if data else 'not answering'}")

response = "\n".join(lines)

Two different links, so two of your five.


Frequently asked questions

How do I call an API from a Discord bot command?

data = await fetch("https://..."). It returns parsed JSON by default, and None if the site errors, so check the result before using it.

How do I send an API key?

Put it in a server variable and pass it as a header: headers={"Authorization": f"Bearer {server_var('api_key')}"}. Do not type the key into the command itself, where anyone who can edit the command can read it.

How many API calls can one command make?

Five different requests on a free server, twenty on premium. Repeating a request you already made in that run does not count.

Why does my command say the maximum was reached?

You are calling five different links in one run. Fetch what you need once and reuse the value, or ask about premium.

Why is my result None?

The site answered with an error, could not be reached, or did not send valid JSON when you asked for JSON. Try to="text" to see what it really sent.

Can I POST to a webhook?

Yes: await fetch(url, method="POST", json={...}). POSTs are never reused, so each one is really sent.

My API gives a different answer every time — why do I keep seeing the first one?

Within one run, the same GET is answered from memory so a loop does not hammer the site. Add dynamic=True for a link whose answer really does change. Each of those calls counts against your requests for that run.

Is anything remembered after my command finishes?

No. Everything fetch keeps is thrown away when the command ends, so the next run always asks the site again. Nothing is shared between commands or between servers.

Loading comments…

{}

Need help?

Have a question, a suggestion, or stuck on something? Reach out — we're happy to help.

Join support server
Custom CommandsCustom Commands

The #1 custom commands Discord bot — build commands, events and databases with zero boilerplate.

Links

HomeTemplatesPremiumBlogPrivacySupport

Contact

[email protected]

2 Frederick StreetLondon, WC1X 0ND

{ / } custom commands

© WEiRDSOFT LTD. All rights reserved.