Skip to main content

pushpush

check PyPI Python License

English | 한국어

Send messages to Telegram, Slack, or Discord from Python. Send text and a single file, and save the destinations you use often under a name (a route) so you can call them by name.

from pushpush import send

send("chip supply crash -- take a look", to="alerts")
send(media="chart.png", caption="today", to="alerts")

Using Claude Code? You can send just by asking, no Python needed → Use it from an AI coding agent

Works on Windows, macOS, and Linux. Its one runtime dependency is credbox (the credential store), itself zero-dependency; messages go over the standard library's urllib.

At a glance

One send() resolves the route, finds the secret, lets the provider frame the request, refuses anything the service could not carry before the network is touched, and returns the result as a SendReceipt.

flowchart TB
    caller["send(text, to='alerts')"] --> cfg["load_config()<br/>config.toml"]
    cfg --> route["Route<br/>provider + destination"]
    route --> secret["resolve_secret()<br/>env / credentials.json"]
    secret --> validate{"Provider.validate()<br/>can the service carry it?"}
    validate -->|"no"| refuse["raise<br/>(at the call site)"]
    validate -->|"yes"| frame["send_text / send_media<br/>frame the request"]
    frame --> http["http.post_json / post_multipart<br/>urllib"]
    http --> service(("Telegram<br/>Slack<br/>Discord"))
    service --> receipt["SendReceipt<br/>route · message_id · response"]

What you can send, and where

Service Text File Credential
Telegram Yes Yes (photo/document) bot token + chat_id
Discord Yes Yes (webhook attachment) webhook URL
Slack Yes Bot token only webhook URL or bot token (xoxb)

Slack files go only through a bot token (with the files:write scope), and the route's destination must be a channel id (C…). An incoming webhook has no file upload at all -- a media push on a webhook route is refused clearly with MediaUnsupportedError, so send it via Telegram or Discord, or put a link in the text.

Limits, checked before the send

Each service publishes fixed limits; pushpush checks them at the call site and refuses an over-limit push -- InvalidPushError for text or a caption, MediaTooLargeError for a file -- rather than letting it fail mid-flight.

Service Text Media caption File
Telegram 4096 chars 1024 chars 50 MB (10 MB for a photo)
Discord 2000 chars 2000 chars 8 MB
Slack no local cap no local cap workspace limit

A caption is the words that ride with a file. Because a media caption often has a lower cap than a plain-text message (Telegram: 1024 vs 4096), attaching media can lower the effective text limit -- the same words that fit as text may be refused as a caption. Slack accepts long text and splits or blocks it itself, so pushpush does not pre-check its length.

Requirements

  • Python 3.11 or newer. Check with python --version in a terminal. (On Windows it may be py --version.)
  • A credential for the service you send to -- a bot token or a webhook URL. See 4. Getting credentials.

1. Install

pushpush's one runtime dependency is credbox, itself zero-dependency; nothing else comes along.

pip install pushpush

Check it worked:

pushpush --version
From source (for development)
git clone https://github.com/seokhoonj/pushpush.git
cd pushpush
python -m venv .venv
source .venv/bin/activate    # Windows: .venv\Scripts\activate
pip install -e ".[dev]"

2. Configure a route

A route saves a "which service, and where" pair under a name. To send, you call the name (to="alerts"). Create it at .config/pushpush/config.toml under your home directory.

Path (default)
macOS · Linux ~/.config/pushpush/config.toml
Windows C:\Users\<username>\.config\pushpush\config.toml

These are the defaults: set an absolute XDG_CONFIG_HOME to move the directory, or PUSHPUSH_CONFIG to point at one config file elsewhere. Create the folder if it does not exist. The contents:

default_route = "alerts"

[routes.alerts]
provider    = "telegram"
destination = "123456789"    # the chat_id the bot messages (the token is a secret, kept elsewhere)

[routes.team]
provider = "slack"           # a webhook needs no destination (the URL carries the channel)

[routes.trades]
provider = "discord"         # the webhook URL points to the channel too

Where destination is needed and where it is not:

  • Telegram -- always needed. The chat_id the bot sends to.
  • Discord -- not needed. The webhook URL already points to the channel.
  • Slack -- a bot token (xoxb) needs a channel (destination = "#alerts"). A webhook URL does not.

If there is only one route, default_route can be omitted.

3. Store the credential

A token or webhook URL is a secret, so it lives not in the config file but in a separate 0600-permission file. Don't write it in plain text in a conversation or a script -- enter it in your own terminal with getpass:

python -c "
from getpass import getpass
from pushpush import load_config, store_secret
route = load_config().resolve_route('alerts')
store_secret(route, getpass('secret for alerts: '))
print('stored')
"

It is saved to ~/.config/pushpush/credentials.json (mode 0600). Once entered, you are not asked again. For a container or a one-off run, you can supply it via an environment variable instead of a file:

export PUSHPUSH_SECRET_ALERTS="bot-token-or-webhook-URL"

With several routes, use PUSHPUSH_SECRET_<ROUTE> -- a bare PUSHPUSH_SECRET cannot say which route it is for, so one service's token could go to another's.

4. Getting credentials

