crudkit-sign
E-signatures for crudkit models. Hand it a PDF and a list of signers; it emails tokenised signing links, shows the document on a public page where each signer draws or types a signature, stamps the signatures into the PDF, appends a certificate of completion and tells your model when it is done. No third-party e-signature service involved.
Signature level: simple electronic signature (eIDAS SES). Consent text, IP, user agent, timestamps and document hashes are recorded per signer.
Install
pip install crudkit-sign
INSTALLED_APPS = [..., "crudkit", "crudkit_sign", ...]
# urls.py, before the crudkit SPA catch-all
path("sign/", include("crudkit_sign.urls"))
CRUDKIT_SIGN = {
"brand_name": "Example Inc",
"logo_url": "https://crm.example.com/static/logo.png", # PNG/JPG: email clients drop SVG
"brand_color": "#111827", # the email's "Review and sign" button, white text on it
"base_url": "https://crm.example.com", # used in email links; falls back to settings.BASE_URL
"from_email": "sign@example.com", # falls back to DEFAULT_FROM_EMAIL
"expiry_days": 30,
"reminder_days": 3,
"download_days": 3, # how long the signing link serves the signed PDF after completion
}
Run migrate. Configure Django's EMAIL_BACKEND. Schedule
crudkit_sign.tasks.expire_requests (hourly) and
crudkit_sign.tasks.send_reminders (daily) with Celery beat.
pdf.js is loaded from cdnjs by default. Set pdfjs_url and pdfjs_worker_url
in CRUDKIT_SIGN to self-hosted copies if you would rather not.
Make a model signable
from crudkit_sign.signable import SignerSpec
class Contract(BaseCrudKitModel):
TYPE_ID = "CNT"
def get_signature_pdf(self) -> bytes:
return render_my_pdf(self)
def get_signers(self) -> list[SignerSpec]:
return [
SignerSpec(role="customer", name="Ada Lovelace", email="ada@example.com", order=1),
SignerSpec(role="company", name="Bob Smith", email="bob@example.com", order=2, user_id=bob.pk),
]
# Optional
def get_signature_title(self): ...
def get_signature_message(self): ...
def get_signature_expiry_days(self): ...
def get_signature_fields(self): ... # explicit FieldSpec list instead of anchors
def on_signature_sent(self, request): ...
def on_signature_viewed(self, request, signer): ...
def on_signature_signed(self, request, signer): ...
def on_signature_completed(self, request, signed_file): ...
def on_signature_declined(self, request, signer, reason): ...
def on_signature_voided(self, request, user, reason): ...
def on_signature_expired(self, request): ...
Signers with the same order sign in parallel; a higher order waits for the
lower ones. A SignerSpec(note=...) is shown only to that signer, in
their invitation email. Django signals in crudkit_sign.signals fire alongside the hooks.
Then, typically from a @crm_action:
from crudkit_sign.service import create_request
request = create_request(contract, request.user) # emails the first signers
Placing fields
Write anchor strings into the PDF where the fields go, in white 1pt text so readers never see them:
| Anchor | Field |
|---|---|
{{sig:customer}} |
signature image |
{{initials:customer}} |
initials (same image, smaller box) |
{{date:customer}} |
date of signing, filled by the server |
{{name:customer}} |
signer's name, filled by the server |
{{text:customer:po_number}} |
free text the signer types |
{{check:customer:terms}} |
checkbox the signer ticks |
The part after the first colon is the signer role and must match a
SignerSpec.role. The box is placed with its bottom-left corner at the anchor.
crudkit_sign.pdf.anchor_text(kind, role, name) builds the string for you.
Every role needs at least one signature or initials anchor.
Checkboxes whose FieldSpecs share a group are a pick-one choice (radio
buttons on the signer page); a required group needs exactly one picked. Set it
by overriding get_signature_fields(), e.g. to make a yes/no question required.
If you know the coordinates, skip anchors and return FieldSpecs from
get_signature_fields() (PDF points, origin bottom-left, 1-based page).
What you get in the crudkit SPA
Four crudkit models with the usual list, detail, search and changelog:
SignatureRequest (SRQ), Signer (SGR), SignatureField (SGF) and
SignatureEvent (SGE). Add ["SRQ", [...]] to your model's Layout.inlines
to see requests on its detail page.
Detail-page actions: Resend emails, Void, Preview unsigned PDF and Download signed PDF on a request; Copy signing link on a signer. Resend and Copy link rotate the signer's token, so earlier links stop working.
Security notes
- The token is the only credential: 256 bits, stored as a SHA-256 hash.
- Public views are throttled per client IP (
throttle_per_minute). - Links stop working at
expires_atwhile open. After completion they serve the signed PDF fordownload_daysonly; everyone also gets it by email. - Public responses send
X-Robots-Tag: noindexandReferrer-Policy: no-referrer. - X-Forwarded-For is ignored unless
trusted_proxy_hopsis set to the number of reverse proxies in front of Django; the throttle key and the audit-trail IPs are only as trustworthy as that setting. - The signed file is written once. To correct anything, void and create a new request.
signed_sha256is the hash of the stamped pages before the certificate was appended; the certificate prints it.- Voiding deletes the stored signature images.
Development
python runtests.py
The tests use sqlite and a fixture model in tests/testapp.
License
MIT.
Metadata
Release files for crudkit-sign 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| crudkit_sign-0.1.0.tar.gz | 100.0 kB | With provenance | Details for crudkit_sign-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| crudkit_sign-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for crudkit_sign-0.1.0-py3-none-any.whl |
Total release size: 141.7 kB
Release files / crudkit_sign-0.1.0.tar.gz
| Download URL | crudkit_sign-0.1.0.tar.gz |
|---|---|
| Size | 100.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
14b6398be0e4cec31731bdffd454b1fda1fa81d0051ba38d3b7a0cb9fb0bd0f5
|
|
BLAKE2b-256 checksum How to use checksums |
c39cb583f9b5bad83d6aefa4381fce955d7c4bb28d536a1793a8179b8848f8bc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Oct 27, 2026.
Transparency logRelease files / crudkit_sign-0.1.0-py3-none-any.whl
| Download URL | crudkit_sign-0.1.0-py3-none-any.whl |
|---|---|
| Size | 41.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3d3c31c12c76714d13fc428dabd1a44c83dd7cb0c398fc3c2d42f1475ff7549e
|
|
BLAKE2b-256 checksum How to use checksums |
01d150e43b834e574de78057afc584df5c5ebbe0afe119484583c07e51fbf4d0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Oct 27, 2026.
Transparency logSecurity for crudkit-sign 0.1.0
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.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for crudkit_sign-0.1.0.tar.gz
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 Oct 27, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.