qls-verified-esign
Python SDK for the Verified (verified.eu) eSign API: create envelopes, upload PDFs and
attachments, add signers (BankID and the other eID methods), publish, get signing links,
download the signed PDF, and verify the webhooks Verified sends through AWS SNS. Sync and
async clients, typed pydantic v2 models, mypy --strict clean.
Status: early release. Verified against the live API on 2026-10-21 for the full draft flow (login, envelope, document + upload, recipients, settings, delete). Publishing, signing links, the signed-PDF download and webhook delivery follow the official docs and OpenAPI spec but are not yet confirmed live. Not an official Verified product. See docs/API_NOTES.md for where the live API differs from the docs.
Install
pip install qls-verified-esign # API client
pip install "qls-verified-esign[webhooks]" # + SNS signature verification (cryptography)
The distribution is qls-verified-esign; the import package is verified_esign.
Requires Python 3.10+. Depends on httpx and pydantic>=2.
Quick start: send a contract for signing
from verified_esign import EnvelopeSettings, RecipientCreate, SigningDocument, VerifiedClient, make_tag
with VerifiedClient() as verified: # reads VERIFIED_* environment variables
sent = verified.send_for_signing(
SigningDocument(
"Avtal AVT-2026-0042.pdf", "avtal.pdf",
attachments=[("Bilaga 1 - Tjänstebeskrivning.pdf", "bilaga1.pdf")],
),
[
RecipientCreate(given_name="Anna", family_name="Andersson", email="anna@kund.se", order=1),
RecipientCreate(given_name="Erik", family_name="Eriksson", email="erik@qls.se", order=2),
],
settings=EnvelopeSettings(greeting="Hej! Här är avtalet för signering.", automatic_reminders=2),
tags=[make_tag("Bygg Dialog AB")], # -> "bygg-dialog-ab"
)
print(sent.envelope_id)
send_for_signing creates the envelope, applies the settings, creates each document,
uploads the PDF and its attachments, adds the recipients and publishes (which sends the
invitations). Pass publish=False to build a draft that notifies nobody. If any step
fails before publishing, the half-built draft is deleted and the error re-raised.
Recipients default to Swedish BankID (bankid-se), sv_SE and the signer role. Other
methods: bankid-no, nets-eident-fi, nets-eident-dk, eideasy-*, email, sms,
touch-sign (eIDs must be enabled for your company by Verified). Set ssn= to require
that the eID matches a specific personnummer.
Tags
Document tags make contracts searchable in the Verified archive and through
envelopes.query. Verified requires lowercase, no spaces, at least 3 characters;
make_tag() builds one from any display value (diacritics folded, "Ölands Åkeri AB" ->
"olands-akeri-ab") and invalid tags are rejected before anything is created. QARP tags
every contract with the customer company:
for envelope in verified.envelopes.iter(
{"field": "documents.tags", "op": "in", "value": [make_tag("Bygg Dialog AB")]}):
print(envelope.id, envelope.status)
Attachments (bilagor)
Attachments travel with the document and are shown to the signers, but only the document PDF is signed. Anything that must be legally covered by the signature (e.g. contract terms) belongs inside the PDF; attachments suit reference material. Up to 20 per document; pdf, docx, xlsx, pptx, zip, png, gif, jpg, rtf, odt. Documents must be PDF and at most 10 MB (3.5 MB for some NETS methods). Both limits are checked before anything is sent.
When it is signed: webhook -> signed PDF
from verified_esign import EVENT_SIGNED, WebhookVerifier
verifier = WebhookVerifier(expected_namespace="/companies/<company-id>",
allowed_topic_arns={"arn:aws:sns:eu-north-1:...:WebhooksAdmin-..."})
def handle(body: bytes) -> int:
msg = verifier.verify(body) # WebhookVerificationError -> answer 403
if msg.type == "SubscriptionConfirmation":
verifier.confirm_subscription(msg) # once, when the webhook is registered
elif msg.event and msg.event.event_type == EVENT_SIGNED:
enqueue(msg.event.resource.envelope_id) # do the work out of band
return 200
# worker
envelope = verified.envelopes.get(envelope_id)
for doc in envelope.documents:
pdf = verified.documents.download_signed(envelope.id, doc.id)
- Webhooks are registered per company in the Verified web app (Company settings > Webhooks), not via the API. Each company needs its own endpoint URL.
- Verified delivers through AWS SNS with no shared secret:
WebhookVerifierchecks the RSA signature against the certificate atSigningCertURL, which must be anhttps://sns.<region>.amazonaws.com/...pemURL. It also checks the company, the topic (when pinned) and a replay window. - The first message is a
SubscriptionConfirmation. Untilconfirm_subscriptionruns the webhook stays pending, and if it never runs Verified disables it (delete and re-create). - Events carry no data, may be late and may arrive out of order: treat them as "go look"
hints and read the envelope through the API.
sealedDocument.attachedmeans every recipient has signed and the sealed PDF exists.
Authentication & configuration
Verified has no API keys. The SDK logs in as a Verified user (POST /auth, Basic auth),
receives a JWT valid 7 days, reuses it until an hour before exp, and logs in again on a
401. Use a dedicated API user and keep its password in a secret store.
| Setting | Env var | Notes |
|---|---|---|
email |
VERIFIED_EMAIL |
required |
password |
VERIFIED_PASSWORD |
required |
company_id |
VERIFIED_COMPANY_ID |
optional if the user belongs to exactly one company |
ip_lock |
VERIFIED_IP_LOCK |
1 (default) or 0 |
base_url |
VERIFIED_BASE_URL |
default https://app.verified.eu/api |
IP lock: by default a token only works from the IP that logged in. On hosts whose
egress IP can change (Kubernetes without a static NAT IP, laptops on a VPN or Cisco Secure
Access) set ip_lock=False.
API map
| Namespace | Operations |
|---|---|
send_for_signing(...) |
documents + attachments + recipients + publish in one call |
envelopes |
create, get, update, publish, abort, trash, delete, query, iter |
documents |
create, get, list, upload, list_files, file_url, download, download_signed |
recipients |
add, get, list, update, sign_link, remind |
userinfo(), namespace, renew_token() |
account helpers |
request(...) |
escape hatch for any path |
Writes are asynchronous at Verified (CQRS): settings and recipient updates answer
202 Accepted and apply shortly after, so read back with get when you need the new
state. Verified has no idempotency keys, so the SDK never retries a POST that may have
reached the server.
Signing links (recipients.sign_link) embed an access token. Treat them as secrets.
Errors
Every error derives from VerifiedError. HTTP errors raise APIStatusError subclasses
(BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError,
ConflictError, UnprocessableEntityError, RateLimitError, ServerError) with
status_code, body, method and path. Login failures raise AuthenticationError
(never echoing the response). Webhook problems raise WebhookVerificationError.
Logging
The SDK logs to the verified_esign logger (silent by default): DEBUG for method, path,
status and timing, WARNING for retries. It never logs credentials, tokens, signing links,
headers or bodies.
Development
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/mypy src/verified_esign
.venv/bin/pytest -q # unit + sanitized live fixtures
.venv/bin/pytest -m integration # live draft round trip; needs VERIFIED_* in the env
scripts/sanitize_fixtures.py turns a raw recording of live responses into
tests/fixtures/live/ and refuses to write a file if any personal value survives.
License
MIT, see LICENSE.
Metadata
Release files for qls-verified-esign 2026.10.21.3
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 |
|---|---|---|---|---|
| qls_verified_esign-2026.10.21.3.tar.gz | 48.1 kB | Without provenance | Details for qls_verified_esign-2026.10.21.3.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| qls_verified_esign-2026.10.21.3-py3-none-any.whl | Python 3 | none | any | Details for qls_verified_esign-2026.10.21.3-py3-none-any.whl |
Total release size: 85.0 kB
Release files / qls_verified_esign-2026.10.21.3.tar.gz
| Download URL | qls_verified_esign-2026.10.21.3.tar.gz |
|---|---|
| Size | 48.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6d75fc03c5cc8d9ddc34aadc922a7b2be3c5280464bdd99928938eeb3912935c
|
|
BLAKE2b-256 checksum How to use checksums |
0de7105f444b313191f5678919f2f4eea93eb17d19336b753f463dd30f66c562
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.16
|
Release files / qls_verified_esign-2026.10.21.3-py3-none-any.whl
| Download URL | qls_verified_esign-2026.10.21.3-py3-none-any.whl |
|---|---|
| Size | 36.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fe7a1b6701192b9adf02b3add8134da7dec26bad62e57aa8a02078ff2d5e9028
|
|
BLAKE2b-256 checksum How to use checksums |
c5096a5fbc3dbfd692992fbc78e797a255901cee079f4551ca78d328f4f7c5e0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.16
|
Security for qls-verified-esign 2026.10.21.3
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A 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.