Telegram

  1. In Telegram, send /newbot to @BotFather to create a bot. At the end it gives you a bot token (123456:ABC-...) -- that is the secret.
  2. Start a chat with your bot (a bot cannot message you first -- that is Telegram's rule).
  3. Find the chat_id: send your bot any message, then open https://api.telegram.org/bot<token>/getUpdates and read chat.id. That number is the destination.

Discord

Channel settings → Integrations → Webhooks → New Webhook → Copy Webhook URL. That whole URL is the secret, and no destination is needed.

Slack

  • The simple way -- webhook: create a per-channel webhook URL at Incoming Webhooks. The whole URL is the secret; no destination.
  • Bot token: create an app, grant it chat:write (and files:write to send files), and get a bot token (xoxb-...). Then put the channel in destination -- a name like #alerts for text, or the channel id C… when you send files.

5. Sending

from pushpush import send

# text
receipt = send("market close -- KOSPI +1.2%", to="alerts")
print(receipt.message_id)

# a file (a photo goes inline, anything else as a document)
send(media="report.pdf", caption="daily report", to="alerts")

# formatting (Telegram: plain/markdown/html; Slack, Discord: plain/markdown)
send("<b>bold</b>", to="alerts", markup="html")

# without a notification sound
send("nightly batch done", to="alerts", silent=True)

Omit to and it goes to default_route. A send needs at least one of text or media.

What comes back

send returns a SendReceipt -- where it went, plus the service's whole raw reply:

receipt = send("hi", to="alerts")
receipt.route        # "alerts"
receipt.provider     # "telegram"
receipt.message_id   # the id the service gave the message (when it returns one)
receipt.response     # the full service reply (read-only)

6. From the shell

Installing pushpush also gives you a pushpush command -- a thin wrapper over send() for scripts and cron.

pushpush send "deploy finished" --to alerts
pushpush send --media chart.png --to alerts       # the piped/positional text captions it
echo "batch done" | pushpush send --to alerts     # text from stdin
pushpush routes                                   # list the configured routes

The options: -t/--to names the route (default: default_route), -m/--media attaches a file, -c/--caption labels it (use either the message text or --caption for a media label, not both), --markup plain|markdown|html picks the rendering, and -s/--silent delivers without a sound.

It reads the same config and secrets as the Python API. Unlike the Python call it does not confirm before sending -- it is for automation. For an interactive, confirm-before-send flow, use the Claude Code skill below.

7. Failures are caught before sending

A message the service could not carry is stopped at the call site, before the network -- rather than a bad send arriving later as a silent non-delivery, it raises right there.

Exception When
InvalidPushError nothing to send (no text, no media), a caption without media, a media send carrying both text and caption (only one can label the file), a destination needed but absent, or the text or caption over the service's length limit
MediaError / MediaTooLargeError the file is missing or not a file / over the service's limit
MediaUnsupportedError the route cannot carry a file (a Slack incoming webhook -- use a bot token)
MarkupUnsupportedError the service does not render that markup (html is Telegram only)
MissingSecretError the route has no secret
SendFailedError the service was reached and refused -- a revoked token, a wrong chat_id, etc. Carries the service's own reason
ConfigError the config file is missing or malformed (UnknownRouteError and UnknownProviderError, for a route or provider not in the config, are kinds of it)
CredentialsError the stored secret could not be read (a malformed or unreadable store)
urllib.error.URLError the network itself failed -- DNS, a refused connection, a timeout

To catch everything:

import urllib.error
from pushpush import send, PushpushError

try:
    send("hi", to="alerts")
except (PushpushError, urllib.error.URLError) as err:
    print("could not send:", err)

8. Use it from an AI coding agent

This repo ships a send skill: describe what to send in plain words ("send this to Telegram") and it confirms the route, shows you the content, and sends only after you approve.

8.1 Claude Code

In the Claude Code chat, add the marketplace and install:

/plugin marketplace add seokhoonj/pushpush
/plugin install pushpush@pushpush

Then invoke it with /pushpush:send, or just ask in plain language. The skill calls the pushpush command, so install the package too (step 1). See plugins/pushpush/skills/send/SKILL.md.

8.2 Codex

In your terminal, add the marketplace and install:

codex plugin marketplace add seokhoonj/pushpush
codex plugin add pushpush@pushpush

The send skill responds automatically to matching requests.

Prefer no plugin? Symlink the skill into your skills directory and call it as /send:

ln -s "$PWD/plugins/pushpush/skills/send" ~/.claude/skills/send   # Claude Code -> /send
ln -s "$PWD/plugins/pushpush/skills/send" ~/.codex/skills/send    # Codex -> $pushpush:send

Claude Code picks it up immediately; Codex needs a restart to load it.

Metadata

Release files for pushpush 0.3.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pushpush 0.3.2
File Size Uploaded
pushpush-0.3.2.tar.gz 52.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pushpush 0.3.2
File Interpreter ABI Platform
pushpush-0.3.2-py3-none-any.whl Python 3 none any Details

Total release size: 87.2 kB

Release files / pushpush-0.3.2.tar.gz

Download URL pushpush-0.3.2.tar.gz
Size 52.7 kB
Tags Source
SHA-256 checksum
How to use checksums
9a1c0a1f21b2e6cb9d094637c19a1a4c438b4f768dd0844664198940a5816e8b
BLAKE2b-256 checksum
How to use checksums
c0541a0f319f3a83cc5e82e74f124462e738e84099538105fc4ca57ef0f234dd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.

Transparency log

Release files / pushpush-0.3.2-py3-none-any.whl

Download URL pushpush-0.3.2-py3-none-any.whl
Size 34.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
91658daef812d44e97a2eb7194be5cd8fdb92b823abdca38ee5b2bb89c2e53bf
BLAKE2b-256 checksum
How to use checksums
208208807fe38bd4eb5ff971f589bde628eece7aa050b39e090089ad240b0c99
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.2 This release

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page