Generates Indonesian power-of-attorney letters (surat kuasa) as PDF. The
layout follows contoh-sk-umum-perorangan.pdf from suratkuasa.com: title,
principal and agent identities, the K H U S U S rule, the granted powers,
closing, and a signature block with a stamp-duty (materai) placeholder.
The letter itself is Indonesian; the CLI, config keys and docs are English.
pip install -r requirements.txt # web UI + CLI
pip install -r requirements-cli.txt # CLI only (just reportlab)
pip install pyhanko # optional: signature fields + signingPython 3.11+ (uses the stdlib tomllib).
streamlit run app.pyThe page scrolls as one and the right column sticks, so the live preview and the actions stay in view while you fill the form. The preview re-renders as you type (cached, so an unchanged form costs nothing); "Lihat layar penuh" opens the page at full size.
- It remembers your whole setup. Tick "Ingat data ini di perangkat ini" and
both parties, the place of signing, the materai side, the e-sign switches
(anchors,
.fields.json, AcroForm fields) and the page setup all come back next visit - compressed into a cookie in your own browser, never a database. A letter number and a fixed date are deliberately left out; they belong to one document, not to your setup. "Lupakan data di perangkat ini" in the sidebar clears everything. You can also download aconfig.toml, keep it, and load it again later (that same file works with the CLI). - Shareable setup link, with a QR code. "Bagikan setelan ini" carries the whole setup - letter type, place, materai side, clause text, letterhead, footer, e-sign switches, page setup - compressed into a single parameter, so a colleague opens the app already configured and fills in their own details. It is shown as a QR to save or photograph; the link itself sits behind a toggle. A QR holds about 2,900 bytes by standard, and compression keeps a large setup well under that. Names, NIK and addresses never travel in it.
- Saved profiles. Keep several parties on the device ("Diri sendiri",
"Ibu", "Kantor") and load one into either side of the letter.
Names, NIK and addresses are never in the link: a URL survives in chat logs
and browser history. To share a finished letter, send the PDF; to hand your
full details to someone you trust, send the
config.tomlfile instead. - Write your own letter. Pick the
customtype, write the purpose, the numbered powers and the limits, then "Simpan teks ini di perangkat" keeps that wording on your device under a name you choose. It reappears the next time you pickcustom. - Less typing. "Alamat sama dengan pemberi kuasa" copies the address across, the place of signing defaults to your city, and a NIK that isn't 16 digits is flagged as you type.
- Blank forms. "Kosongkan" turns either party into dotted lines to fill in by hand.
- Signing help. A panel at the bottom explains what to do next in Privy, DocuSign and Adobe Sign, and how the materai space works.
"Buat surat" produces one PDF, or a ZIP when several types are selected. Letters are rendered in a temporary directory and deleted right after the download is handed over; nothing is written to the server's disk and nothing is logged.
- Streamlit Community Cloud - point it at the GitHub repo, main file
app.py. It readsrequirements.txtby itself. Free tier sleeps when idle and wakes on the next visit. - Hugging Face Space - create a Space with SDK
streamlit, push these files, done. Samerequirements.txt.
Both run the app on someone else's server, so the NIK and addresses typed into
the form travel over the network (as they already do with any Streamlit app -
widget values go to the server process). Nothing is stored there: no database,
no files, no logs of the form. For sensitive data, run streamlit run app.py
locally or use the CLI, and then nothing leaves the machine at all.
Fill in your details once in config.toml, then pick a letter and generate:
python generate.py # no arguments -> interactive menuThe menu asks, in order: which config holds your details (config.toml plus
anything in examples/), which letter you need (numbered 1-15; comma-separated
for several, or type all), whether the agent's / your own details should be
left blank, whether to edit the details (field by field, Enter keeps the
current value), place, date, and the output folder. Press Enter to accept the
value in brackets.
After editing it offers to save everything back to a config file - the same one or a new name. Saving rewrites the file from the values, so a hand-written config trades its comments for the new data; it asks before overwriting.
python generate.py -t bank
python generate.py -t skck --date 2026-06-01 --place Bandung
python generate.py -t skck,bank,hr -d output/august
python generate.py --all --blank-agent
python generate.py --list-types| Flag | Purpose |
|---|---|
-c, --config |
config file .toml / .json / .yaml (default config.toml) |
-t, --type |
letter type; comma-separated for several, or all |
--all |
generate every letter type |
-o, --output |
output path or pattern, e.g. out/{type}.pdf |
-d, --dir |
output folder (keeps the config's file pattern) |
--blank-agent |
agent details become dotted lines (fill-in form) |
--blank-principal |
principal details become dotted lines |
--date |
override [document].date; '' leaves a dotted line |
--place |
override [document].place |
--esign |
anchors + .fields.json + empty signature fields |
--sign P12 |
sign the PDF (PAdES) with a PKCS#12 certificate |
--sign-as |
which field to sign: principal (default) or agent |
--sign-pass |
PKCS#12 passphrase (prefer the env var, see below) |
-i, --interactive |
force the interactive menu |
--list-types |
list the letter types |
Output is always PDF; another extension is replaced with .pdf.
python generate.py -t skck,bank,hr -o "output/{type}.pdf"
python generate.py --all -d output/blank-forms{type} in -o is replaced by the type code; {date}, {principal} and
{agent} work too. Without a {type} placeholder the type code is appended to
the file name (sk.pdf -> sk-skck.pdf, sk-bank.pdf, …) so nothing gets
overwritten. Custom purpose / powers / limits in the config are skipped
in batch runs - each letter uses its own template text - and the reason is
printed to stderr.
python generate.py --blank-agent
python generate.py --all --blank-agent -d output/blank-formsThe blanked party's data becomes dotted lines, including the name above the
signature (( .................... )). Same as blank = true under
[principal] / [agent].
Standard blank rows: Nama, Alamat, Kelurahan/Kecamatan, Kota/Provinsi, NIK.
Add more with blank_fields:
[agent]
blank = true
blank_fields = ["birth", "occupation", "phone"]The PDF is a plain, unencrypted file with a real text layer, so it uploads to
any e-sign platform as-is. --esign prepares it so you don't have to place
fields by hand:
python generate.py -t skck --esignThat produces three things:
- Invisible anchor strings on each signature area -
/ttd_pemberi/and/ttd_penerima/by default (configurable). They are never printed (PDF text render mode 3) but are extractable, which is what DocuSignanchorStringand Adobe Sign text tags look for. <name>.fields.jsonnext to the PDF, listing every signature area with its page and rectangle in both coordinate systems -rect(origin bottom-left, for Acrobat/pyHanko) andrect_top_left(origin top-left, for DocuSign tabs, Privy and web viewers). Feed it to the API instead of guessing positions.- Empty AcroForm signature fields named
PemberiKuasaandPenerimaKuasa, so Acrobat shows a click-to-sign box. Needspip install pyhanko; without it this step is skipped with a note and the other two still work.
export PKCS12_PASSPHRASE='…'
python generate.py -t skck --esign --sign cert.p12 --sign-as principalSigns with your PKCS#12 certificate; the result validates as intact and
covering the whole file. The other party's field stays empty for a
counter-signature. Passphrase resolution order: --sign-pass, then the
PKCS12_PASSPHRASE env var, then an interactive prompt - prefer the last two,
since a passphrase on the command line is visible to other processes on the
machine.
The e-meterai nominal is fixed at Rp10.000 by the government; what differs between sellers is the service fee on top (some official distributors add about Rp2.500 per stamp). Anything cheaper than the nominal is a red flag - the tax office warns that below-nominal meterai are likely counterfeit. So the cheapest honest route is to buy from an official Peruri distributor with the lowest service fee, in a bundle if you need several, and affix it yourself. Meterai is only required for documents used as civil evidence; many counters accept a surat kuasa without one, so ask the office first.
[document.stamp] only reserves blank space (width_cm × height_cm, default
3 × 2 cm) on the chosen side; nothing is drawn there. A printed copy takes a
physical stamp, an e-signed one takes the e-meterai overlay, and the reserved
rectangle shows up in .fields.json as the area flagged holds_stamp. Set
image to paste a stamp picture in instead.
dukcapil, bpjs_tk, bpjs_kes, pajak, imigrasi, skck, kendaraan,
bank, hr, pendidikan, asuransi, instansi, utilitas,
ambil_dokumen, umum, plus custom for a letter you write
yourself - the text of each lives in templates.py.
[document]-type,number,place,date(auto, a date, free text, or""for a dotted line),substitution_right,valid_until,clause_label,footnote.purpose/powers/limitsoverride the template text when set.[document.stamp]-enabled,on(principal|agent|both),width_cm,height_cm,image(optional PNG/JPG to paste in).[esign]-anchors,anchor_principal,anchor_agent,fields_json,signature_fields.--esignswitches the three toggles on at once.[principal]and[agent]- identities. Optional fields (birth,occupation,position,phone) are skipped when empty.attach_id_copyprints "(fotokopi KTP terlampir)";show_nik_on_signatureprints the NIK under the name in the signature block;blank/blank_fieldsdrive the fill-in form.[output]- defaults only:dirandpattern.-oand-doverride them.[document.header]/[document.footer]- optional letterhead (text, logo, rule) and page footer (small print, page numbers), drawn outside the text frame on every page.[layout]-paper(A4|F4|LETTER|LEGAL), margins,font/font_bold(Times-Roman,CourierorHelvetica) or your ownfont_file/font_bold_file(.ttf, embedded),font_size,signature_space_cm.
[layout].fit_one_page (default true) first tightens the gaps between
blocks, then steps the font down to font_size_min (default 8.5) so the
signature block never ends up stranded on a second page. Each candidate is
rendered and its page count read back, so a wide face like Courier is measured
rather than guessed. Raise font_size_min
if you would rather have two pages than small type, or set fit_one_page to
false to keep the spacing and font exactly as configured.
Everything in examples/ uses fictional data and runs as-is:
| File | Shows |
|---|---|
skck-personal.toml |
the common case, plain template text |
bank-card-pickup.toml |
letter number, expiry, strict limits, NIK under the signature |
hr-employment-letter.toml |
hand-written powers, employer named |
vehicle-tax.toml |
F4 paper, plate number and vehicle named |
diploma-pickup.toml |
no stamp duty, student number named |
blank-agent-forms.toml |
your details filled, agent blank + blank_fields |
general-minimal.json |
JSON format, smallest possible config |
dukcapil.yaml |
YAML format (needs pip install pyyaml) |
python generate.py -c examples/bank-card-pickup.toml
python generate.py -c examples/blank-agent-forms.toml --allgenerate.py CLI + PDF renderer (reportlab)
app.py Streamlit web UI (same renderer)
esign.py field coordinates, AcroForm fields, PAdES signing
templates.py the 15 letter templates
config.toml your details (the default config)
examples/ ready-to-run configs per scenario
output/ generated PDFs (git-ignored)
- The signature area is left blank on purpose; the materai and the wet signature go on the printed copy, or come from the e-sign platform.
- Keep certificates out of the repo -
.gitignorealready excludes*.p12,*.pfx,*.pem,*.keyand.env. - Some offices insist on their own power-of-attorney form - check before relying on this one.