Skip to content

Inline mode

Users can interact with bots using inline queries, by starting a message with bot's username and then typing their query.

You can learn more about them in Bot API docs

Example of an inline botExample of an inline bot

Handling inline queries

To handle inline queries to your bot, simply use onInlineQuery:

ts
dp.onInlineQuery(async (query) => {
  // ...
})
dp.onInlineQuery(async (query) => {
  // ...
})

You can also use filters.regex to filter based on the query text:

ts
dp.onInlineQuery(
  filters.regex(/^cats /),
  async (query) => {
    // ...
  }
)
dp.onInlineQuery(
  filters.regex(/^cats /),
  async (query) => {
    // ...
  }
)

Answering to inline queries

Answering an inline query means sending results of the query to Telegram. The results may contain any media, including articles, photos, videos, stickers, etc, and can also be heterogeneous, which means that you can use different media types in results for the same query.

To answer a query, simply use .answer() and pass an array that contains results:

ts
dp.onInlineQuery(async (query) => {
  query.answer([...])
})
dp.onInlineQuery(async (query) => {
  query.answer([...])
})

TIP

Clients wait about 10 seconds for results, after which they assume the bot timed out. Make sure not to do heavy computations there!

Results

Every inline result must contain a unique result ID, which can be later used in chosen inline result updates updates to determine which one was chosen.

Media inside inline results must be provided via either HTTP URL, or re-used from Telegram (e.g. using File IDs). Telegram does not allow uploading new files directly to inline results.

When choosing a result, by default, a message containing the respective result is sent, but the message contents can be customized using message field.

Telegram supports a bunch of result types, and mtcute supports sending all of them as an inline result. In the below examples we'll be using direct URLs to content, but you can also use File IDs instead.

Article

An article is a result that contains a title, description and optionally a thumbnail and a URL:

Article result exampleArticle result example

ts
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.article(
      'RESULT_ID',
      {
        title: 'Article title',
        description: 'Article description',
        thumb: 'https://example.com/image.jpg',
        url: 'https://example.com/some-article.html'
      }
    )
  ])
})
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.article(
      'RESULT_ID',
      {
        title: 'Article title',
        description: 'Article description',
        thumb: 'https://example.com/image.jpg',
        url: 'https://example.com/some-article.html'
      }
    )
  ])
})

When choosing this result, by default, a message with the following text is sent (Handlebars syntax is used here):

handlebars
{{#if url}}
<a href="{{url}}"><b>{{title}}</b></a>
{{else}}
<b>{{title}}</b>
{{/if}}
{{#if description}}
{{description}}
{{/if}}
{{#if url}}
<a href="{{url}}"><b>{{title}}</b></a>
{{else}}
<b>{{title}}</b>
{{/if}}
{{#if description}}
{{description}}
{{/if}}

For the above example, this would result in the following message:

GIF

You can send an animated GIF (either real GIF, or an MP4 without sound) as a result.

GIF result exampleGIF result example

ts
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.gif(
      'RESULT_ID',
      'https://media.tenor.com/videos/98bf1db10cb172aae086b09ae88ebf22/mp4'
    )
  ])
})
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.gif(
      'RESULT_ID',
      'https://media.tenor.com/videos/98bf1db10cb172aae086b09ae88ebf22/mp4'
    )
  ])
})

You can also add title and description, however only some clients display them (e.g. Telegram Desktop doesn't, screenshot below is from Telegram for Android)

GIF result with titleGIF result with title

ts
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.gif(
      'RESULT_ID',
      'https://media.tenor.com/videos/98bf1db10cb172aae086b09ae88ebf22/mp4',
      {
          title: 'GIF title',
          description: 'GIF description',
      }
    )
  ])
})
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.gif(
      'RESULT_ID',
      'https://media.tenor.com/videos/98bf1db10cb172aae086b09ae88ebf22/mp4',
      {
          title: 'GIF title',
          description: 'GIF description',
      }
    )
  ])
})

Video

You can send an MP4 video as an inline result.

When sending by direct URL, there's a file size limit of 20 MB, and you must provide a thumbnail, otherwise the result will be ignored. Thumbnail is only used until the video file is cached by Telegram, and is then overridden by Telegram-generated video thumbnail.

Video result exampleVideo result example

