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 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='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')

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,
)

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
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
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.4

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.4
File Size Uploaded Provenance
formsmarts-2.1.4.tar.gz 28.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for formsmarts 2.1.4
File Interpreter ABI Platform
formsmarts-2.1.4-py3-none-any.whl Python 3 none any Details

Total release size: 52.5 kB

Release files / formsmarts-2.1.4.tar.gz

Download URL formsmarts-2.1.4.tar.gz
Size 28.2 kB
Tags Source
SHA-256 checksum
How to use checksums
4d51f32a275749a5a15717473a531df5828401d7498e97744d0cba5eedf13691
BLAKE2b-256 checksum
How to use checksums
c6240828aa300069af732fe18aba77aa99dd15fc051c314b847d395009ed126b
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.4-py3-none-any.whl

Download URL formsmarts-2.1.4-py3-none-any.whl
Size 24.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1c6f7828c01256a906d899a99d651e9ab0e19084e1026912d3268b523d7c70d4
BLAKE2b-256 checksum
How to use checksums
7bc19bcb925982528c869fd43481b813d26957e68479ba023e800652212176c6
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.4

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