Skip to main content

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 raises APIRequestError with status 403.
  • note.is_from_api tells notes added with the API from those written by you or your users; note.author_group is 'admin' or 'user'.
  • note.created_at and note.modified_at are timezone-aware datetime objects; modified_at is None for a note that was never edited.
  • entry.notes is 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 and reply_all() the list of addresses. If the email fields were left empty, nothing is sent and they return None and [].
  • 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

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)

Source distribution for formsmarts 2.1.5
File Size Uploaded Provenance
formsmarts-2.1.5.tar.gz 32.1 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for formsmarts 2.1.5
File Interpreter ABI Platform
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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page