ts
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.video(
      'RESULT_ID',
      'https://amvnews.ru/index.php?go=Files&file=down&id=1858&alt=4',
      {
        thumb:
          'https://amvnews.ru/images/news098/1257019986-Bad-Apple21_5.jpg',
        title: 'Video title',
        description: 'Video description',
      }
    )
  ])
})
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.video(
      'RESULT_ID',
      'https://amvnews.ru/index.php?go=Files&file=down&id=1858&alt=4',
      {
        thumb:
          'https://amvnews.ru/images/news098/1257019986-Bad-Apple21_5.jpg',
        title: 'Video title',
        description: 'Video description',
      }
    )
  ])
})

Alternatively, you can send a video by its URL (e.g. from YouTube) using isEmbed: true:

Video page result exampleVideo page result example

ts
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.video(
      'RESULT_ID',
      'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
      {
        isEmbed: true,
        thumb: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg',
        title: 'Video title',
        description: 'Video description',
      }
    )
  ])
})
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.video(
      'RESULT_ID',
      'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
      {
        isEmbed: true,
        thumb: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg',
        title: 'Video title',
        description: 'Video description',
      }
    )
  ])
})

Choosing such result would send a message containing URL to that video:

Audio

You can send an MPEG audio file as an inline result.

When sending by direct URL, there's a file size limit of 20 MB.

Audio result exampleAudio result example

ts
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.audio(
      'RESULT_ID',
      'https://vk.com/mp3/cc_ice_melts.mp3',
      {
        performer: 'Griby',
        title: 'Tayet Lyod',
      }
    )
  ])
})
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.audio(
      'RESULT_ID',
      'https://vk.com/mp3/cc_ice_melts.mp3',
      {
        performer: 'Griby',
        title: 'Tayet Lyod',
      }
    )
  ])
})

NOTE

Performer, title and other meta can't be changed once the file is cached by Telegram (they will still be displayed in the results, but not in the message). To avoid caching when sending by URL, add a random query parameter (e.g. ?notgcache), which will make Telegram think this is a new file.

Voice

You can send an OGG file as a voice note inline result.

Waveform seems to only be generated for OGG files encoded with OPUS, so if it is not generated, try re-encoding your file with OPUS.

Voice result exampleVoice result example

ts
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.voice(
      'RESULT_ID',
      'https://tei.su/test_voice.ogg',
      {
        title: 'Voice title',
      }
    )
  ])
})
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.voice(
      'RESULT_ID',
      'https://tei.su/test_voice.ogg',
      {
        title: 'Voice title',
      }
    )
  ])
})

Photo

You can send an image as an inline result.

Photo result examplePhoto result example

ts
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.photo(
      'RESULT_ID',
      'https://nyanpa.su/renge.jpg'
    )
  ])
})
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.photo(
      'RESULT_ID',
      'https://nyanpa.su/renge.jpg'
    )
  ])
})

You can also add title and description, however only some clients display them (e.g. Telegram Desktop doesn't, screenshot below is from Telegram for Android)

Photo result with titlePhoto result with title

ts
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.photo(
      'RESULT_ID',
      'https://nyanpa.su/renge.jpg',
      {
        title: 'Photo title',
        description: 'Photo description',
      }
    )
  ])
})
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.photo(
      'RESULT_ID',
      'https://nyanpa.su/renge.jpg',
      {
        title: 'Photo title',
        description: 'Photo description',
      }
    )
  ])
})

Sticker

You can send a sticker as an inline result. You can't send a sticker by URL (Telegram limitation), only by File ID.

Sticker result exampleSticker result example

ts
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.sticker(
      'RESULT_ID',
      'CAACAgIAAxk...JtzysqiUK3IAQ'
    )
  ])
})
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.sticker(
      'RESULT_ID',
      'CAACAgIAAxk...JtzysqiUK3IAQ'
    )
  ])
})

File

You can send a file as an inline result.

Due to Telegram limitations, when using URLs, you can only send PDF and ZIP files, and must set mime accordingly (application/pdf and application/zip MIMEs respectively). With File IDs, you can send any file.

File result exampleFile result example

ts
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.file(
      'RESULT_ID',
      'https://file-examples-com.github.io/uploads/2017/10/file-sample_150kB.pdf',
      {
        mime: 'application/pdf',
        title: 'File title',
        description: 'File description'
      }
    )
  ])
})
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.file(
      'RESULT_ID',
      'https://file-examples-com.github.io/uploads/2017/10/file-sample_150kB.pdf',
      {
        mime: 'application/pdf',
        title: 'File title',
        description: 'File description'
      }
    )
  ])
})

Geolocation

You can send a geolocation as an inline result.

By default, Telegram generates thumb for result based on the location provided.

Geo result exampleGeo result example

