The short answer

Use Python to request a private PDF upload, PUT the file bytes, register the document, and request an IRIS mailing quote. Keep submission separate so a person can review the final PDF and payload. The example below prepares a mailing and never calls the paid submission endpoint.

Prepare your inputs

You need Python 3.10 or later, a verified IRIS developer account, and a server-side key with account:read, documents:write, and mailings:read scopes. The example uses only the Python standard library. Supply IRIS_API_KEY through your environment rather than putting it in the script.

Save a finished US Letter PDF as letter.pdf. Create mailing-input.json using the letter payload in the IRIS quickstart, with your actual sender, recipient, and print options. The script replaces primaryDocumentId after registering the file. Use recipient, primaryLine, city, state, and zipCode within each address; do not substitute provider-specific field names.

Upload, register, and quote the PDF

Save this as prepare_mail.py and run python prepare_mail.py from a private working directory. It creates a review file containing mailing addresses, so keep that file out of source control. Preparing uploads and quotes uses your account and private storage, but this script does not create an order.

prepare_mail.py — preparation only
import json
import os
from pathlib import Path
from urllib.error import HTTPError
from urllib.request import Request, urlopen

BASE = "https://api.irispostalforms.com"
KEY = os.environ["IRIS_API_KEY"]

def api(path, payload):
    request = Request(
        BASE + path,
        data=json.dumps(payload).encode("utf-8"),
        headers={"Authorization": "Bearer " + KEY,
                 "Content-Type": "application/json"},
        method="POST",
    )
    try:
        with urlopen(request, timeout=60) as response:
            return json.load(response)["data"]
    except HTTPError as error:
        raise RuntimeError(f"IRIS returned HTTP {error.code}; stop and inspect the request.") from error

pdf = Path("letter.pdf").read_bytes()
if not 0 < len(pdf) <= 26 * 1024 * 1024:
    raise ValueError("The PDF must be between 1 byte and 26 MiB.")

mailing = json.loads(Path("mailing-input.json").read_text(encoding="utf-8"))
if mailing.get("product") != "letter":
    raise ValueError("This example prepares letters only.")

upload = api("/v1/uploads/presign", {
    "fileName": "letter.pdf", "sizeBytes": len(pdf), "kind": "letter"
})
put = Request(upload["uploadUrl"], data=pdf,
              headers=upload["uploadHeaders"], method="PUT")
with urlopen(put, timeout=60) as response:
    if not 200 <= response.status < 300:
        raise RuntimeError("PDF upload did not complete.")

document = api("/v1/documents", {
    "key": upload["key"], "fileName": "letter.pdf", "kind": "letter"
})
mailing["primaryDocumentId"] = document["id"]
quote = api("/v1/mailings/quote", mailing)
review = {"mailing": mailing, "document": document, "quote": quote}
with Path("mailing-review.json").open("x", encoding="utf-8") as output:
    json.dump(review, output, indent=2)
print(f"Quoted total: USD {quote['priceCents'] / 100:.2f}")
print("Review letter.pdf and mailing-review.json. No mailing was submitted.")

Understand the output before submitting

The review file contains three objects: mailing is the payload intended for quote and submission; document includes the registered PDF identity and metadata; quote contains priceCents and its breakdown. The script uses exclusive file creation so a previous review file is not silently overwritten. Start a new private working directory for a new intent.

The two authenticated POST calls after upload use the same account key. The storage PUT uses only the returned uploadHeaders; do not attach your IRIS bearer key to the storage host. The upload URL expires after ten minutes. If it expires, request a new upload and repeat preparation before seeking approval.

Add a separate, reviewed submission command

After a person has reviewed the PDF, addresses, options, price, and authority to send, your trusted submission component can POST the mailing object to /v1/mailings. Do not send the entire review wrapper. Submission requires mailings:write, sufficient prepaid credits, and an Idempotency-Key saved once for this intent.

Use the IRIS quickstart’s submission example as the next step and keep that command separate from this preparation script. Save the returned orderId, then use GET /v1/mailings/{id} for status. Before running live submissions, test your client against mocked responses for 401, 402, 409, 429, and an interrupted response. This tutorial is not a live mailing test or a sandbox service.

Sources & further reading

Product details checked against the IRIS API contract when this article was published. Platform references can change; consult the linked documentation when implementing.

Published by IRIS Postal Forms, operated by Great Lakes Computing Office LLC. Send a correction.

Put it into practice

Prepare your first API mailing

Use the API documentation to prepare and quote a letter, then require a separate approval before submission.