FormSmarts API & Webhook Client
Project description
FormSmarts API & Webhook Client
A Python client for the FormSmarts API and webhook system. Automate form workflows: search and edit submissions, download attachments and PDFs, submit forms programmatically, pre-populate form URLs, 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 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='ABC-123456')
Fetch a batch of entries
ref_nums = ['ABC-123456', 'ABC-123457', 'ABC-123458']
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 |
upload ID (str) |
signature |
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).
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.value, # the upload ID
})
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'))
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='ABC-123456', io_stream='/downloads/entry.pdf')
# With timezone localisation:
FormEntry.download_pdf(auth, ref_num='ABC-123456', 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')
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,
)
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) # datetime the form was submitted
print(entry.amount_due) # for forms with deferred payment
print(entry.is_validation_hook) # True for validation webhooks
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 |
APIError |
Base class for all client errors |
Links
- FormSmarts API & Webhook documentation
- Form Submission API
- Form Response API
- Webhooks
- Pre-populate a form
- Account settings
License
Copyright © Syronex LLC. All rights reserved.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file formsmarts-2.1.0.tar.gz.
File metadata
- Download URL: formsmarts-2.1.0.tar.gz
- Upload date:
- Size: 24.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
94220296a45e70c455e7f1125fd91de0572aa33c8144048dc08a69188766717d
|
|
| MD5 |
02ebe75df09494c934a5c7807b0c6823
|
|
| BLAKE2b-256 |
22eefd36b54fd80991e1de996affc625bd63abaf53bb1d09387183d55ba88f66
|
File details
Details for the file formsmarts-2.1.0-py3-none-any.whl.
File metadata
- Download URL: formsmarts-2.1.0-py3-none-any.whl
- Upload date:
- Size: 20.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0e58ac9dbb1fdb29dc5eddd543dad859f49ce2f92ff398b72aac2e4000f69b78
|
|
| MD5 |
d9b4302f69693603473782565f1017dd
|
|
| BLAKE2b-256 |
e8ca093f48e00b3e17cd572a12bf0b15133f6133a1ff8c5255432bd7955bfb4d
|