ts
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.geo(
      'RESULT_ID',
      {
        latitude: 55.751999,
        longitude: 37.617734,
        title: 'Kremlin'
      }
    ),
  ])
})
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.geo(
      'RESULT_ID',
      {
        latitude: 55.751999,
        longitude: 37.617734,
        title: 'Kremlin'
      }
    ),
  ])
})

Venue

You can send a venue as an inline result.

By default, Telegram generates thumb for result based on the location provided.

Venue result exampleVenue result example

ts
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.venue(
      'RESULT_ID',
      {
        latitude: 55.751999,
        longitude: 37.617734,
        title: 'Kremlin',
        address: 'Red Square'
      }
    ),
  ])
})
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.venue(
      'RESULT_ID',
      {
        latitude: 55.751999,
        longitude: 37.617734,
        title: 'Kremlin',
        address: 'Red Square'
      }
    ),
  ])
})

Contact

You can send a contact as an inline result.

Contact result exampleContact result example

ts
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.contact(
      'RESULT_ID',
      {
        firstName: 'Alice',
        phone: '+79001234567',
        thumb: 'https://avatars.githubusercontent.com/u/86301490'
      }
    ),
  ])
})
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.contact(
      'RESULT_ID',
      {
        firstName: 'Alice',
        phone: '+79001234567',
        thumb: 'https://avatars.githubusercontent.com/u/86301490'
      }
    ),
  ])
})

Games

Finally, you can send a game as an inline result.

Game result exampleGame result example

ts
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.game('RESULT_ID', 'game_short_name'),
  ])
})
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.game('RESULT_ID', 'game_short_name'),
  ])
})

Custom message

By default, mtcute generates a message based on result contents. However, you can override the message that will be sent using message

Text

Instead of media or default article message, you may want to send a custom text message:

ts
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.photo(
      'RESULT_ID',
      'https://nyanpa.su/renge.jpg',
      {
        message: BotInlineMessage.text(
          'Ha-ha, just kidding. No Renge for you :p'
        )
      }
    )
  ])
})
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.photo(
      'RESULT_ID',
      'https://nyanpa.su/renge.jpg',
      {
        message: BotInlineMessage.text(
          'Ha-ha, just kidding. No Renge for you :p'
        )
      }
    )
  ])
})

Media

You can customize media message (for photos, videos, voices, documents, etc.) with custom caption, keyboard, etc:

ts
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.photo(
      'RESULT_ID',
      'https://nyanpa.su/renge.jpg',
      {
        message: BotInlineMessage.media({ text: 'Nyanpasu!' }),
      }
    )
  ])
})
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.photo(
      'RESULT_ID',
      'https://nyanpa.su/renge.jpg',
      {
        message: BotInlineMessage.media({ text: 'Nyanpasu!' }),
      }
    )
  ])
})

Sadly, Telegram does not allow sending another media instead of the one you provided in the result (however, you could use web previews to accomplish similar results)

Geolocation

Instead of sending the default message, you can send geolocation, or even live geolocation:

ts
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.photo(
      'RESULT_ID',
      'https://nyanpa.su/renge.jpg',
      {
        // or BotInlineMessage.geoLive
        message: BotInlineMessage.geo({
          latitude: 55.751999,
          longitude: 37.617734,
        }),
      }
    )
  ])
})
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.photo(
      'RESULT_ID',
      'https://nyanpa.su/renge.jpg',
      {
        // or BotInlineMessage.geoLive
        message: BotInlineMessage.geo({
          latitude: 55.751999,
          longitude: 37.617734,
        }),
      }
    )
  ])
})

Venue

Instead of sending the default message, you can send a venue

ts
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.photo(
      'RESULT_ID',
      'https://nyanpa.su/renge.jpg',
      {
        message: BotInlineMessage.venue({
          latitude: 55.751999,
          longitude: 37.617734,
          title: 'Kremlin',
          address: 'Red Square'
        }),
      }
    )
  ])
})
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.photo(
      'RESULT_ID',
      'https://nyanpa.su/renge.jpg',
      {
        message: BotInlineMessage.venue({
          latitude: 55.751999,
          longitude: 37.617734,
          title: 'Kremlin',
          address: 'Red Square'
        }),
      }
    )
  ])
})

Contact

Instead of sending the default message, you can send a contact

ts
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.photo(
      'RESULT_ID',
      'https://nyanpa.su/renge.jpg',
      {
        message: BotInlineMessage.contact({
          firstName: 'Alice',
          phone: '+79001234567',
        }),
      }
    )
  ])
})
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.photo(
      'RESULT_ID',
      'https://nyanpa.su/renge.jpg',
      {
        message: BotInlineMessage.contact({
          firstName: 'Alice',
          phone: '+79001234567',
        }),
      }
    )
  ])
})

