📄

Branding e-policies (OSAGO) with a Telegram bot

for accident commissioners: contacts on the form, data and QR code untouched

A Telegram bot stamps an accident commissioner's contacts onto an electronic OSAGO policy in seconds, leaving data, the QR code and the signature block untouched. Four insurers' forms. Python, PyMuPDF, aiogram 3.

In productionNDA
4 insurers
forms of four companies are supported; the insurer is detected from the page text
380 → 520 KB
policy size after branding, not 6.5 MB as in the first build
0 files
kept on disk: the document lives only in memory, only counters are written

The task

Accident commissioners work on call: a client has an accident, takes out the OSAGO policy (Russia's compulsory motor third-party liability insurance) and phones. Previously the firm stuck a label with its phone number onto the printed form and added a stamp. An electronic policy cannot be labelled that way, and most clients keep it on their phone.

The goal is the same PDF with the commissioner's contacts in the right places: within a couple of seconds and without manual layout work. The volume is small, up to 20 forms a day, handled by three people. The key requirement is not to damage the document itself: the form number, the holder's and the car's data, the QR code and the insurer's signature must stay untouched.

The solution

A Telegram bot accepts a finished electronic OSAGO policy and returns the same one with branding: a yellow card with the commissioner's phone number in place of the insurer's service block, and a “CALL IN CASE OF AN ACCIDENT” stamp over the paragraph with the terms. What is implemented:

  • protected zones: the form number, holder's name, VIN, plate number, premium, QR code and electronic signature block, as well as the insurer's logo and hotline, stay where they are;
  • automatic placement: the card and the stamp are located from the page text, not from fixed coordinates;
  • recognition of forms from four insurance companies, with a dedicated placement strategy for one of them;
  • file-size control: a 380 KB policy comes out at roughly 520 KB;
  • processing without storage: the document lives in memory and goes straight back;
  • an allow-list plus owner approval by button, and processing statistics for the day, week, month, year and per person;
  • button menus, a progress bar by processing stage and a hidden /version command for the owner;
  • inspect, check and brand command-line tools for checking the layout on your own form without the bot.

How it works

Protected PDF zones: what must never be covered

The first version placed blocks at coordinates from a config file and fell apart on the second form. Now the bot analyzes the page first: text blocks, lines and words, images, the QR code. Anything that looks like policy data goes into a list of forbidden rectangles. The card is then shrunk until it stops overlapping them: the side with the smaller overlap is cut off. The card's width is capped at 45% of the page so that it does not stretch across the whole sheet.

Finding a block by its text in a PDF

The bot finds the place for the card by meaning: every form has a paragraph about the notice of the financial ombudsman, which means nothing to the client. It is found with a regular expression, and its rectangle becomes the card's place. If the paragraph is missing, the insurer's header is used instead. The stamp goes over the paragraph with clauses 4 and 5, which is also located by its start and end.

An occupancy map: where the page is free

If the target paragraph is not found, free space is chosen from an occupancy map. The page is cut into cells of 1.5 points, a table of sums is built over them, and any rectangle can be checked for emptiness in a single step. The search goes to the right first, then downward.

PDF size after the overlay

By default, erasing text under the card also redraws images. On a real form a full-page raster background sits under the text, and the file was being recompressed from 380 KB to 6.5 MB. The fix is to erase only the text and leave images untouched: nothing changes visually, since the card is opaque anyway. Fonts are trimmed to the glyphs actually used, otherwise each would add about 700 KB. The project has a test for this.

Card text by priority

The card text is assembled in priority groups. When space is tight, the insurer's phone, the website, the list of services and the spare number go first, while the main phone number and the name always stay. The font size is fitted to the block height.

Automated acceptance check of the result

The check command is an automated verification of the result: it reports if the card spilled outside its block, the stamp went beyond the clauses 4 and 5 paragraph, or a protected zone was touched. The heavy work runs in a separate thread so that it does not block receiving Telegram messages.

Results

  • A policy with the commissioner's contacts is ready within a couple of seconds after the file is sent, with no manual layout.
  • The form number, the holder's and car's data, the premium and the QR code are never touched.
  • The file size after branding stays comparable to the original, about +40%, not several times larger.
  • Clients' documents are not stored anywhere: only counters remain on disk.
  • The first release shipped with 42 tests on a synthetic form; there is no automated check on real forms.

Technologies and why

  • Python 3.13 — the service language.
  • PyMuPDF — page analysis and redrawing: text blocks, images, erasing and overlaying.
  • aiogram 3 — the Telegram bot: menu, processing progress, statistics, access.
  • segno — the QR code on the card.
  • SQLite — processing counters, with no files or their content.
  • Docker Compose — deployment: a read-only container with no open ports, the bot connects to Telegram itself.

Status

Version 1.1.1 of 20 September 2026, in production at the client. Known limitations: after branding the insurer's built-in signature can no longer be verified (authenticity is confirmed through the QR code and on the RSA website); only four insurance companies' forms are supported; two instances must not run with one bot token; the backup of the counters volume is made manually. Other details about the client are not disclosed.

Questions about this project

How do I add an accident commissioner's contacts to an electronic OSAGO policy?
An employee sends the policy PDF to the bot and within seconds receives the same file with a yellow contact card and a “CALL IN CASE OF AN ACCIDENT” stamp. The card is placed over a block of the form the client does not need, and the stamp goes over the paragraph with the terms.
Can the branding cover the form number, the QR code or the policy data?
No. The bot analyzes the page and marks the form number, the holder's name, VIN, plate number, premium, QR code and the electronic signature block as protected zones. Branding elements never enter those areas. A separate check command reports if anything was touched.
What happens to the insurer's built-in electronic signature?
Once the file is modified, the signature can no longer be verified. The policy remains valid, and authenticity is checked through the QR code and on the Russian Union of Motor Insurers (RSA) website. The bot warns about this once and pins the message in the chat.
Why does the file not grow several times larger after branding?
The bot erases only the text under the card and leaves images alone, and it keeps only the glyphs actually used in fonts. A 380 KB policy comes out at roughly 520 KB instead of 6.5 MB.
Where are clients' policies stored?
Nowhere. The file is processed in memory and sent straight back. Only counters are written to disk: when, who, how many pages and bytes, and how many milliseconds it took. No files, no file names and no policy data.
How does the bot tell whose form it is, and does it work with other insurers?
The insurer is identified from the page text, and forms from four insurance companies are supported. For others a general page analysis runs, but without guarantees: coordinates from the settings serve only as a fallback.

Need something similar?

Tell us about the task — we'll show how we solved it and estimate the scope.