FormSmarts API & Webhook Client
A Python client for the FormSmarts API and webhook system. Automate form workflows: search and edit submissions, tag and annotate them, download attachments and PDFs, submit forms programmatically, pre-populate forms, and verify incoming webhook callbacks.
Requirements
Installation
pip install formsmarts
Authentication
All API operations require an APIAuthenticator object, initialised with your FormSmarts Account ID and API Key. You can find both in the Security Settings of your account.
from formsmarts import APIAuthenticator
auth = APIAuthenticator('FSA-999999', 'your-api-key')
Security: Never hard-code credentials. Load them from environment variables or a secrets manager.
import os
auth = APIAuthenticator(os.environ['FS_ACCOUNT_ID'], os.environ['FS_API_KEY'])
Searching & Retrieving Form Entries
Search by date range
Returns a generator of FormEntry objects for all submissions to a form between two dates.
from formsmarts import APIAuthenticator, FormEntry
auth = APIAuthenticator('FSA-999999', 'your-api-key')
for entry in FormEntry.search_by_dates(auth, form_id='lqh', start_date='2025-01-01'):
print(entry.reference_number, entry.submitted_at)
Search by email, phone, or ID
for entry in FormEntry.search(auth, query='jane@example.com'):
print(entry.reference_number)
Filter results by tag, and exclude entries with specific tags:
for entry in FormEntry.search(
auth,
query='jane@example.com',
tags='vip',
exclude_tags=['archived', 'duplicate'],
):
print(entry.reference_number)
Pass 'latest' as the query to retrieve the most recent submission:
entry = next(FormEntry.search(auth, query='latest', form_id='lqh'))
Fetch a single entry by Reference Number
entry = FormEntry.fetch(auth, ref_num='3KTM7N9X2QP4R8WVHJ5F6DYC1')
Fetch a batch of entries
ref_nums = ['3KTM7N9X2QP4R8WVHJ5F6DYC1', '7BF2HJ4KN9M3PT6X8DQWYRC5L', '5LR8QV1WN7K4BM2XHTF9GJ3CP']
for entry in FormEntry.fetch_batch(auth, form_id='lqh', reference_numbers=ref_nums):
print(entry.reference_number)
Working with Fields
Iterate over all fields
for field in entry.fields:
print(field.name, field.type, field.value)
Look up fields by name, type, or ID
# By name (returns a list, as names may not be unique)
email = entry.fields_by_name('Email')[0].value
# By datatype
uploads = entry.fields_by_type('upload')
# By field ID
field = entry.field_by_id(12345)
Field types and their values
| Type | Field subclass |
.value returns |
|---|---|---|
text, email, phone, … |
Field |
str |
boolean (checkbox) |
Boolean |
bool |
number |
Field |
Decimal |
posint |
Field |
int |
date |
Date |
datetime.date |
option (drop-down, radio) |
Option |
str |
upload |
Upload |
the Upload object itself (None if empty): call read() or download() to get the file |
signature |
Signature |
the text of the signature (str) |
country |
Country |
ISO country code (str) |
csd (country subdivision) |
CSD |
ISO subdivision code (str) |
Country and subdivision names
country_field = entry.fields_by_type('country')[0]
print(country_field.country_code) # 'US'
print(country_field.country_name) # 'United States'
csd_field = entry.fields_by_type('csd')[0]
print(csd_field.subdivision_code) # 'NY'
print(csd_field.subdivision_name) # 'New York'
Edit a field value
Assigning to field.value immediately persists the change via the API.
entry.field_by_id(12345).value = 'Updated text'
This is different from
field.set_value(), which only updates the local object (used when building a submission).
Upload fields are replaced with replace() (see Replace an attachment); assigning to the value of an Upload or Signature raises NotImplementedError. To see who changed a field and when, use field.revisions() (see Edited Fields).
Submitting Forms
Simple submission with a field value dict
Keys are field IDs; values are converted to strings automatically.
ref_num = FormEntry.submit(auth, form_id='lqh', field_values={
122619: 'Jane Doe',
122620: 'jane@example.com',
122621: 'Hello, thanks for your help.',
})
print(f'Submitted: {ref_num}')
Submission using Field objects
Use the Form class to look up fields by name, then submit:
from formsmarts import APIAuthenticator, FormEntry, Form
auth = APIAuthenticator('FSA-999999', 'your-api-key')
form = Form(auth, 'lqh')
fields = []
for name, value in {'First Name': 'Jane', 'Last Name': 'Doe'}.items():
field = form.fields_by_name(name)[0]
field.set_value(value)
fields.append(field)
ref_num = FormEntry.submit(auth, 'lqh', fields)
Submission with a file attachment
from formsmarts import Upload
FORM_ID = 'lqh'
UPLOAD_FIELD_ID = 122621
# Step 1: upload the file (accepts a path string or a file-like object)
upload = Upload.upload(
auth,
form_id=FORM_ID,
field_id=UPLOAD_FIELD_ID,
io_stream='/path/to/report.pdf',
filename='report.pdf',
)
# Step 2: submit the form, passing the Upload object as the field value
ref_num = FormEntry.submit(auth, FORM_ID, {
122619: 'Jane Doe',
122620: 'jane@example.com',
UPLOAD_FIELD_ID: upload,
})
Attachments & PDFs
Download an attachment
upload_field = entry.fields_by_type('upload')[0]
# To a file path:
upload_field.download(f'/downloads/{upload_field.filename}')
# Or to a file-like object:
upload_field.download(open('copy.pdf', 'wb'))
# Or as bytes:
content = upload_field.read()
Replace an attachment
upload_field.replace('/path/to/new-file.pdf', filename='new-file.pdf')
Download an entry as a PDF
FormEntry.download_pdf(auth, ref_num='3KTM7N9X2QP4R8WVHJ5F6DYC1', io_stream='/downloads/entry.pdf')
# With timezone localisation:
FormEntry.download_pdf(auth, ref_num='3KTM7N9X2QP4R8WVHJ5F6DYC1', io_stream='/downloads/entry.pdf', timezone='Europe/London')
Tags
# Read tags
print(entry.tags) # user-defined tags
print(entry.system_tags) # tags assigned by FormSmarts
# Add and remove tags
entry.add_tag('reviewed')
entry.add_tags(['approved', 'priority'])
entry.remove_tag('reviewed')
Edited Fields
Field values can be edited after a form is submitted. edited_fields lists the fields whose value differs from the original submission:
entry = FormEntry.fetch(auth, ref_num, return_edited_status=True)
for field in entry.fields:
marker = ' (edited)' if field.is_edited else ''
print(f'{field.name}: {field.value}{marker}')
print([f.name for f in entry.edited_fields])
return_edited_status=True (also accepted by fetch_batch(), search() and search_by_dates()) returns the edited status with the entry; without it, the first access makes an extra API request.
Field.revisions() returns the field's revision history: the value submitted (None if the field was never edited), and the list of revisions, oldest first:
for field in entry.edited_fields:
original, revisions = field.revisions(timezone='America/New_York')
print(f'{field.name}: submitted as {original}')
for rev in revisions:
# rev.value is None for upload fields: use rev.upload_id, rev.filename and rev.file_hash
print(rev.created_at, rev.author, rev.is_from_api, rev.value)
Revisions made with the API have API as the author. rev.previous_value is the value before the revision, and rev.diffs the changes as a list of (operation, text) tuples (Revision.DIFF_DELETE, DIFF_EQUAL or DIFF_INSERT).
Notes
Notes added with the API are shown with the entry in FormSmarts, with API as the author.
from formsmarts import Note
# Read notes, newest first
for note in entry.notes:
print(note.created_at, note.author, note.text)
# Add a note (up to 400 characters)
note = entry.add_note('Exported to CRM, contact 0035f00000XyZ')
# Edit or delete a note added with the API
note.update('Exported to CRM, contact 0035f00000XyZ (updated)')
note.delete()
# Fetch a note by ID
note = Note.fetch(auth, note_id=1234)
- Only notes added with the API can be edited or deleted with the API (
note.can_modify,note.can_delete). Trying to change other notes raisesAPIRequestErrorwith status 403. note.is_from_apitells notes added with the API from those written by you or your users;note.author_groupis'admin'or'user'.note.created_atandnote.modified_atare timezone-awaredatetimeobjects;modified_atisNonefor a note that was never edited.entry.notesis fetched once and cached; it returns up to the 350 most recent notes.
Sharing an Entry
Send a copy of a form submission by email:
entry.share('manager@example.com')
# Multiple recipients, with an optional note:
entry.share(
['alice@example.com', 'bob@example.com'],
note='Please review this submission.',
sign_note=True,
)
Replying to the Submitter
Reply to the person who submitted the form with a note, shown at the top of an email that includes the form entry:
entry.reply('Your appointment is confirmed for Monday at 10am.')
# All the email addresses entered on the form (up to 3), signing the note with your name:
entry.reply_all('We received your application and will be in touch shortly.', sign_note=True)
- Recipients are the email addresses submitted in the form's email fields:
reply()uses the first one,reply_all()up to 3. reply()returns the address the entry was sent to andreply_all()the list of addresses. If the email fields were left empty, nothing is sent and they returnNoneand[].- The note is required: an empty note raises
ValueError, as does replying to an entry of a form that has no email field.
Pre-filling Forms
Generate a signed URL that opens the form with fields pre-populated. Optionally lock fields to prevent the user from changing them.
from formsmarts import APIAuthenticator, PreFilledForm
import os
auth = APIAuthenticator(os.environ['FS_ACCOUNT_ID'], os.environ['FS_API_KEY'])
pf = PreFilledForm(auth, form_id='lqh', prefill_key=os.environ['FS_PREFILL_KEY'])
data = {
'Client ID': {'value': 789034, 'readonly': True},
'First Name': {'value': 'Jane', 'readonly': True},
'Last Name': {'value': 'Doe', 'readonly': True},
'Email': {'value': 'jane@example.com'},
}
for name, opts in data.items():
fields = pf.fields_by_name(name)
if fields:
pf.pre_fill(fields[0], opts['value'], readonly=opts.get('readonly', False))
url = pf.get_url()
print(url)
Webhooks
Verifying a webhook callback
Always verify that incoming requests originate from FormSmarts before processing them.
from formsmarts import WebhookAuthenticator, FormEntry, AuthenticationError
import json, os
wh_auth = WebhookAuthenticator(os.environ['FS_WEBHOOK_KEY'])
def handle_webhook(headers, body):
try:
wh_auth.verify_request(headers['Authorization'])
except AuthenticationError:
return 403
entry = FormEntry.create(json.loads(body))
for field in entry.fields:
print(field.name, field.value)
return 200
Accessing webhook-only properties
print(entry.submitted_at) # timezone-aware datetime the form was submitted (UTC)
print(entry.amount_due) # for forms with deferred payment
Downloading attachments from a webhook
Webhook entries include presigned URLs (valid for 5 minutes) for direct attachment access, in addition to the standard download() method which requires API authentication.
upload_field = entry.fields_by_type('upload')[0]
print(upload_field.presigned_url) # direct download URL, no auth needed
upload_field.download('/tmp/attachment.pdf') # uses API auth
Concurrency
The client is synchronous. For workloads that require parallel API calls — such as fetching entries for a dashboard or processing a large batch — use concurrent.futures.ThreadPoolExecutor:
from concurrent.futures import ThreadPoolExecutor, as_completed
from formsmarts import FormEntry
entries = list(FormEntry.search_by_dates(auth, form_id='lqh', start_date='2025-01-01'))
with ThreadPoolExecutor(max_workers=5) as executor:
futures = {
executor.submit(
FormEntry.download_pdf, auth, entry.reference_number,
f'/downloads/{entry.reference_number}.pdf'
): entry
for entry in entries
}
for future in as_completed(futures):
future.result() # re-raises any exception from the worker
The client handles HTTP 429 (rate limit) responses automatically with exponential backoff. The default is 3 retries; pass max_retries to the underlying request if you need to adjust this.
Error Handling
from formsmarts import APIRequestError, AuthenticationError
try:
entry = FormEntry.fetch(auth, ref_num='INVALID')
except AuthenticationError as e:
print('Auth failed:', e)
except APIRequestError as e:
print(f'API error {e.status}: {e.text}')
| Exception | When raised |
|---|---|
AuthenticationError |
Invalid credentials or expired/malformed JWT |
APIRequestError |
Non-200 HTTP response from the API; exposes .status, .text, .json |
APITimeoutError |
The request timed out; a subclass of APIConnectionError |
APIConnectionError |
The request failed without an HTTP response (DNS failure, connection refused, …) |
APIError |
Base class for all client errors |
Links
- FormSmarts API & Webhook documentation
- Changelog
- Form Submission API
- Form Response API
- Webhooks
- Pre-populate a form
- Account settings
License
Copyright © Syronex LLC. All rights reserved.
Metadata
Release files for formsmarts 2.1.5
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 | |
|---|---|---|---|---|
| formsmarts-2.1.5.tar.gz | 32.1 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| formsmarts-2.1.5-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 59.1 kB
Release files / formsmarts-2.1.5.tar.gz
| Download URL | formsmarts-2.1.5.tar.gz |
|---|---|
| Size | 32.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cce2bad8099c63b433b1b99f26a78765b35bf59279aaf5510957cbf07a13e6b7
|
|
BLAKE2b-256 checksum How to use checksums |
e1bf2ea9804ed026de101df6b1b2abb1be0fb0c135d6f64c9424d631207aab71
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.8
|
Release files / formsmarts-2.1.5-py3-none-any.whl
| Download URL | formsmarts-2.1.5-py3-none-any.whl |
|---|---|
| Size | 27.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3e1de3d242d93f850a10b5b7d7fa444e623985367e908de55a6244b2f11daea0
|
|
BLAKE2b-256 checksum How to use checksums |
55b21732d677c2f124643ccd9d054763ccc1e65839bd30c0f54f751f7da288a8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.8
|
Security for formsmarts 2.1.5
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.