Game

For inline results containing a game, you can customize keyboard under the message (learn more):

ts
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.game('RESULT_ID', 'test', {
      message: BotInlineMessage.game({
        replyMarkup: ...
      })
    }),
  ])
})
dp.onInlineQuery(async (query) => {
  query.answer([
    BotInline.game('RESULT_ID', 'test', {
      message: BotInlineMessage.game({
        replyMarkup: ...
      })
    }),
  ])
})

Switch to PM

Some bots may benefit from switching to PM with the bot for some action (e.g. logging in with your account). For that, you can use switchPm button along with your results:

Switch to PM exampleSwitch to PM example

ts
dp.onInlineQuery(async (query) => {
  query.answer([], {
    switchPm: {
      text: 'Log in',
      parameter: 'login_inline'
    }
  })
})

dp.onNewMessage(filters.deeplink('login_inline'), async (msg) => {
    await msg.answerText('Thanks for logging in!')
})
dp.onInlineQuery(async (query) => {
  query.answer([], {
    switchPm: {
      text: 'Log in',
      parameter: 'login_inline'
    }
  })
})

dp.onNewMessage(filters.deeplink('login_inline'), async (msg) => {
    await msg.answerText('Thanks for logging in!')
})

Chosen inline results

When a user selects an inline result, and assuming that you have inline feedback feature enabled, an update is received, which can be handled:

ts
dp.onChosenInlineResult(async (result) => {
  await result.editMessage({
    text: `${result.user.displayName}, thanks for using inline!`
  })
})
dp.onChosenInlineResult(async (result) => {
  await result.editMessage({
    text: `${result.user.displayName}, thanks for using inline!`
  })
})

You can use filters.regex to filter by chosen result ID:

ts
dp.onChosenInlineResult(
  filters.regex(/^CATS_/),
  async (result) => {
    // ...
  }
)
dp.onChosenInlineResult(
  filters.regex(/^CATS_/),
  async (result) => {
    // ...
  }
)

These updates are only sent by Telegram when you have enabled inline feedback feature. You can enable it in @BotFather.

It is however noted by Telegram that this should only be used for statistical purposes, and even if probability setting is 100%, not all chosen inline results may be reported (source).

Editing inline message

You can edit an inline message in Chosen inline query update handlers, and in Callback query updates:

NOTE

In the below examples, it is assumed that callback query originates from an inline message.

ts
dp.onChosenInlineResult(async (result) => {
  await result.editMessage({...})
})

dp.onCallbackQuery(async (query) => {
  await query.editMessage({...})
})
dp.onChosenInlineResult(async (result) => {
  await result.editMessage({...})
})

dp.onCallbackQuery(async (query) => {
  await query.editMessage({...})
})

You can also save inline message ID and edit the message later:

ts
function updateMessageLater(msgId: string) {
  setTimeout(() => {
    tg.editInlineMessage(msgId, {...})
      .catch(console.error)
  }, 5000)
}

dp.onChosenInlineResult(async (result) => {
  updateMessageLater(result.messageIdStr)
})

dp.onCallbackQuery(async (query) => {
  updateMessageLater(query.inlineMessageIdStr)
})
function updateMessageLater(msgId: string) {
  setTimeout(() => {
    tg.editInlineMessage(msgId, {...})
      .catch(console.error)
  }, 5000)
}

dp.onChosenInlineResult(async (result) => {
  updateMessageLater(result.messageIdStr)
})

dp.onCallbackQuery(async (query) => {
  updateMessageLater(query.inlineMessageIdStr)
})

TIP

messageIdStr and inlineMessageIdStr contain string representation of the inline message ID, to simplify its storage.

You can also use the messageId and inlineMessageId respectively, which contain a TL object, in case you need to use Raw API. In mtcute, they are interchangeable.

You can also edit media inside inline messages, and even upload new media directly to them (unlike Bot API):

ts
dp.onChosenInlineResult(async (result) => {
  const link = await getDownloadLink(result.id)

  await result.editMessage({
    media: InputMedia.audio(await fetch(link))
  })
})
dp.onChosenInlineResult(async (result) => {
  const link = await getDownloadLink(result.id)

  await result.editMessage({
    media: InputMedia.audio(await fetch(link))
  })
})

mtcute is not affiliated with Telegram.