Interact with Midjourney using Discord API • Part II
Published: 2023-10-16 01:49:29

### Introduction  

This article builds upon [Part I](https://useapi.net/docs/articles/discord-api-part-1) and covers Discord [rate limits](https://discord.com/developers/docs/topics/rate-limits) and the Midjourney moderation system, two of the most intricate topics. We've provided working code for your convenience.

Before we proceed, make sure you have a Discord account with an active Midjourney subscription. The $10 Basic Plan will suffice.

Follow these [simple steps](https://useapi.net/docs/start-here/setup-midjourney) to obtain:
- Discord server id number, referenced in this article as `server_id`
- Discord channel id number, referenced in this article as `channel_id`
- Discord token, referenced in this article as `discord_token`

### Discord rate limits

Discord's [original documentation](https://discord.com/developers/docs/topics/rate-limits) suggests using response headers to detect cases where the rate limit is exceeded and adjust accordingly. While this approach is appropriate for complex or commercial applications, we'll opt for a simpler yet equally efficient method for this article.  

We'll introduce a 350-millisecond pause (sleep) before each Discord API call. Since our code operates with a single thread, this will be sufficient to remain comfortably under the limit. Moreover, our small program's overall performance will not be impacted, as Midjourney's response time consistently exceeds the proposed 350 milliseconds.

### Midjourney moderation system

Midjourney performs both pre- and post-moderation of [/imagine](https://docs.midjourney.com/docs/quick-start#5-use-the-imagine-command) command prompts, and you'll need a reliable way to detect both.

When you execute a POST request for an imagine interaction with your prompt in a Discord channel (as covered in [Part I](https://useapi.net/docs/articles/discord-api-part-1)), you'll encounter _**seven**_ possible cases:

* 1 **Happy path**

After a few seconds, the posted message will appear in the channel with the original message `id`, indicating its status as `(Waiting to start)` in the `content` field and with the `type` field set to 0.
![](https://useapi.net/assets/images/articles/mj-moderaion-happy-path-waiting-to-start.png)

GET `https://discord.com/api/v10/channels/channel_id/messages`

```json
[
 {
  "id": "<Discord original message id>",
  "type": 0,
  "content": "**Wonders of the World…** - <@Discord user id> (Waiting to start)",
  "channel_id": "<Discord channel id>",
```

Once Midjourney completes the generation, the original message will be deleted, and a final message with a new `id` containing the generation results will be posted by Midjourney.

![](https://useapi.net/assets/images/articles/mj-moderaion-happy-path-completed.jpg)

GET `https://discord.com/api/v10/channels/channel_id/messages`

```json
[
 {
  "id": "<Discord final message id>",
  "type": 0,
  "content": "**Wonders of the World…** - <@Discord user id> (fast)",
  "channel_id": "<Discord channel id>",
```

* 2 **Pre-moderation**

This is the most straightforward case: the posted message will never appear in the channel. You will see something like this.

![](https://useapi.net/assets/images/articles/mj-moderaion-pre-moderation.png)

<small>Messages marked "Only you can see this" can be retrieved by [Discord Gateway API](https://discord.com/developers/docs/topics/gateway)</small><sup>1</sup>

* 3 **Post-moderation** 

The post will appear in the channel just like in the "Happy path" case.

![](https://useapi.net/assets/images/articles/mj-moderaion-post-moderation-waiting.png)

GET `https://discord.com/api/v10/channels/channel_id/messages`

```json
[
 {
  "id": "<Discord original message id>",
  "type": 0,
  "content": "**Beautiful lady swimming in the pool…** - <@Discord user id> (Waiting to start)",
  "channel_id": "<Discord channel id>",
```

Generation may even start, and you will see the progress, but suddenly it will stop with a message similar to the one below.

![](https://useapi.net/assets/images/articles/mj-moderaion-post-moderation-stopped.png)

GET `https://discord.com/api/v10/channels/channel_id/messages`

```json
[
 {
  "id": "<Discord original message id>",
  "type": 0,
  "content": "**Beautiful lady swimming in the pool…** - <@Discord user id> (Stopped)",
  "channel_id": "<Discord channel id>",

```

* 4 **Ephemeral moderation** 

Certain words in your prompt trigger "soft moderation". Generation starts and completes successfully, but after completion, the message will display "Original message was deleted". The message is no longer returned by the GET `https://discord.com/api/v10/channels/channel_id/messages` endpoint, even though it was initially present. For a better understanding, refer to the sequence below.

![](https://useapi.net/assets/images/articles/mj-moderaion-ephemeral-start.png)

GET `https://discord.com/api/v10/channels/channel_id/messages`

```json
[
 {
  "id": "<Discord original message id>",
  "type": 0,
  "content": "**Irresistibly beautiful woman, pinup…** - <@Discord user id> (Waiting to start)",
  "channel_id": "<Discord channel id>",

```

Generated message deleted.

![](https://useapi.net/assets/images/articles/mj-moderaion-ephemeral-deleted.jpg)

<small>Messages marked "Only you can see this" can be retrieved by [Discord Gateway API](https://discord.com/developers/docs/topics/gateway)</small><sup>1</sup>

* 5 **Invalid request**

This occurs when you specify an incorrect parameter. Like the "Pre-moderation" case, the posted message will not appear in the channel. It's advisable to include code for checking the prompt syntax before posting. We plan to cover the topic of prompt syntax verification in one of our follow-up articles.

![](https://useapi.net/assets/images/articles/mj-moderaion-invalid-request.png)

<small>Messages marked "Only you can see this" can be retrieved by [Discord Gateway API](https://discord.com/developers/docs/topics/gateway)</small><sup>1</sup>

* 6 **Job queued**

Depending on your [Midjourney Subscription Plan](https://docs.midjourney.com/docs/plans), you can have anywhere from 3 (Basic & Standard) to 15 (Pro & Mega) concurrent job executions. Once this limit is reached, job requests will be queued, and messages associated with these jobs will not immediately appear in the GET `https://discord.com/api/v10/channels/channel_id/messages` until Midjourney has completed one of the executing jobs. This essentially means that the job has been placed into Midjourney's internal query.

![](https://useapi.net/assets/images/articles/mj-moderaion-job-queued.png)

<small>Messages marked "Only you can see this" can be retrieved by [Discord Gateway API](https://discord.com/developers/docs/topics/gateway)</small><sup>1</sup>

You generally want to minimize encountering the "Job queued" status, as there is substantial evidence that Midjourney may throttle your generation requests in such cases and use it as an indicator of a [Midjourney ToS](https://docs.midjourney.com/docs/terms-of-service) violation (<small>...may not use automated tools to access, interact with, or generate Assets through the Services...</small>).

* 7 **Queue full**

Midjourney job queue can hold up to 10 jobs, after that you will get "Queue full" message. You will have to wait and try again.

![](https://useapi.net/assets/images/articles/mj-moderaion-queue-full.png)

<small>Messages marked "Only you can see this" can be retrieved by [Discord Gateway API](https://discord.com/developers/docs/topics/gateway)</small><sup>1</sup>

You generally want to minimize encountering the "Queue full" status, as there is substantial evidence that Midjourney may throttle your generation requests in such cases and use it as an indicator of a [Midjourney ToS](https://docs.midjourney.com/docs/terms-of-service) violation (<small>...may not use automated tools to access, interact with, or generate Assets through the Services...</small>).

### Discord REST API (HTTPS) vs Discord Gateway API (WebSocket) 

As you noticed in the previous paragraph, certain messages <sup>1</sup> can still be retrieved using the [Discord Gateway API](https://discord.com/developers/docs/topics/gateway). Unlike the [Discord REST API](https://discord.com/developers/docs/reference) we're using in this article, the Discord Gateway API requires the use of [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) to facilitate real-time communication between your client application and the Discord server. This adds another level of complexity and, more importantly, may increase the chances of getting banned. 

In the vast majority of cases, you can build your logic without the use of the Discord Gateway API. We plan to cover corner cases where the use of the Discord Gateway API is the only feasible solution in later articles.

### Midjourney /imagine generation logic

Taking into account what we learned from the previous paragraphs, we can implement the following simple strategy:

1. Ensure there is at least one available concurrent job slot. To achieve this, you can either use a separate Midjourney account for all your automation work or carefully manage the number of manually initiated jobs, keeping them to a maximum of two. This approach will help avoid encountering both "Job queued" and "Queue full" cases.

2. Before posting an image prompt, execute a GET request to `https://discord.com/api/v10/channels/channel_id/messages` to retrieve the `id` of the very first record. By default, this endpoint returns the 50 most recent messages from the channel, sorted in descending order, with the most recent message listed first.

3. After posting the image prompt using a POST request to `https://discord.com/api/v10/interactions`, continuously check the channel every 3 to 5 seconds by making a GET request to `https://discord.com/api/v10/channels/channel_id/messages`. Compare the `id` of the very first record with the value obtained in the previous step. If, after 30 seconds, you do not see a new record in the channel, you can assume you are either dealing with a "Pre-moderation" or "Invalid request" case, and your prompt may require revision.

4. Once you have determined that a new message has indeed appeared in your channel, save the `id` of this message. Continue calling GET requests to `https://discord.com/api/v10/channels/channel_id/messages` every 20 to 30 seconds to check for the following remaining cases:

* Happy path: when the generation is completed, the message with the original `id` is no longer present in the channel. Instead, a new message is present with a non-empty components array. You can obtain the generated image from the `attachments[0].url field`.
* Post-moderation: when the message with the original `id` is still present, and its `content` field ends with `(Stopped)`.
* Ephemeral moderation: when the `id` of the very first record matches the value obtained before posting the image prompt.

### Working JavaScript code

```javascript
// bash
// USEAPI_DISCORD="..." USEAPI_SERVER="..." USEAPI_CHANNEL="..." node index.js

// Node 18+

const imagine_prompt = "Hologram cat in neon lights";

const discordAPI = `https://discord.com/api/v10`;
const MidjourneyAppId = `936929561302675456`;

// Load Discord settings from environment 
const discord = process.env.USEAPI_DISCORD ?? 'Discord token';
const server = process.env.USEAPI_SERVER ?? 'Discord server id number';
const channel = process.env.USEAPI_CHANNEL ?? 'Discord channel id number';

console.info({ discord, server, channel, imagine_prompt });

const sleep = (ms = 0) => new Promise(resolve => setTimeout(resolve, ms));

const DiscordHeaders = (token) => ({
    "Content-Type": "application/json",
    "Authorization": `${token}`,
});

// https://discord.com/developers/docs/resources/channel#get-channel-messages
const GetDiscordChannelMessages = async (discord, channel) => {
    const response = await fetch(
        `${discordAPI}/channels/${channel}/messages`,
        { headers: DiscordHeaders(discord) });

    return response;
}

// Midjourney Imagine https://discord.com/api/v10/channels/channel_id/application-commands/search?type=1&include_applications=true&query=imagine
const PostDiscordImagine = async (discord, server, channel, prompt) => {
    const data = {
        "type": 2,
        "application_id": MidjourneyAppId,
        "guild_id": server,
        "channel_id": channel,
        "session_id": (new Date()).getTime(),
        "data": {
            "version": "1118961510123847772",
            "id": "938956540159881230",
            "name": "imagine",
            "type": 1,
            "options": [
                {
                    "type": 3,
                    "name": "prompt",
                    "value": prompt
                }
            ],
            "application_command": {
                "id": "938956540159881230",
                "application_id": MidjourneyAppId,
                "version": "1118961510123847772",
                "default_permission": true,
                "default_member_permissions": null,
                "type": 1,
                "nsfw": false,
                "name": "imagine",
                "description": "Create images with Midjourney",
                "dm_permission": true,
                "options": [
                    {
                        "type": 3,
                        "name": "prompt",
                        "description": "The prompt to imagine",
                        "required": true
                    }
                ]
            },
            "attachments": []
        }
    };

    const response = await fetch(`${discordAPI}/interactions`, {
        method: "POST",
        body: JSON.stringify(data),
        headers: DiscordHeaders(discord)
    });

    return response;
}

const demo = async () => {
    const getBeforeMessages = await GetDiscordChannelMessages(discord, channel);

    if (getBeforeMessages.status !== 200) {
        console.error(`Discord /messages status ${getBeforeMessages.status}`, await getBeforeMessages.json());
        process.exit(1);
    }

    const beforeMessages = await getBeforeMessages.json();

    const beforeIds = new Set();
    beforeMessages.forEach(msg => beforeIds.add(msg.id));

    await sleep(350);

    const postImagine = await PostDiscordImagine(discord, server, channel, imagine_prompt);

    if (postImagine.status != 204) {
        console.error(`Discord /interactions status ${postImagine.status}`, await postImagine.json());
        process.exit(1);
    }

    const maxPostedAttempts = 10;

    let attempt = 1;
    let postedMessage;

    // Check for message to appear in the channel for 20 seconds total 
    do {
        await sleep(2000);

        const getMessages = await GetDiscordChannelMessages(discord, channel);

        if (getMessages.status !== 200) {
            console.error(`Discord /messages status ${getMessages.status}`, await getMessages.body());
            process.exit(1);
        }

        const messages = await getMessages.json();

        // New (Waiting to start) imagine interaction
        postedMessage = messages.find(msg => !beforeIds.has(msg.id));

        if (postedMessage) {
            console.log(`Found new message ${postedMessage.id}`, postedMessage.content);
            break;
        }

        attempt++;

    } while (attempt <= maxPostedAttempts);

    if (postedMessage === undefined) {
        console.error(`Posted message not found due to moderation or an invalid prompt`);
        process.exit(1);
    }

    const termStopped = `> (Stopped)`;
    const maxGeneratedAttempts = 60;

    attempt = 1;

    let generatedMessage;

    // Check for posted message to appear in the channel with new id for 10 minutes max
    do {
        // Wait for 10 seconds before checking on message progress
        await sleep(10000);

        const getMessages = await GetDiscordChannelMessages(discord, channel);

        if (getMessages.status !== 200) {
            console.error(`Discord /messages status ${getMessages.status}`, await getMessages.body());
            process.exit(1);
        }

        const messages = await getMessages.json();

        const progress = messages.find(message =>
            message.id == postedMessage.id &&
            // Not completed 
            !message.components?.length &&
            // Not stopped 
            !message.content.endsWith(termStopped));
        if (progress) {
            console.log(`#${attempt} ${progress.id} progress`, progress.content);
        } else {
            const completed = messages.find(message =>
                !beforeIds.has(message.id) &&
                message.id !== postedMessage.id &&
                // Either completed or stopped
                (!!message.components?.length || message.content.endsWith(termStopped))
            );

            generatedMessage = (completed && !!completed.components?.length) ? completed : undefined;

            break;
        }

        attempt++;

    } while (attempt <= maxGeneratedAttempts);

    if (generatedMessage === undefined) {
        console.error(`Message not found due to post-moderation or ephemeral moderation`);
        process.exit(1);
    }

    // Successful generation
    console.info(`Completed`, generatedMessage.content);
    console.info(`Download URL`, generatedMessage.attachments[0].url);
}

demo();
```

Subscribe to stay informed.

Please visit https://useapi.net to learn more about Midjourney API