Polling is the worst way to watch for new filings. You burn API requests checking every 30 minutes, and you still find out late. Meanwhile, some quant's bot already scraped the PDF.
Axiora webhooks flip this around. When a new filing hits EDINET, we push it to your URL within minutes. You don't ask — we tell.
This tutorial wires that into a Discord channel. 40 lines of Python, a free Axiora key, and a Discord webhook URL. Every time a Japanese company files a securities report, your channel gets a message.
How Axiora Webhooks Work
Three events:
| Event | Fires when |
|---|---|
new_filing | A new securities report (annual, semi-annual, quarterly) is ingested from EDINET |
amendment | An amendment to a previously filed report is published |
correction | A correction to a previously filed report is published |
You register a URL, pick your events, and we send POST requests with the filing data as JSON. Each delivery includes an HMAC-SHA256 signature so you can verify it came from us.
HTTPS required. 10-second timeout. Failed deliveries retry twice with exponential backoff (1s, 2s). Three attempts total.
Step 1: Register a Webhook
curl -X POST https://api.axiora.dev/v1/webhooks \
-H "Authorization: Bearer $AXIORA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/webhooks/axiora",
"events": ["new_filing", "amendment"]
}'Response:
{
"data": {
"id": "wh_8f2a3b...",
"url": "https://your-server.com/webhooks/axiora",
"events": ["amendment", "new_filing"],
"secret": "a1b2c3d4e5f6...64 hex chars..."
}
}Save that secret. It's returned once, at creation. You need it to verify signatures.
Step 2: Build the Receiver
Here's a Flask server that receives webhooks, verifies signatures, and forwards to Discord:
import os
import hmac
import hashlib
import httpx
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["AXIORA_WEBHOOK_SECRET"]
DISCORD_WEBHOOK = os.environ["DISCORD_WEBHOOK_URL"]
def verify_signature(payload: bytes, signature: str) -> bool:
expected = hmac.new(
WEBHOOK_SECRET.encode(), payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.route("/webhooks/axiora", methods=["POST"])
def handle_webhook():
signature = request.headers.get("X-Axiora-Webhook-Signature", "")
event_type = request.headers.get("X-Axiora-Event", "")
if not verify_signature(request.data, signature):
abort(401)
filing = request.json
company = filing.get("company_name", "Unknown")
doc_type = filing.get("doc_type_label", "Filing")
doc_id = filing.get("doc_id", "")
fiscal_year = filing.get("fiscal_year", "")
# Post to Discord
message = (
f"📄 **{event_type}**: {company}\n"
f"Type: {doc_type} (FY{fiscal_year})\n"
f"Doc: `{doc_id}`\n"
f"[View on EDINET](https://disclosure2.edinet-fsa.go.jp/WZEK0040.aspx?S100{doc_id[-4:]})"
)
httpx.post(DISCORD_WEBHOOK, json={"content": message})
return "", 200That's the whole thing. Verify the signature, extract the filing data, format a message, post to Discord.
Step 3: Deploy
Any server that accepts HTTPS POST requests works. Some options:
Railway / Render — Push to GitHub, deploy in 2 minutes. Free tier available.
pip install flask httpx gunicorn
gunicorn app:app --bind 0.0.0.0:8000Cloudflare Workers / AWS Lambda — If you want serverless, adapt the handler. The verification logic is the same.
Once deployed, register the webhook with your public URL:
curl -X POST https://api.axiora.dev/v1/webhooks \
-H "Authorization: Bearer $AXIORA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.railway.app/webhooks/axiora",
"events": ["new_filing", "amendment", "correction"]
}'What the Discord Message Looks Like
When Toyota files their annual report:
📄 new_filing: Toyota Motor Corporation Type: Annual Securities Report (FY2025) Doc:
S100XXXXView on EDINET
Every filing, every company, delivered in minutes. No polling. No cron jobs.
Signature Verification: Why It Matters
Without verification, anyone who discovers your webhook URL can send fake filings. The HMAC-SHA256 signature prevents this:
- We compute
HMAC-SHA256(request_body, your_secret)before sending - The digest goes in the
X-Axiora-Webhook-Signatureheader - You recompute the same HMAC on the received body and compare
Constant-time comparison (hmac.compare_digest) prevents timing attacks. This is the same pattern Stripe, GitHub, and Slack use.
Node.js version:
const crypto = require("crypto");
function verifySignature(body, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}Beyond Discord
The webhook payload is JSON with full filing metadata. Some things you can build:
- Slack alerts — Same pattern, different URL. Slack's incoming webhooks accept the same
{"content": "..."}shape (use{"text": "..."}instead). - Email digest — Batch filings hourly with a queue, send a summary email.
- Auto-analysis pipeline — On
new_filing, fetch the company's financials via the API, run your model, and post results to your dashboard. - Compliance monitoring — Track corrections and amendments for companies in your portfolio. An
amendmentevent means something changed.
The webhook fires for all 4,125+ companies. Filter on your side based on the payload fields — sector, company code, doc type, whatever matters to your use case.
Keep Going
- EDINET for Developers — understand what's in the filings you're getting notified about
- Build a Japan Stock Screener — a UI for the data behind those filings
Free tier. No credit card. Get your API key →