diff --git a/config/config_test.go b/config/config_test.go
index f74b8fcc..a3dd2c13 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -2960,7 +2960,7 @@ func TestWaybackMeiliEndpoint(t *testing.T) {
 	}{
 		{
 			endpoint: "",
-			expected: defWaybackMeiliEndpoint,
+			expected: defMeiliEndpoint,
 		},
 		{
 			endpoint: "https://example.com",
@@ -2979,7 +2979,7 @@ func TestWaybackMeiliEndpoint(t *testing.T) {
 				t.Fatalf(`Parsing environment variables failed: %v`, err)
 			}
 
-			got := opts.WaybackMeiliEndpoint()
+			got := opts.MeiliEndpoint()
 			if got != test.expected {
 				t.Fatalf(`Unexpected set meilisearch endpoint got %s instead of %s`, got, test.expected)
 			}
@@ -2996,7 +2996,7 @@ func TestWaybackMeiliIndexing(t *testing.T) {
 	}{
 		{
 			indexing: "",
-			expected: defWaybackMeiliIndexing,
+			expected: defMeiliIndexing,
 		},
 		{
 			indexing: "foo-bar",
@@ -3015,7 +3015,7 @@ func TestWaybackMeiliIndexing(t *testing.T) {
 				t.Fatalf(`Parsing environment variables failed: %v`, err)
 			}
 
-			got := opts.WaybackMeiliIndexing()
+			got := opts.MeiliIndexing()
 			if got != test.expected {
 				t.Fatalf(`Unexpected set meilisearch indexing got %s instead of %s`, got, test.expected)
 			}
@@ -3032,7 +3032,7 @@ func TestWaybackMeiliApikey(t *testing.T) {
 	}{
 		{
 			apikey:   "",
-			expected: defWaybackMeiliApikey,
+			expected: defMeiliApikey,
 		},
 		{
 			apikey:   "foo.bar",
@@ -3051,7 +3051,7 @@ func TestWaybackMeiliApikey(t *testing.T) {
 				t.Fatalf(`Parsing environment variables failed: %v`, err)
 			}
 
-			got := opts.WaybackMeiliApikey()
+			got := opts.MeiliApikey()
 			if got != test.expected {
 				t.Fatalf(`Unexpected set meilisearch api key got %s instead of %s`, got, test.expected)
 			}
@@ -3095,6 +3095,78 @@ func TestEnabledMeilisearch(t *testing.T) {
 	}
 }
 
+func TestOmnivoreApikey(t *testing.T) {
+	t.Parallel()
+
+	var tests = []struct {
+		apikey   string
+		expected string
+	}{
+		{
+			apikey:   "",
+			expected: defOmnivoreApikey,
+		},
+		{
+			apikey:   "foo.bar",
+			expected: "foo.bar",
+		},
+	}
+
+	for i, test := range tests {
+		t.Run(strconv.Itoa(i), func(t *testing.T) {
+			os.Clearenv()
+			os.Setenv("WAYBACK_OMNIVORE_APIKEY", test.apikey)
+
+			parser := NewParser()
+			opts, err := parser.ParseEnvironmentVariables()
+			if err != nil {
+				t.Fatalf(`Parsing environment variables failed: %v`, err)
+			}
+
+			got := opts.OmnivoreApikey()
+			if got != test.expected {
+				t.Fatalf(`Unexpected set Omnivore api key got %s instead of %s`, got, test.expected)
+			}
+		})
+	}
+}
+
+func TestEnabledOmnivore(t *testing.T) {
+	t.Parallel()
+
+	var tests = []struct {
+		apikey   string
+		expected bool
+	}{
+		{
+			apikey:   "",
+			expected: false,
+		},
+		{
+			apikey:   "foo-bar",
+			expected: true,
+		},
+	}
+
+	for i, test := range tests {
+		t.Run(strconv.Itoa(i), func(t *testing.T) {
+			os.Clearenv()
+			os.Setenv("WAYBACK_OMNIVORE_APIKEY", test.apikey)
+
+			parser := NewParser()
+			opts, err := parser.ParseEnvironmentVariables()
+			if err != nil {
+				t.Fatalf(`Parsing environment variables failed: %v`, err)
+			}
+
+			got := opts.EnabledOmnivore()
+			if got != test.expected {
+				t.Fatalf(`Unexpected enabled meilisearch got %t instead of %t`, got, test.expected)
+			}
+		})
+	}
+}
+
 func TestEnableServices(t *testing.T) {
 	tests := []struct {
 		name     string
diff --git a/config/options.go b/config/options.go
index e9a93b3e..22dd655e 100644
--- a/config/options.go
+++ b/config/options.go
@@ -98,9 +98,11 @@ const (
 	defWaybackFallback     = false
 	defProxy               = ""
 
-	defWaybackMeiliEndpoint = ""
-	defWaybackMeiliIndexing = "capsules"
-	defWaybackMeiliApikey   = ""
+	defMeiliEndpoint = ""
+	defMeiliIndexing = "capsules"
+	defMeiliApikey   = ""
+
+	defOmnivoreApikey = ""
 
 	maxAttachSizeTelegram = 50000000   // 50MB
 	maxAttachSizeDiscord  = 8000000    // 8MB
@@ -141,6 +143,8 @@ type Options struct {
 	irc      *irc
 	onion    *onion
 	xmpp     *xmpp
+	omnivore *omnivore
+	meili    *meili
 
 	listenAddr          string
 	chromeRemoteAddr    string
@@ -154,10 +158,6 @@ type Options struct {
 	waybackMaxRetries   int
 	waybackUserAgent    string
 	waybackFallback     bool
-
-	waybackMeiliEndpoint string
-	waybackMeiliIndexing string
-	waybackMeiliApikey   string
 }
 
 type ipfs struct {
@@ -253,28 +253,35 @@ type xmpp struct {
 	helptext string
 }
 
+type meili struct {
+	endpoint string
+	indexing string
+	apikey   string
+}
+
+type omnivore struct {
+	apikey string
+}
+
 // NewOptions returns Options with default values.
 func NewOptions() *Options {
 	opts := &Options{
-		debug:                defDebug,
-		logTime:              defLogTime,
-		logLevel:             defLogLevel,
-		overTor:              defOverTor,
-		metrics:              defMetrics,
-		listenAddr:           defListenAddr,
-		chromeRemoteAddr:     defChromeRemoteAddr,
-		enabledChromeRemote:  defEnabledChromeRemote,
-		boltPathname:         defBoltPathname,
-		poolingSize:          defPoolingSize,
-		storageDir:           defStorageDir,
-		maxMediaSize:         defMaxMediaSize,
-		waybackTimeout:       defWaybackTimeout,
-		waybackMaxRetries:    defWaybackMaxRetries,
-		waybackUserAgent:     defWaybackUserAgent,
-		waybackFallback:      defWaybackFallback,
-		waybackMeiliEndpoint: defWaybackMeiliEndpoint,
-		waybackMeiliIndexing: defWaybackMeiliIndexing,
-		waybackMeiliApikey:   defWaybackMeiliApikey,
+		debug:               defDebug,
+		logTime:             defLogTime,
+		logLevel:            defLogLevel,
+		overTor:             defOverTor,
+		metrics:             defMetrics,
+		listenAddr:          defListenAddr,
+		chromeRemoteAddr:    defChromeRemoteAddr,
+		enabledChromeRemote: defEnabledChromeRemote,
+		boltPathname:        defBoltPathname,
+		poolingSize:         defPoolingSize,
+		storageDir:          defStorageDir,
+		maxMediaSize:        defMaxMediaSize,
+		waybackTimeout:      defWaybackTimeout,
+		waybackMaxRetries:   defWaybackMaxRetries,
+		waybackUserAgent:    defWaybackUserAgent,
+		waybackFallback:     defWaybackFallback,
 		ipfs: &ipfs{
 			host:   defIPFSHost,
 			port:   defIPFSPort,
@@ -357,6 +364,14 @@ func NewOptions() *Options {
 			noTLS:    defXMPPNoTLS,
 			helptext: defXMPPHelptext,
 		},
+		meili: &meili{
+			endpoint: defMeiliEndpoint,
+			indexing: defMeiliIndexing,
+			apikey:   defMeiliApikey,
+		},
+		omnivore: &omnivore{
+			apikey: defOmnivoreApikey,
+		},
 	}
 
 	return opts
@@ -926,24 +941,34 @@ func (o *Options) WaybackFallback() bool {
 	return o.waybackFallback
 }
 
-// WaybackMeiliEndpoint returns the Meilisearch API endpoint.
-func (o *Options) WaybackMeiliEndpoint() string {
-	return o.waybackMeiliEndpoint
+// MeiliEndpoint returns the Meilisearch API endpoint.
+func (o *Options) MeiliEndpoint() string {
+	return o.meili.endpoint
 }
 
-// WaybackMeiliIndexing returns the Meilisearch indexing name.
-func (o *Options) WaybackMeiliIndexing() string {
-	return o.waybackMeiliIndexing
+// MeiliIndexing returns the Meilisearch indexing name.
+func (o *Options) MeiliIndexing() string {
+	return o.meili.indexing
 }
 
-// WaybackMeiliApikey returns the Meilisearch admin apikey.
-func (o *Options) WaybackMeiliApikey() string {
-	return o.waybackMeiliApikey
+// MeiliApikey returns the Meilisearch admin apikey.
+func (o *Options) MeiliApikey() string {
+	return o.meili.apikey
 }
 
 // EnabledMeilisearch returns whether enable meilisearch server.
 func (o *Options) EnabledMeilisearch() bool {
-	return o.WaybackMeiliEndpoint() != ""
+	return o.MeiliEndpoint() != ""
+}
+
+// OmnivoreApikey returns the Omnivore apikey.
+func (o *Options) OmnivoreApikey() string {
+	return o.omnivore.apikey
+}
+
+// EnabledOmnivore returns whether enable Omnivore.
+func (o *Options) EnabledOmnivore() bool {
+	return o.OmnivoreApikey() != ""
 }
 
 // HTTPdEnabled returns whether enable HTTP daemon service.
diff --git a/config/parser.go b/config/parser.go
index 73cb27cc..d3ede970 100644
--- a/config/parser.go
+++ b/config/parser.go
@@ -222,11 +222,13 @@ func (p *Parser) parseLines(lines []string) (err error) {
 		case "WAYBACK_FALLBACK":
 			p.opts.waybackFallback = parseBool(val, defWaybackFallback)
 		case "WAYBACK_MEILI_ENDPOINT":
-			p.opts.waybackMeiliEndpoint = parseString(val, defWaybackMeiliEndpoint)
+			p.opts.meili.endpoint = parseString(val, defMeiliEndpoint)
 		case "WAYBACK_MEILI_INDEXING":
-			p.opts.waybackMeiliIndexing = parseString(val, defWaybackMeiliIndexing)
+			p.opts.meili.indexing = parseString(val, defMeiliIndexing)
 		case "WAYBACK_MEILI_APIKEY":
-			p.opts.waybackMeiliApikey = parseString(val, defWaybackMeiliApikey)
+			p.opts.meili.apikey = parseString(val, defMeiliApikey)
+		case "WAYBACK_OMNIVORE_APIKEY":
+			p.opts.omnivore.apikey = parseString(val, defOmnivoreApikey)
 		default:
 			if os.Getenv(key) == "" && val != "" {
 				os.Setenv(key, val)
diff --git a/docs/environment.md b/docs/environment.md
index eb929a14..6c0116ab 100644
--- a/docs/environment.md
+++ b/docs/environment.md
@@ -40,6 +40,7 @@ Use the `-c` / `--config` option to specify the build definition file to use.
 | -                   | `WAYBACK_MEILI_ENDPOINT`          | -                          | Meilisearch API endpoint                                     |
 | -                   | `WAYBACK_MEILI_INDEXING`          | `capsules`                 | Meilisearch indexing name                                    |
 | -                   | `WAYBACK_MEILI_APIKEY`            | -                          | Meilisearch admin API key                                    |
+| -                   | `WAYBACK_OMNIVORE_APIKEY`         | -                          | Omnivore API key                                             |
 | `-d`, `--daemon`    | -                                 | -                          | Run as daemon service, e.g. `telegram`, `web`, `mastodon`, `twitter`, `discord` |
 | `--ia`              | `WAYBACK_ENABLE_IA`               | `true`                     | Wayback webpages to **Internet Archive**                     |
 | `--is`              | `WAYBACK_ENABLE_IS`               | `true`                     | Wayback webpages to **Archive Today**                        |
diff --git a/docs/integrations/omnivore.md b/docs/integrations/omnivore.md
new file mode 100644
index 00000000..044a76fd
--- /dev/null
+++ b/docs/integrations/omnivore.md
@@ -0,0 +1,11 @@
+---
+title: Publish to Omnivore
+---
+
+Omnivore lets you save and organize articles, newsletters, and documents for later reading. It also syncs with popular knowledge management systems and offers text-to-speech and distraction free features.
+
+## Configuration
+
+Create API key, place key in environment or configuration file:
+
+- `WAYBACK_OMNIVORE_APIKEY`: Omnivore API key.
diff --git a/docs/integrations/omnivore.zh.md b/docs/integrations/omnivore.zh.md
new file mode 100644
index 00000000..ab03f1e0
--- /dev/null
+++ b/docs/integrations/omnivore.zh.md
@@ -0,0 +1,11 @@
+---
+title: 发布到Omnivore
+---
+
+Omnivore 可让您保存和整理文章、简报和文档以供日后阅读。它还可与流行的知识管理系统同步,并提供文本转语音和无干扰功能。
+
+## 配置
+
+创建 API 密钥,将密钥放置在环境或配置文件中:
+
+- `WAYBACK_OMNIVORE_APIKEY`: Omnivore API key.
diff --git a/go.mod b/go.mod
index df6a6bb3..942471c9 100644
--- a/go.mod
+++ b/go.mod
@@ -16,7 +16,9 @@ require (
 	github.com/gabriel-vasile/mimetype v1.4.2
 	github.com/go-shiori/go-readability v0.0.0-20220215145315-dd6828d2f09b
 	github.com/go-shiori/obelisk v0.0.0-20230316095823-42f6a2f99d9d
+	github.com/goccy/go-json v0.10.3
 	github.com/google/go-github/v40 v40.0.0
+	github.com/google/uuid v1.3.0
 	github.com/gookit/color v1.5.3
 	github.com/gorilla/mux v1.8.0
 	github.com/gorilla/websocket v1.5.1
@@ -36,7 +38,6 @@ require (
 	github.com/wabarc/archive.is v1.4.0
 	github.com/wabarc/archive.org v1.2.1-0.20210708220121-cb9b83ff9896
 	github.com/wabarc/ghostarchive v0.1.1
-	github.com/wabarc/go-anonfile v0.1.0
 	github.com/wabarc/go-catbox v0.1.0
 	github.com/wabarc/helper v0.0.0-20230418130954-be7440352bcb
 	github.com/wabarc/imgbb v1.0.0
@@ -95,7 +96,6 @@ require (
 	github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/google/pprof v0.0.0-20240528025155-186aa0362fba // indirect
-	github.com/google/uuid v1.3.0 // indirect
 	github.com/iawia002/lia v0.0.0-20221116085912-1f653221be4b // indirect
 	github.com/inconshreveable/mousetrap v1.0.1 // indirect
 	github.com/ipfs/boxo v0.8.1 // indirect
diff --git a/go.sum b/go.sum
index 6476fbbc..6bc19ae4 100644
--- a/go.sum
+++ b/go.sum
@@ -169,6 +169,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
 github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
 github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q=
 github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
+github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
+github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
 github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
@@ -479,13 +481,10 @@ github.com/wabarc/archive.org v1.2.1-0.20210708220121-cb9b83ff9896 h1:c3uD+IKXpN
 github.com/wabarc/archive.org v1.2.1-0.20210708220121-cb9b83ff9896/go.mod h1:yEmUMlNO2PPAxIvo/Hf/VxOrCS5SBwL2/vCW8pyTWjA=
 github.com/wabarc/ghostarchive v0.1.1 h1:iGnDwvUixynKGlUTtxGfWAD1p6QRtCj7pAYCqgZNZTQ=
 github.com/wabarc/ghostarchive v0.1.1/go.mod h1:+HB72/CrKK4at+QhWDKRZYB6WLLyE/xlXr6/LgfqFro=
-github.com/wabarc/go-anonfile v0.1.0 h1:M4jKUAMROxxVaqLQt30ONmaHA/0YnvyJtX8qg/zI9+I=
-github.com/wabarc/go-anonfile v0.1.0/go.mod h1:CH1LzSKQ0x/RlKpEG2gi+hXaOOZrfeRx6Rp717o+65A=
 github.com/wabarc/go-catbox v0.1.0 h1:/UhV9md3MJrjZtm+EToSyFjawXgPiHSExLNRqsWNisg=
 github.com/wabarc/go-catbox v0.1.0/go.mod h1:Zjs9Y55f2WOwGWwmKSCrUuMfwh+nDktkjub9jgHq4CQ=
 github.com/wabarc/helper v0.0.0-20210127120855-10af37cc2616/go.mod h1:N9P4r7Rn46p4nkWtXV6ztN3p5ACVnp++bgfwjTqSxQ8=
 github.com/wabarc/helper v0.0.0-20210407153720-1bfe98b427fe/go.mod h1:TuTZtoiOu984UWOf7FfX58JllKMjq7FCz701kB5W88E=
-github.com/wabarc/helper v0.0.0-20210614160629-1a5ba5e551eb/go.mod h1:TuTZtoiOu984UWOf7FfX58JllKMjq7FCz701kB5W88E=
 github.com/wabarc/helper v0.0.0-20210701193643-e0fe0a807cb9/go.mod h1:TuTZtoiOu984UWOf7FfX58JllKMjq7FCz701kB5W88E=
 github.com/wabarc/helper v0.0.0-20210718171053-59c70d0b20c2/go.mod h1:uS6mimKlWkGvEZXkJ6JoW7LYnnB2JP6dLU9q7pgDaWQ=
 github.com/wabarc/helper v0.0.0-20230418130954-be7440352bcb h1:psEAY4wXvhXp/Hp5CJWgAOKWqhvAom+/hOjK+Qscx7o=
diff --git a/ingress/register/publish.go b/ingress/register/publish.go
index d7b07d08..c1ebe788 100644
--- a/ingress/register/publish.go
+++ b/ingress/register/publish.go
@@ -12,6 +12,7 @@ import (
 	_ "github.com/wabarc/wayback/publish/meili"
 	_ "github.com/wabarc/wayback/publish/nostr"
 	_ "github.com/wabarc/wayback/publish/notion"
+	_ "github.com/wabarc/wayback/publish/omnivore"
 	_ "github.com/wabarc/wayback/publish/relaychat"
 	_ "github.com/wabarc/wayback/publish/slack"
 	_ "github.com/wabarc/wayback/publish/telegram"
diff --git a/metrics/metrics.go b/metrics/metrics.go
index 49743a8e..3aee1ed4 100644
--- a/metrics/metrics.go
+++ b/metrics/metrics.go
@@ -26,17 +26,18 @@ const (
 	ServiceTwitter  = "twitter"
 	ServiceXMPP     = "xmpp"
 
-	PublishIRC     = "irc"      // IRC channel
-	PublishGithub  = "github"   // GitHub issues
-	PublishNotion  = "notion"   // Notion page
-	PublishChannel = "telegram" // Telegram channel
-	PublishMstdn   = "mastodon" // Mastodon toot
-	PublishDiscord = "discord"  // Discord channel
-	PublishTwitter = "twitter"
-	PublishMatrix  = "matrix"
-	PublishSlack   = "slack"
-	PublishNostr   = "nostr"
-	PublishMeili   = "meili"
+	PublishIRC      = "irc"      // IRC channel
+	PublishGithub   = "github"   // GitHub issues
+	PublishNotion   = "notion"   // Notion page
+	PublishChannel  = "telegram" // Telegram channel
+	PublishMstdn    = "mastodon" // Mastodon toot
+	PublishDiscord  = "discord"  // Discord channel
+	PublishTwitter  = "twitter"
+	PublishMatrix   = "matrix"
+	PublishSlack    = "slack"
+	PublishNostr    = "nostr"
+	PublishMeili    = "meili"
+	PublishOmnivore = "omnivore"
 
 	StatusRequest = "request"
 	StatusSuccess = "success"
diff --git a/publish/meili/meili.go b/publish/meili/meili.go
index 49bd16f6..8b19ae0d 100644
--- a/publish/meili/meili.go
+++ b/publish/meili/meili.go
@@ -55,7 +55,7 @@ type Meili struct {
 
 // New returns a Meilisearch client.
 func New(client *http.Client, opts *config.Options) *Meili {
-	endpoint, apikey, idxname := opts.WaybackMeiliEndpoint(), opts.WaybackMeiliApikey(), opts.WaybackMeiliIndexing()
+	endpoint, apikey, idxname := opts.MeiliEndpoint(), opts.MeiliApikey(), opts.MeiliIndexing()
 
 	if client == nil {
 		client = &http.Client{
diff --git a/publish/omnivore/doc.go b/publish/omnivore/doc.go
new file mode 100644
index 00000000..32bd1923
--- /dev/null
+++ b/publish/omnivore/doc.go
@@ -0,0 +1,5 @@
+// Copyright 2024 Wayback Archiver. All rights reserved.
+// Use of this source code is governed by the GNU GPL v3
+// license that can be found in the LICENSE file.
+
+package omnivore // import "github.com/wabarc/wayback/publish/omnivore"
diff --git a/publish/omnivore/omnivore.go b/publish/omnivore/omnivore.go
new file mode 100644
index 00000000..a5c6fc28
--- /dev/null
+++ b/publish/omnivore/omnivore.go
@@ -0,0 +1,153 @@
+// Copyright 2024 Wayback Archiver. All rights reserved.
+// Use of this source code is governed by the GNU GPL v3
+// license that can be found in the LICENSE file.
+
+package omnivore // import "github.com/wabarc/wayback/publish/omnivore"
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"io"
+	"net/http"
+	"time"
+
+	"github.com/goccy/go-json"
+	"github.com/google/uuid"
+	"github.com/wabarc/logger"
+	"github.com/wabarc/wayback"
+	"github.com/wabarc/wayback/config"
+	"github.com/wabarc/wayback/errors"
+	"github.com/wabarc/wayback/ingress"
+	"github.com/wabarc/wayback/metrics"
+	"github.com/wabarc/wayback/publish"
+	"github.com/wabarc/wayback/reduxer"
+)
+
+const (
+	defaultClientTimeout = 10 * time.Second
+	defaultApiEndpoint   = "https://api-prod.omnivore.app/api/graphql"
+)
+
+var mutation = `
+mutation SaveUrl($input: SaveUrlInput!) {
+  saveUrl(input: $input) {
+    ... on SaveSuccess {
+      url
+      clientRequestId
+    }
+    ... on SaveError {
+      errorCodes
+      message
+    }
+  }
+}
+`
+
+// Interface guard
+var _ publish.Publisher = (*Omnivore)(nil)
+
+type Omnivore struct {
+	bot  *http.Client
+	opts *config.Options
+}
+
+type errorResponse struct {
+	Errors []struct {
+		Message string `json:"message"`
+	} `json:"errors"`
+}
+
+type successResponse struct {
+	Data struct {
+		SaveUrl struct {
+			Url             string `json:"url"`
+			ClientRequestId string `json:"clientRequestId"`
+		} `json:"saveUrl"`
+	} `json:"data"`
+}
+
+// New returns a omnivore client.
+func New(client *http.Client, opts *config.Options) *Omnivore {
+	if opts.OmnivoreApikey() == "" {
+		logger.Debug("Onmnivore integration access token is required")
+		return nil
+	}
+
+	bot := ingress.Client()
+	if client != nil {
+		bot = client
+	}
+	bot.Timeout = defaultClientTimeout
+
+	return &Omnivore{bot: bot, opts: opts}
+}
+
+// Publish save url to the Omnivore of the given cols and args.
+func (o *Omnivore) Publish(_ context.Context, _ reduxer.Reduxer, cols []wayback.Collect, args ...string) error {
+	metrics.IncrementPublish(metrics.PublishOmnivore, metrics.StatusRequest)
+
+	if len(cols) == 0 {
+		metrics.IncrementPublish(metrics.PublishOmnivore, metrics.StatusFailure)
+		return errors.New("publish to omnivore: collects empty")
+	}
+
+	var payload = map[string]interface{}{
+		"query": mutation,
+		"variables": map[string]interface{}{
+			"input": map[string]interface{}{
+				"clientRequestId": uuid.NewString(),
+				"source":          "api",
+				"url":             cols[0].Src,
+			},
+		},
+	}
+	b, err := json.Marshal(payload)
+	if err != nil {
+		return err
+	}
+	req, err := http.NewRequest(http.MethodPost, defaultApiEndpoint, bytes.NewReader(b))
+	if err != nil {
+		return err
+	}
+
+	req.Header.Set("Authorization", o.opts.OmnivoreApikey())
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("User-Agent", o.opts.WaybackUserAgent())
+
+	resp, err := o.bot.Do(req)
+	if err != nil {
+		metrics.IncrementPublish(metrics.PublishOmnivore, metrics.StatusFailure)
+		return err
+	}
+
+	defer resp.Body.Close()
+	b, err = io.ReadAll(resp.Body)
+	if err != nil {
+		metrics.IncrementPublish(metrics.PublishOmnivore, metrics.StatusFailure)
+		return fmt.Errorf("omnivore: failed to parse response: %s", err)
+	}
+
+	if resp.StatusCode >= 400 {
+		metrics.IncrementPublish(metrics.PublishOmnivore, metrics.StatusFailure)
+		var errResponse errorResponse
+		if err = json.Unmarshal(b, &errResponse); err != nil {
+			return fmt.Errorf("omnivore: failed to save URL: status=%d %s", resp.StatusCode, string(b))
+		}
+		return fmt.Errorf("omnivore: failed to save URL: status=%d %s", resp.StatusCode, errResponse.Errors[0].Message)
+	}
+
+	var successReponse successResponse
+	if err = json.Unmarshal(b, &successReponse); err != nil {
+		metrics.IncrementPublish(metrics.PublishOmnivore, metrics.StatusFailure)
+		return fmt.Errorf("omnivore: failed to parse response, however the request appears successful, is the url correct?: status=%d %s", resp.StatusCode, string(b))
+	}
+
+	metrics.IncrementPublish(metrics.PublishOmnivore, metrics.StatusSuccess)
+	return nil
+}
+
+// Shutdown shuts down the Omnivore publish service, it always return a nil error.
+func (o *Omnivore) Shutdown() error {
+	return nil
+}
diff --git a/publish/omnivore/omnivore_test.go b/publish/omnivore/omnivore_test.go
new file mode 100644
index 00000000..3f83510a
--- /dev/null
+++ b/publish/omnivore/omnivore_test.go
@@ -0,0 +1,60 @@
+// Copyright 2024 Wayback Archiver. All rights reserved.
+// Use of this source code is governed by the GNU GPL v3
+// license that can be found in the LICENSE file.
+
+package omnivore // import "github.com/wabarc/wayback/publish/omnivore"
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/wabarc/helper"
+	"github.com/wabarc/wayback/config"
+	"github.com/wabarc/wayback/publish"
+	"github.com/wabarc/wayback/reduxer"
+)
+
+const saveURLResp = `{"data":{"saveUrl":{"url":"https://omnivore.app/repo/links/cff02ab5-c36e-4efe-a976-2de32dc1685d","clientRequestId":"cff02ab5-c36e-4efe-a976-2de32dc1685d"}}}`
+
+func TestPublish(t *testing.T) {
+	t.Setenv("WAYBACK_OMNIVORE_APIKEY", "foo")
+	opts, _ := config.NewParser().ParseEnvironmentVariables()
+
+	httpClient, mux, server := helper.MockServer()
+	defer server.Close()
+
+	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+		switch r.URL.Path {
+		case "/api/graphql":
+			fmt.Fprintln(w, saveURLResp)
+		default:
+			fmt.Fprintln(w, `{}`)
+		}
+	})
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
+	defer cancel()
+
+	o := New(httpClient, opts)
+	got := o.Publish(ctx, reduxer.BundleExample(), publish.Collects)
+	if got != nil {
+		t.Errorf("unexpected save url got %v", got)
+	}
+}
+
+func TestShutdown(t *testing.T) {
+	opts, _ := config.NewParser().ParseEnvironmentVariables()
+
+	httpClient, _, server := helper.MockServer()
+	defer server.Close()
+
+	no := New(httpClient, opts)
+	err := no.Shutdown()
+	if err != nil {
+		t.Errorf("Unexpected shutdown: %v", err)
+	}
+}
diff --git a/publish/omnivore/setup.go b/publish/omnivore/setup.go
new file mode 100644
index 00000000..86302329
--- /dev/null
+++ b/publish/omnivore/setup.go
@@ -0,0 +1,27 @@
+// Copyright 2024 Wayback Archiver. All rights reserved.
+// Use of this source code is governed by the GNU GPL v3
+// license that can be found in the LICENSE file.
+
+package omnivore // import "github.com/wabarc/wayback/publish/omnivore"
+
+import (
+	"github.com/wabarc/wayback/config"
+	"github.com/wabarc/wayback/publish"
+)
+
+func init() {
+	publish.Register(publish.FlagOmnivore, setup)
+}
+
+func setup(opts *config.Options) *publish.Module {
+	if opts.EnabledOmnivore() {
+		publisher := New(nil, opts)
+
+		return &publish.Module{
+			Publisher: publisher,
+			Opts:      opts,
+		}
+	}
+
+	return nil
+}
diff --git a/publish/publish.go b/publish/publish.go
index 69c6a4e6..bd0e83e1 100644
--- a/publish/publish.go
+++ b/publish/publish.go
@@ -32,6 +32,7 @@ const (
 	FlagNotion               // FlagNotion is a flag for notion publish service
 	FlagGitHub               // FlagGitHub is a flag for github publish service
 	FlagMeili                // FlagMeili is a flag for meilisearch publish service
+	FlagOmnivore             // FlagOmnivore is a flag for Omnivore publish service
 )
 
 // Publisher is the interface that wraps the basic Publish method.
@@ -72,6 +73,8 @@ func (f Flag) String() string {
 		return "github"
 	case FlagMeili:
 		return "meilisearch"
+	case FlagOmnivore:
+		return "omnivore"
 	default:
 		return "unknown"
 	}
diff --git a/reduxer/reduxer.go b/reduxer/reduxer.go
index f768d774..771a7571 100644
--- a/reduxer/reduxer.go
+++ b/reduxer/reduxer.go
@@ -177,7 +177,7 @@ func Do(ctx context.Context, opts *config.Options, urls ...*url.URL) (Reduxer, e
 		g.Go(func() error {
 			basename := strings.TrimSuffix(helper.FileName(uri.String(), ""), ".html")
 			basename = strings.TrimSuffix(basename, ".htm")
-			ctx = context.WithValue(ctx, ctxBasenameKey, basename)
+			ctx = context.WithValue(ctx, ctxBasenameKey, basename) // nolint:staticcheck
 
 			shot, er := capture(ctx, opts, uri, dir)
 			if er != nil {
diff --git a/wayback.1 b/wayback.1
index d90f95f5..6c57f0e4 100644
--- a/wayback.1
+++ b/wayback.1
@@ -189,6 +189,9 @@ Meilisearch indexing name.\&.
 .B WAYBACK_MEILI_APIKEY
 Meilisearch admin API key.\&.
 .TP
+.B WAYBACK_OMNIVORE_APIKEY
+Omnivore API key.\&.
+.TP
 .B WAYBACK_BOLT_PATH
 File path of bolt database. default ./wayback.db\&.
 .TP
diff --git a/wayback.conf b/wayback.conf
index cea43bb5..3dc5f333 100644
--- a/wayback.conf
+++ b/wayback.conf
@@ -55,6 +55,7 @@ WAYBACK_XMPP_HELPTEXT=Hi,\n\nI'm a 🤖 to help you backup webpages more easily.
 WAYBACK_MEILI_ENDPOINT=
 WAYBACK_MEILI_INDEXING=capsules
 WAYBACK_MEILI_APIKEY=
+WAYBACK_OMNIVORE_APIKEY=
 WAYBACK_USE_TOR=false
 WAYBACK_ONION_PRIVKEY=
 WAYBACK_ONION_LOCAL_PORT=8964