Back to blog
tutorial

Get Notified When Japanese Companies File: Webhooks + Discord

Onkar Dahale

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:

EventFires when
new_filingA new securities report (annual, semi-annual, quarterly) is ingested from EDINET
amendmentAn amendment to a previously filed report is published
correctionA 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 "", 200

That'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:8000

Cloudflare 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: S100XXXX View 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:

  1. We compute HMAC-SHA256(request_body, your_secret) before sending
  2. The digest goes in the X-Axiora-Webhook-Signature header
  3. 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 amendment event 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

Free tier. No credit card. Get your API key →