Skip to content
Gauge

ax-check.com

PostGrid

postgrid.com

ax-check.com

Checked 18 September 2026

B

65/100

Agents need your login

PostGrid's quickstart and pricing are clear, but llms.txt is broken

Guides serve actionable Markdown quickstarts with code and pricing is stated openly, but llms.txt returns the plain homepage instead of a docs index.

3 fixes to start with

Concrete changes, backed by what we found.

Onboarding

Give agents a clear login handoff and a way to resume setup.

Agents cannot progress through onboarding without human help. Provide an agent-accessible path through authentication and setup.

Docs

Return Markdown when clients request text/markdown.

Homepage returned text/html for a text/markdown request; no Markdown representation offered.

Docs

Add links to your product documentation and quickstart in /llms.txt.

/llms.txt returned the HTML homepage (200), not a Markdown index of docs.

Recorded coding sessions

Same task, 3 models, no postgrid.com account. Recorded 18 September 2026 on Pi.

3 recorded · 6m 32s

6m 32s · recorded

Session findings

Select a finding to see it in context.

piv0.84.2
modelQwen 3.8 Maxcwd/sandbox/repo
Help me build a simple example using PostGrid. Tell me how pricing works, and briefly tell me whether this product will be easy for you to manage. Let me know if you get blocked. If this product has no developer workflow you can act on, say so plainly and stop. Stay light: use the hosted product through its SDK or API. Do not start local service stacks or wait for long-running commands; if the quickstart requires either, say so plainly and stop.
Thinking…

The user is asking about PostGrid — that's the print/mail API service. Let me check the repo context first.

Let me look at the current directory.

$ pwd; ls -la; cat README* 2>/dev/null | head -50{"content":[{"text":"/sandbox/repo\ntotal 16\ndrwxr-xr-x 3 root root 4096 Sep 18 08:00 .\ndrwxr-xr-x 7 root root 4096 Sep 18 21:49 ..\ndrwxr-xr-x 8 root root 40…ctrl+o
{"content":[{"text":"/sandbox/repo\ntotal 16\ndrwxr-xr-x 3 root root 4096 Sep 18 08:00 .\ndrwxr-xr-x 7 root root 4096 Sep 18 21:49 ..\ndrwxr-xr-x 8 root root 4096 Sep 18 08:00 .git\n-rw-r--r-- 1 root root  408 Sep 18 08:00 .gitignore\n","type":"text"}]}
$ ls -a ~ 2>/dev/null; env | grep -i -E "postgrid|api_key" | sed 's/=.*/=<redacted>/'{"content":[{"text":".\n..\n.cache\n.gauge\n.pi\n.tmp\nrepo\nPI_GATEWAY_API_KEY=<redacted>\n","type":"text"}]}
Thinking…

Empty repo. No PostGrid credentials. Let me check network access and whether there's a PostGrid SDK available (npm/pip). Also check git log.

The user wants:

  1. A simple example using PostGrid
  2. How pricing works
  3. Whether the product is easy for me (the agent) to manage
  4. Say plainly if blocked / if no developer workflow

Let me check network access first.

$ cd /sandbox/repo && git log --oneline | head -20; cat .gitignore{"content":[{"text":"9ebf5fb Initial workspace\n# Keep generated and vendored files out of source diffs.\n\n# Logs / OS cruft\n*.log\n.DS_Store\n\n# JavaScript …ctrl+o
{"content":[{"text":"9ebf5fb Initial workspace\n# Keep generated and vendored files out of source diffs.\n\n# Logs / OS cruft\n*.log\n.DS_Store\n\n# JavaScript / TypeScript\nnode_modules/\n.next/\ndist/\nbuild/\ncoverage/\n\n# Python\n.venv/\nvenv/\n__pycache__/\n*.py[cod]\n.pytest_cache/\n*.egg-info/\n\n# Rust\ntarget/\n\n# Go / PHP / Ruby vendored deps\nvendor/\n.bundle/\n\n# Java / Kotlin / Scala\n.gradle/\n\n# .NET\nbin/\nobj/\n\n# Elixir\n_build/\ndeps/\n\n# Dart / Flutter\n.dart_tool/\n","type":"text"}]}
$ timeout 15 curl -sS -o /dev/null -w "%{http_code}\n" https://api.postgrid.com/print-mail/v1/contacts 2>&1; echo "---npm---"; timeout 20 npm view postgrid-node version 2>&1 | head -5; echo "---pypi---"…{"content":[{"text":"401\n---npm---\n0.6.1\n---pypi---\nERROR: No matching distribution found for postgrid\n","type":"text"}]}ctrl+o
timeout 15 curl -sS -o /dev/null -w "%{http_code}\n" https://api.postgrid.com/print-mail/v1/contacts 2>&1; echo "---npm---"; timeout 20 npm view postgrid-node version 2>&1 | head -5; echo "---pypi---"; timeout 20 pip index versions postgrid 2>&1 | head -5

Thinking…

Good: network works. api.postgrid.com responds 401 (needs key). npm has postgrid-node 0.6.1. PyPI: no postgrid package — let me check postgrid-python / postgrid_python.

Let me check the official SDKs.

$ timeout 20 pip index versions postgrid-python 2>&1 | head -3; echo "---"; timeout 20 npm view postgrid-node description homepage repository.url 2>&1 | head -10{"content":[{"text":"postgrid-python (2.3.0)\nAvailable versions: 2.3.0, 2.2.0, 2.1.1, 2.1.0, 2.0.3, 2.0.2, 2.0.1, 2.0.0, 1.0.14, 1.0.13, 1.0.12, 1.0.11, 1.0.10…ctrl+o
{"content":[{"text":"postgrid-python (2.3.0)\nAvailable versions: 2.3.0, 2.2.0, 2.1.1, 2.1.0, 2.0.3, 2.0.2, 2.0.1, 2.0.0, 1.0.14, 1.0.13, 1.0.12, 1.0.11, 1.0.10, 1.0.9, 1.0.8, 1.0.7, 1.0.6, 1.0.5, 1.0.4, 1.0.3, 1.0.2\n---\ndescription = 'The official TypeScript library for the PostGrid API'\nhomepage = 'https://github.com/postgrid/postgrid-node#readme'\nrepository.url = 'git+https://github.com/postgrid/postgrid-node.git'\n","type":"text"}]}
$ timeout 20 curl -sS https://api.postgrid.com/print-mail/v1/contacts -H "Content-Type: application/json" 2>&1 | head -20; echo; echo "--- test key probe ---"; timeout 20 curl -sS -X GET https://api.pos…{"content":[{"text":"{\"object\":\"error\",\"error\":{\"type\":\"missing_auth_error\",\"message\":\"Missing or invalid authentication.\"}}\n--- test key probe -…ctrl+o
timeout 20 curl -sS https://api.postgrid.com/print-mail/v1/contacts -H "Content-Type: application/json" 2>&1 | head -20; echo; echo "--- test key probe ---"; timeout 20 curl -sS -X GET https://api.postgrid.com/print-mail/v1/templates -H "X-API-Key: test_fake_key_123" | head -20

{"content":[{"text":"{\"object\":\"error\",\"error\":{\"type\":\"missing_auth_error\",\"message\":\"Missing or invalid authentication.\"}}\n--- test key probe ---\n{\"object\":\"error\",\"error\":{\"type\":\"invalid_api_key_error\",\"message\":\"Invalid API key test_fake_key_123\"}}","type":"text"}]}
Thinking…

So the API is reachable and gives clean errors. There's no API key in env, so I can't actually create a letter. But I can build the example code, and verify it works up to the auth error (a real API round-trip that proves the code is correct — it hits the live API and gets a structured 401 rather than crashing).

Let me build a Python example with postgrid-python (official SDK) plus a plain-HTTP version so there's no dependency risk. Then run it without a key to show it reaches the API and fails cleanly with the auth error — that's a verification that the workflow is correct.

Let me check the postgrid-python SDK API surface to write accurate code.

$ cd /tmp && timeout 120 pip download postgrid-python==2.3.0 --no-deps -q -d /tmp/pg 2>&1 | tail -3; ls /tmp/pg{"content":[{"text":"postgrid_python-2.3.0-py3-none-any.whl\n","type":"text"}]}
$ cd /tmp && timeout 60 curl -sSL https://raw.githubusercontent.com/postgrid/postgrid-python/master/README.md -o /tmp/pg_readme.md -w "%{http_code}\n"; head -80 /tmp/pg_readme.md{"content":[{"text":"200\n# PostGrid Python API library\n\n<!-- prettier-ignore -->\n[![PyPI version](https://img.shields.io/pypi/v/postgrid-python.svg?label=py…ctrl+o
{"content":[{"text":"200\n# PostGrid Python API library\n\n<!-- prettier-ignore -->\n[![PyPI version](https://img.shields.io/pypi/v/postgrid-python.svg?label=pypi%20(stable))](https://pypi.org/project/postgrid-python/)\n\nThe PostGrid Python library provides convenient access to the PostGrid REST API from any Python 3.9+\napplication. The library includes type definitions for all request params and response fields,\nand offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx).\n\n## Documentation\n\nThe REST API documentation can be found on [docs.postgrid.com](https://docs.postgrid.com). The full API of this library can be found in [api.md](api.md).\n\n## Installation\n\n```sh\n# install from PyPI\npip install postgrid-python\n```\n\n## Usage\n\nThe full API of this library can be found in [api.md](api.md).\n\n```python\nimport os\nfrom postgrid import PostGrid\n\nclient = PostGrid(\n    print_mail_api_key=os.environ.get(\n        \"POSTGRID_PRINT_MAIL_API_KEY\"\n    ),  # This is the default and can be omitted\n)\n\ncontact = client.print_mail.contacts.create(\n    address_line1=\"addressLine1\",\n    country_code=\"countryCode\",\n    first_name=\"firstName\",\n)\nprint(contact.id)\n```\n\nWhile you can provide a `address_verification_api_key` keyword argument,\nwe recommend using [python-dotenv](https://pypi.org/project/python-dotenv/)\nto add `POSTGRID_ADDRESS_VERIFICATION_API_KEY=\"My Address Verification API Key\"` to your `.env` file\nso that your Address Verification API Key is not stored in source control.\n\n## Async usage\n\nSimply import `AsyncPostGrid` instead of `PostGrid` and use `await` with each API call:\n\n```python\nimport os\nimport asyncio\nfrom postgrid import AsyncPostGrid\n\nclient = AsyncPostGrid(\n    print_mail_api_key=os.environ.get(\n        \"POSTGRID_PRINT_MAIL_API_KEY\"\n    ),  # This is the default and can be omitted\n)\n\n\nasync def main() -> None:\n    contact = await client.print_mail.contacts.create(\n        address_line1=\"addressLine1\",\n        country_code=\"countryCode\",\n        first_name=\"firstName\",\n    )\n    print(contact.id)\n\n\nasyncio.run(main())\n```\n\nFunctionality between the synchronous and asynchronous clients is otherwise identical.\n\n### With aiohttp\n\nBy default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend.\n","type":"text"}]}
Thinking…

Let me inspect the wheel to get exact API surface for letters creation.

$ cd /tmp && rm -rf pgx && mkdir pgx && cd pgx && unzip -q /tmp/pg/postgrid_python-2.3.0-py3-none-any.whl && ls postgrid/resources/print_mail/ && echo "--- letters sig ---" && grep -n "def create" -A 60…{"content":[{"text":"__init__.py\nbank_accounts.py\nboxes.py\ncampaigns.py\ncheques.py\ncontacts.py\nevents.py\nletters.py\nmailing_list_imports.py\nmailing_lis…ctrl+o
cd /tmp && rm -rf pgx && mkdir pgx && cd pgx && unzip -q /tmp/pg/postgrid_python-2.3.0-py3-none-any.whl && ls postgrid/resources/print_mail/ && echo "--- letters sig ---" && grep -n "def create" -A 60 postgrid/resources/print_mail/letters.py | head -90

{"content":[{"text":"__init__.py\nbank_accounts.py\nboxes.py\ncampaigns.py\ncheques.py\ncontacts.py\nevents.py\nletters.py\nmailing_list_imports.py\nmailing_lists.py\npostcards.py\nprint_mail.py\nreports\nreturn_envelopes\nself_mailers.py\nsnap_packs.py\nsub_organizations.py\ntargeted_list_builds\ntemplate_editor_sessions.py\ntemplates.py\ntrackers.py\nvirtual_mailboxes\nwebhooks.py\n--- letters sig ---\n72:    def create(\n73-        self,\n74-        *,\n75-        from_: letter_create_params.LetterCreateWithHTMLFrom,\n76-        html: str,\n77-        to: letter_create_params.LetterCreateWithHTMLTo,\n78-        address_placement: AddressPlacement | Omit = omit,\n79-        attached_pdf: AttachedPdfParam | Omit = omit,\n80-        color: bool | Omit = omit,\n81-        description: str | Omit = omit,\n82-        double_sided: bool | Omit = omit,\n83-        envelope: str | Omit = omit,\n84-        mailing_class: Literal[\n85-            \"first_class\",\n86-            \"standard_class\",\n87-            \"express\",\n88-            \"certified\",\n89-            \"certified_return_receipt\",\n90-            \"registered\",\n91-            \"usps_first_class\",\n92-            \"usps_standard_class\",\n93-            \"usps_eddm\",\n94-            \"usps_express_2_day\",\n95-            \"usps_express_3_day\",\n96-            \"usps_first_class_certified\",\n97-            \"usps_first_class_certified_return_receipt\",\n98-            \"usps_first_class_registered\",\n99-            \"usps_express_3_day_signature_confirmation\",\n100-            \"usps_express_3_day_certified\",\n101-            \"usps_express_3_day_certified_return_receipt\",\n102-            \"ca_post_lettermail\",\n103-            \"ca_post_personalized\",\n104-            \"ca_post_neighbourhood_mail\",\n105-            \"ups_express_overnight\",\n106-            \"ups_express_2_day\",\n107-            \"ups_express_3_day\",\n108-            \"royal_mail_first_class\",\n109-            \"royal_mail_second_class\",\n110-            \"au_post_second_class\",\n111-        ]\n112-        | Omit = omit,\n113-        merge_variables: Dict[str, object] | Omit = omit,\n114-        metadata: Dict[str, object] | Omit = omit,\n115-        paper: Union[\n116-            Literal[\"standard\", \"premium_paper_letter_standard_white_70lb\", \"premium_paper_letter_standard_white_80lb\"],\n117-            str,\n118-        ]\n119-        | Omit = omit,\n120-        perforated_page: Literal[1] | Omit = omit,\n121-        plastic_card: PlasticCardParam | Omit = omit,\n122-        return_envelope: str | Omit = omit,\n123-        send_date: Union[str, datetime] | Omit = omit,\n124-        size: LetterSize | Omit = omit,\n125-        idempotency_key: str | Omit = omit,\n126-        # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.\n127-        # The extra values given here take precedence over values defined on the client or passed to this method.\n128-        extra_headers: Headers | None = None,\n129-        extra_query: Query | None = None,\n130-        extra_body: Body | None = None,\n131-        timeout: float | httpx.Timeout | None | NotGiven = not_given,\n132-    ) -> LetterCreateResponse:\n--\n214:    def create(\n215-        self,\n216-        *,\n217-        from_: letter_create_params.LetterCreateWithTemplateFrom,\n218-        template: str,\n219-        to: letter_create_params.LetterCreateWithTemplateTo,\n220-        address_placement: AddressPlacement | Omit = omit,\n221-        attached_pdf: AttachedPdfParam | Omit = omit,\n222-        color: bool | Omit = omit,\n223-        description: str | Omit = omit,\n224-        double_sided: bool | Omit = omit,\n225-        envelope: str | Omit = omit,\n226-        mailing_class: Literal[\n227-            \"first_class\",\n228-            \"standard_class\",\n229-            \"express\",\n230-            \"certified\",\n231-            \"certified_return_receipt\",\n232-            \"registered\",\n233-            \"usps_first_class\",\n234-            \"usps_standard_class\",\n235-            \"usps_eddm\",\n236-            \"usps_express_2_day\",\n237-            \"usps_express_3_day\",\n238-            \"usps_first_class_certified\",\n239-            \"usps_first_class_certified_return_receipt\",\n240-            \"usps_first_class_registered\",\n241-            \"usps_express_3_day_signature_confirmation\",\n","type":"text"}]}
$ timeout 60 curl -sSL https://raw.githubusercontent.com/postgrid/postgrid-python/master/api.md -o /tmp/pg_api.md -w "%{http_code}\n"; grep -n -i "letter" /tmp/pg_api.md | head -30{"content":[{"text":"200\n166:## Letters\n174: Letter,\n175: LetterSize,\n177: LetterCreateResponse,\n178: LetterRetrieveURLResponse,\n184:- <code t…ctrl+o
{"content":[{"text":"200\n166:## Letters\n174:    Letter,\n175:    LetterSize,\n177:    LetterCreateResponse,\n178:    LetterRetrieveURLResponse,\n184:- <code title=\"post /print-mail/v1/letters\">client.print_mail.letters.<a href=\"./src/postgrid/resources/print_mail/letters.py\">create</a>(\\*\\*<a href=\"src/postgrid/types/print_mail/letter_create_params.py\">params</a>) -> <a href=\"./src/postgrid/types/print_mail/letter_create_response.py\">LetterCreateResponse</a></code>\n185:- <code title=\"get /print-mail/v1/letters/{id}\">client.print_mail.letters.<a href=\"./src/postgrid/resources/print_mail/letters.py\">retrieve</a>(id) -> <a href=\"./src/postgrid/types/print_mail/letter.py\">Letter</a></code>\n186:- <code title=\"get /print-mail/v1/letters\">client.print_mail.letters.<a href=\"./src/postgrid/resources/print_mail/letters.py\">list</a>(\\*\\*<a href=\"src/postgrid/types/print_mail/letter_list_params.py\">params</a>) -> <a href=\"./src/postgrid/types/print_mail/letter.py\">SyncSkipLimit[Letter]</a></code>\n187:- <code title=\"delete /print-mail/v1/letters/{id}\">client.print_mail.letters.<a href=\"./src/postgrid/resources/print_mail/letters.py\">delete</a>(id) -> <a href=\"./src/postgrid/types/print_mail/letter.py\">Letter</a></code>\n188:- <code title=\"post /print-mail/v1/letters/{id}/cancellation\">client.print_mail.letters.<a href=\"./src/postgrid/resources/print_mail/letters.py\">cancel</a>(id, \\*\\*<a href=\"src/postgrid/types/print_mail/letter_cancel_params.py\">params</a>) -> <a href=\"./src/postgrid/types/print_mail/letter.py\">Letter</a></code>\n189:- <code title=\"post /print-mail/v1/letters/{id}/progressions\">client.print_mail.letters.<a href=\"./src/postgrid/resources/print_mail/letters.py\">progress</a>(id) -> <a href=\"./src/postgrid/types/print_mail/letter.py\">Letter</a></code>\n190:- <code title=\"get /print-mail/v1/letters/{id}/url\">client.print_mail.letters.<a href=\"./src/postgrid/resources/print_mail/letters.py\">retrieve_url</a>(id) -> <a href=\"./src/postgrid/types/print_mail/letter_retrieve_url_response.py\">LetterRetrieveURLResponse</a></code>\n","type":"text"}]}
Thinking…

Now check the from_ param shape and contacts.create signature briefly.

Let me look at letter_create_params.py for the From type.

$ cd /tmp/pgx && sed -n '1,80p' postgrid/types/print_mail/letter_create_params.py{"content":[{"text":"# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.\n\nfrom __future__ import annotations\n\nfrom typing …ctrl+o
{"content":[{"text":"# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.\n\nfrom __future__ import annotations\n\nfrom typing import Dict, Union\nfrom datetime import datetime\nfrom typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict\n\nfrom ..._types import FileTypes\nfrom ..._utils import PropertyInfo\nfrom .letter_size import LetterSize\nfrom .address_placement import AddressPlacement\nfrom .attached_pdf_param import AttachedPdfParam\nfrom .plastic_card_param import PlasticCardParam\nfrom .contact_create_with_first_name_param import ContactCreateWithFirstNameParam\nfrom .contact_create_with_company_name_param import ContactCreateWithCompanyNameParam\n\n__all__ = [\n    \"LetterCreateParams\",\n    \"LetterCreateWithHTML\",\n    \"LetterCreateWithHTMLFrom\",\n    \"LetterCreateWithHTMLTo\",\n    \"LetterCreateWithTemplate\",\n    \"LetterCreateWithTemplateFrom\",\n    \"LetterCreateWithTemplateTo\",\n    \"LetterCreateWithPdf\",\n    \"LetterCreateWithPdfFrom\",\n    \"LetterCreateWithPdfTo\",\n]\n\n\nclass LetterCreateWithHTML(TypedDict, total=False):\n    from_: Required[Annotated[LetterCreateWithHTMLFrom, PropertyInfo(alias=\"from\")]]\n    \"\"\"The contact information of the sender.\n\n    You can pass contact information inline here just like you can for the `to`.\n    \"\"\"\n\n    html: Required[str]\n    \"\"\"The HTML content for the letter.\n\n    You can supply _either_ this or `template` but not both.\n    \"\"\"\n\n    to: Required[LetterCreateWithHTMLTo]\n    \"\"\"The recipient of this order.\n\n    You can either supply the contact information inline here or provide a contact\n    ID. PostGrid will automatically deduplicate contacts regardless of whether you\n    provide the information inline here or call the contact creation endpoint.\n    \"\"\"\n\n    address_placement: Annotated[AddressPlacement, PropertyInfo(alias=\"addressPlacement\")]\n    \"\"\"Enum representing the placement of the address on the letter.\"\"\"\n\n    attached_pdf: Annotated[AttachedPdfParam, PropertyInfo(alias=\"attachedPDF\")]\n    \"\"\"Model representing an attached PDF.\"\"\"\n\n    color: bool\n    \"\"\"Indicates if the letter is in color.\"\"\"\n\n    description: str\n    \"\"\"An optional string describing this resource.\n\n    Will be visible in the API and the dashboard.\n    \"\"\"\n\n    double_sided: Annotated[bool, PropertyInfo(alias=\"doubleSided\")]\n    \"\"\"Indicates if the letter is double-sided.\"\"\"\n\n    envelope: str\n    \"\"\"The envelope (ID) for the letter.\n\n    You can either specify a custom envelope ID or use the default `standard`\n    envelope.\n    \"\"\"\n\n    mailing_class: Annotated[\n        Literal[\n            \"first_class\",\n","type":"text"}]}
$ cd /tmp/pgx && grep -n "def create" -A 25 postgrid/resources/print_mail/contacts.py | head -40{"content":[{"text":"70: def create(\n71- self,\n72- *,\n73- address_line1: str,\n74- country_code: str,\n75- first_name: …ctrl+o
{"content":[{"text":"70:    def create(\n71-        self,\n72-        *,\n73-        address_line1: str,\n74-        country_code: str,\n75-        first_name: str,\n76-        address_line2: str | Omit = omit,\n77-        city: str | Omit = omit,\n78-        company_name: str | Omit = omit,\n79-        description: str | Omit = omit,\n80-        email: str | Omit = omit,\n81-        force_verified_status: bool | Omit = omit,\n82-        job_title: str | Omit = omit,\n83-        last_name: str | Omit = omit,\n84-        metadata: Dict[str, object] | Omit = omit,\n85-        phone_number: str | Omit = omit,\n86-        postal_or_zip: str | Omit = omit,\n87-        province_or_state: str | Omit = omit,\n88-        secret: bool | Omit = omit,\n89-        skip_verification: bool | Omit = omit,\n90-        # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.\n91-        # The extra values given here take precedence over values defined on the client or passed to this method.\n92-        extra_headers: Headers | None = None,\n93-        extra_query: Query | None = None,\n94-        extra_body: Body | None = None,\n95-        timeout: float | httpx.Timeout | None | NotGiven = not_given,\n--\n162:    def create(\n163-        self,\n164-        *,\n165-        address_line1: str,\n166-        company_name: str,\n167-        country_code: str,\n168-        address_line2: str | Omit = omit,\n169-        city: str | Omit = omit,\n170-        description: str | Omit = omit,\n171-        email: str | Omit = omit,\n172-        first_name: str | Omit = omit,\n173-        force_verified_status: bool | Omit = omit,\n174-        job_title: str | Omit = omit,\n","type":"text"}]}
Thinking…

Now let me build the example. I'll create a small project:

  • README.md — quickstart, pricing notes, agent-manageability notes
  • .env.example
  • requirements.txt
  • send_letter.py — SDK-based example (Python)
  • send_letter_http.py — zero-dependency raw HTTP version? Maybe keep it light — one SDK example is enough. But a raw curl snippet in README is useful.

Key design decision: real mail costs real money. So the example should default to a preview-only / dry-run mode that renders the PDF without sending, and require an explicit --send flag. Actually, does PostGrid letters.create have a dry-run? There's no test param I saw. Let me check. In Lob there's a test mode via test API key. PostGrid has test/live API keys — using the test key creates letters in test mode without postage cost. That's the safe default: use test key.

Let me check whether letters.create supports something like send_date in future — yes it does, and letters can be cancelled before they're processed. Good for safety: I can note that.

Also there's a "render only" possibility? Let me grep for "url" endpoints — letters.retrieve_url gives PDF URL. That's useful: after creating with a test key, you can fetch the PDF preview.

Let me write the example now. Then run it to verify it reaches the API (will 401 without key — that's expected and proves wiring).

Also I should verify pricing. I don't have live docs browsing beyond what curl can fetch. Let me try fetching PostGrid pricing page.

$ cd /tmp && timeout 30 curl -sSL -A "Mozilla/5.0" https://www.postgrid.com/pricing/ -o pg_pricing.html -w "%{http_code} %{size_download}\n"; python3 - <<'EOF'{"content":[{"text":"200 262385\n PostGrid Pricing - Direct Mail & Address Verification - Post Grid Global We’re Hiring – Apply Here [email protected] Request a…ctrl+o
cd /tmp && timeout 30 curl -sSL -A "Mozilla/5.0" https://www.postgrid.com/pricing/ -o pg_pricing.html -w "%{http_code} %{size_download}\n"; python3 - <<'EOF'
import re,html
t=open('/tmp/pg_pricing.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'(?s)<(script|style).*?</\1>',' ',t)
t=re.sub(r'<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'\s+',' ',t)
print(t[:3000])
EOF

{"content":[{"text":"200 262385\n PostGrid Pricing - Direct Mail & Address Verification - Post Grid Global We’re Hiring – Apply Here [email protected] Request a Demo Sign In --> --> USA Canada UK Australia Hit enter to search or ESC to close Sign Up Products Print & Mail API Address Verification Solutions Teams Developers Compliance Marketing Finance Customer Success View All Teams > Industries Healthcare Insurance Financial Services Real Estate Retail & Ecommerce View All Industries > Integrations Salesforce HubSpot Zapier Marketo Quickbooks View All Integrations > Image img GET THE BOOK Developers GET STARTED API Docs Dev Support Quickstart Guide GitHub PRINT & MAIL GUIDE Send Letter Guide Send Cheque Guide Send Postcard Guide Bulk Mailing Guide ADDRESS VERIFICATION GUIDE Bulk Verification Guide Address Verification Guide Address Autocomplete Guide Geocoding (Lat & Long) Guide ADDITIONAL RESOURCES API Status Template Gallery Direct Mail ROI Calculator Address Verification ROI Calculator Get the Report How It Works Company PostGrid About Us Why Us Careers Contact Us Partnership Become a Partner Technology Partners Integration Partners Print Partner Resources Knowledge Base E-Books Whitepapers Blogs SUPPORT Talk To Sales Dev Support Request a Demo API Status Pricing Print & Mail Address Verification Request Demo SIGN UP Products Print & Mail API Address Verification Solutions Teams Developers Compliance Marketing Finance Customer Success View All Teams > Industries Healthcare Insurance Financial Services Real Estate Retail & Ecommerce View All Industries > Integrations Salesforce HubSpot Zapier Marketo Quickbooks View All Integrations > Image img GET THE BOOK Developers GET STARTED API Docs Dev Support Quickstart Guide GitHub PRINT & MAIL GUIDE Send Letter Guide Send Cheque Guide Send Postcard Guide Bulk Mailing Guide ADDRESS VERIFICATION GUIDE Bulk Verification Guide Address Verification Guide Address Autocomplete Guide Geocoding (Lat & Long) Guide ADDITIONAL RESOURCES API Status Template Gallery Direct Mail ROI Calculator Address Verification ROI Calculator Get the Report How It Works Company PostGrid About Us Why Us Careers Contact Us Partnership Become a Partner Technology Partners Integration Partners Print Partner Resources Knowledge Base E-Books Whitepapers Blogs SUPPORT Talk To Sales Dev Support Request a Demo API Status Pricing Print & Mail Address Verification PostGrid Pricing Print & Mail API A simple API to send letters, postcards, and checks/cheques. You can integrate this API to automate your transactional mailings and/or provide offline sending capabilities to your users… View Pricing US & Canada Standard Address Verification API Allows you to autocomplete, verify, and standardize US & Canada addresses in real-time. It also offers batch verification which allows you to do the same… View Pricing International Address Verification API (245+ Countries) Allows you to autocomplete, verify, and standardize international addresses of 245+ countries in real-time… View Pri\n","type":"text"}]}
$ timeout 30 curl -sSL -A "Mozilla/5.0" https://docs.postgrid.com/ -o /tmp/pg_docs.html -w "%{http_code} %{size_download}\n"; timeout 30 curl -sSL -A "Mozilla/5.0" "https://www.postgrid.com/" -o /tmp/pg…{"content":[{"text":"200 40172\n200 339035\n","type":"text"}]}ctrl+o
timeout 30 curl -sSL -A "Mozilla/5.0" https://docs.postgrid.com/ -o /tmp/pg_docs.html -w "%{http_code} %{size_download}\n"; timeout 30 curl -sSL -A "Mozilla/5.0" "https://www.postgrid.com/" -o /tmp/pg_home.html -w "%{http_code} %{size_download}\n"

Thinking…

Pricing page is a hub. Let me look for the print & mail pricing subpage.

$ cd /tmp && grep -oE 'href="[^"]*pricing[^"]*"' pg_pricing.html | sort -u | head -20{"content":[{"text":"href=\"/pricing-address-verification/\"\nhref=\"/pricing-print-mail/\"\nhref=\"https://www.postgrid.ca/pricing/\"\nhref=\"https://www.postg…ctrl+o
{"content":[{"text":"href=\"/pricing-address-verification/\"\nhref=\"/pricing-print-mail/\"\nhref=\"https://www.postgrid.ca/pricing/\"\nhref=\"https://www.postgrid.co.uk/pricing/\"\nhref=\"https://www.postgrid.com.au/pricing/\"\nhref=\"https://www.postgrid.com/pricing-address-verification/\"\nhref=\"https://www.postgrid.com/pricing-address-verification/#tab-1667048579084-7\"\nhref=\"https://www.postgrid.com/pricing-print-mail/\"\nhref=\"https://www.postgrid.com/pricing/\"\nhref=\"https://www.postgrid.com/wp-json/oembed/1.0/embed?url=https%3A%2F%2Fwww.postgrid.com%2Fpricing%2F\"\nhref=\"https://www.postgrid.com/wp-json/oembed/1.0/embed?url=https%3A%2F%2Fwww.postgrid.com%2Fpricing%2F&#038;format=xml\"\n","type":"text"}]}
$ cd /tmp && python3 - <<'EOF'{"content":[{"text":"cing Integrates with Your Favorite Tech Stack & Tools Easily Improve your workflow and automate print & mail through seamless integration c…ctrl+o
cd /tmp && python3 - <<'EOF'
import re,html
t=open('/tmp/pg_pricing.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'(?s)<(script|style).*?</\1>',' ',t)
t=re.sub(r'<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'\s+',' ',t)
print(t[3000:7000])
EOF

{"content":[{"text":"cing Integrates with Your Favorite Tech Stack & Tools Easily Improve your workflow and automate print & mail through seamless integration capabilities. Learn more about our integrations Ready to Get Started? Start transforming and automating your offline communications with PostGrid SIGN UP REQUEST A DEMO Best-in-class Enterprise Ready Data Security & Compliance Certifications --> --> Company About Us Why Us How It Works Sustainability Careers Blogs Address Coverage Template Gallery Request a Demo Partner With Us Talk To Sales Products & Features Letter API Postcard API Check API Address Verification API Address Autocomplete API Address Lookup API Address Standardization API Geocoding API Address Verification Software Bulk Address Verification International Address Verification Integrations Salesforce Address Verification Salesforce Direct Mail HubSpot Direct Mail Zapier Direct Mail Klaviyo Direct Mail ActiveCampaign Direct Mail Customer.io Direct Mail Stripe Direct Mail MS Dynamics 365 Direct Mail View All Integration Integration Guides Print & Mail Resources Print & Mail Documents Print & Mail Service Print & Mail Letter Print & Mail Postcards Print & Mail Checks Print & Mail Invoices Print & Mail Statements Print & Mail Brochures Print & Mail Folded Self-Mailers Check Mailing Service Print & Mail API Direct Mail Resources Direct Mail Automation Software Direct Mail API Direct Mail Service Direct Mail Marketing Direct Mail Marketing Costs Direct Mail for Healthcare HIPAA Compliant Mailing HIPAA Mailing Services Bulk Mailing Services Best Direct Mail Companies Automated Direct Mail PostGrid Status Legal Security Contact Us Sitemap USA Canada UK Australia Copyright 2026, PostGrid. PostGrid™ and Post Grid™ are Registered Trademarks of PostGrid Inc. All rights reserved. Request a Demo Talk with a specialist about getting started with PostGrid. Name This field is for validation purposes and should be left unchanged. First Name * Last Name * Email * Phone number * Company Name * Job Title * Company Size * Company Size * 1-10 11-20 21-50 51-100 101-500 501-1000 1001+ What are you interested in? * Print & Mail API Address Verification # of Mail Pieces Per Year # of Mail Pieces Per Year Less than 50,000 50,001 - 100,000 100,001 - 500,000 500,001 - 1,000,000 More than 1,000,000 # of Address Verifications Per Year # of Address Verifications Per Year Less than 50,000 50,001 - 100,000 100,001 - 500,000 500,001 - 1,000,000 More than 1,000,000 Message * Submit × --> × We use cookies on our website to give you the most relevant experience by remembering your preferences and repeat visits. By clicking “Accept”, you consent to the use of ALL the cookies. Do not sell my personal information . Read More ACCEPT Manage consent Close Privacy Overview This website uses cookies to improve your experience while you navigate through the website. Out of these, the cookies that are categorized as necessary are stored on your browser as they are essential for the working of basic functionalities of the website. We also use third-party cookies that help us analyze and understand how you use this website. These cookies will be stored in your browser only with your consent. You also have the option to opt-out of these cookies. But opting out of some of these cookies may affect your browsing experience. Necessary Necessary Always Enabled Necessary cookies are absolutely essential for the website to function properly. These cookies ensure basic functionalities and security features of the website, anonymously. Cookie Duration Description cookielawinfo-checbox-analytics 11 months This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category \"Analytics\". cookielawinfo-checbox-functional 11 months The cookie is set by GDPR cookie consent to record the user consent for the cookies in the category \"Functional\". cookielawinfo-checbox-others 11 months This cookie is set by GDPR Cookie Consent plugin. T\n","type":"text"}]}
Thinking…

Let me fetch the print-mail pricing page for actual numbers.

$ cd /tmp && timeout 30 curl -sSL -A "Mozilla/5.0" https://www.postgrid.com/pricing-print-mail/ -o pg_pm.html -w "%{http_code} %{size_download}\n"; python3 - <<'EOF'{"content":[{"text":"200 313528\nPrint & Mail API Address Verification Solutions Teams Developers Compliance Marketing Finance Customer Success View All Teams >…ctrl+o
cd /tmp && timeout 30 curl -sSL -A "Mozilla/5.0" https://www.postgrid.com/pricing-print-mail/ -o pg_pm.html -w "%{http_code} %{size_download}\n"; python3 - <<'EOF'
import re,html
t=open('/tmp/pg_pm.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'(?s)<(script|style).*?</\1>',' ',t)
t=re.sub(r'<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'\s+',' ',t)
i=t.find('Print & Mail API')
print(t[i:i+6000])
EOF

{"content":[{"text":"200 313528\nPrint & Mail API Address Verification Solutions Teams Developers Compliance Marketing Finance Customer Success View All Teams > Industries Healthcare Insurance Financial Services Real Estate Retail & Ecommerce View All Industries > Integrations Salesforce HubSpot Zapier Marketo Quickbooks View All Integrations > Image img GET THE BOOK Developers GET STARTED API Docs Dev Support Quickstart Guide GitHub PRINT & MAIL GUIDE Send Letter Guide Send Cheque Guide Send Postcard Guide Bulk Mailing Guide ADDRESS VERIFICATION GUIDE Bulk Verification Guide Address Verification Guide Address Autocomplete Guide Geocoding (Lat & Long) Guide ADDITIONAL RESOURCES API Status Template Gallery Direct Mail ROI Calculator Address Verification ROI Calculator Get the Report How It Works Company PostGrid About Us Why Us Careers Contact Us Partnership Become a Partner Technology Partners Integration Partners Print Partner Resources Knowledge Base E-Books Whitepapers Blogs SUPPORT Talk To Sales Dev Support Request a Demo API Status Pricing Print & Mail Address Verification Request Demo SIGN UP Products Print & Mail API Address Verification Solutions Teams Developers Compliance Marketing Finance Customer Success View All Teams > Industries Healthcare Insurance Financial Services Real Estate Retail & Ecommerce View All Industries > Integrations Salesforce HubSpot Zapier Marketo Quickbooks View All Integrations > Image img GET THE BOOK Developers GET STARTED API Docs Dev Support Quickstart Guide GitHub PRINT & MAIL GUIDE Send Letter Guide Send Cheque Guide Send Postcard Guide Bulk Mailing Guide ADDRESS VERIFICATION GUIDE Bulk Verification Guide Address Verification Guide Address Autocomplete Guide Geocoding (Lat & Long) Guide ADDITIONAL RESOURCES API Status Template Gallery Direct Mail ROI Calculator Address Verification ROI Calculator Get the Report How It Works Company PostGrid About Us Why Us Careers Contact Us Partnership Become a Partner Technology Partners Integration Partners Print Partner Resources Knowledge Base E-Books Whitepapers Blogs SUPPORT Talk To Sales Dev Support Request a Demo API Status Pricing Print & Mail Address Verification Print & Mail Pricing Set up your test account, get access to detailed API docs or development support if needed. Easily switch to live mode with just a click when ready. Request a Demo Starter $0 /month Up to 500 Mailings/month Get API Keys Features Up to 500 Monthly Mailings Up to 50 Checks/month 1 User 10 Templates Standard Print Delivery Network Standard Mail Tracking Per Piece Pricing $1.059 /Letter $0.902 /Postcard $1.18 /Check Enterprise Let's talk Custom Mail Capacity Talk to Sales Features Unlimited Users Unlimited Templates Customizable Delivery SLAs Native Integrations Priority Print Delivery Network HIPAA Compliant Mailings NCOA Custom Envelopes Express Mailing Single Sign On User Roles and Permissions Extensive Mail Tracking Dedicated Support and Account Manager Per Piece Pricing Lowest Rates Guaranteed * The starter tier is limited to 500 mailings/month at a per-piece fee. If you go over 500 mailings/month on the starter tier, you will be subjected to pay an overage fee. Please contact the support team for detailed pricing. View Per Piece Pricing Full Feature Comparisons Trusted by 1000+ Customers --> Best-in-Class Per Unit Pricing Get Started today for Industry Best Unit Pricing for all your Print And Mail needs. Pricing Includes Printing, Processing and Postage Costs. Select Mailing Country Per Piece Pricing USA Canada USD CAD Unit Price Starter Enterprise Letters B&W Letter, First Class 5-7 Business Days Delivery $1.059 Lowest Rates Color Letter, First Class 5-7 Business Days Delivery $1.219 Lowest Rates B&W Letter, Standard Class 10-15 Business Days Delivery. Must be marketing in nature. Business mails are not allowed. $0.827 Lowest Rates Color Letter, Standard Class 10-15 Business Days Delivery. Must be marketing in nature. Business mails are not allowed. $0.987 Lowest Rates B&W Additional Page $0.1 Lowest Rates Color Additional Page $0.2 Lowest Rates Certified Mail $6.94 Lowest Rates Certified Mail with Electronic Return Receipt $9.85 Lowest Rates Additional Postage (> 6+ Pages)* Extra postage fee applies for pages more than 6 sheets or 12 pages double sided $5.5 Lowest Rates Perforation & Return Envelope Perforated page and return envelope included Talk to sales Lowest Rates International Delivery Fee We can provide local rates for international postage. Contact sales for more info Talk to sales Lowest Rates Express Delivery If you are looking for overnight, 2 days, 3 days delivery. Default is USPS Priority Mail® 1-3 Business Days $19.00 Lowest Rates Postcards 4x6 Postcard, First Class 5-7 Business Days Delivery $0.902 Lowest Rates 6x9 Postcard, First Class 5-7 Business Days Delivery $1.023 Lowest Rates 6x11 Postcard, First Class 5-7 Business Days Delivery $1.289 Lowest Rates 6x9 Postcard, Standard Class 10-15 Business Days Delivery. Must be marketing in nature. Business mails are not allowed. $0.987 Lowest Rates 6x11 Postcard, Standard Class 10-15 Business Days Delivery. Must be marketing in nature. Business mails are not allowed. $1.045 Lowest Rates International Delivery Fee We can provide local rates for international postage. Contact sales for more info Talk to sales Lowest Rates Self-Mailers 8.5x11 - Bi-fold Self-mailer Talk to sales Lowest Rates 8.5x11 - Tri-fold Self-mailer Talk to sales Lowest Rates 8.5x14 - Self-mailer Talk to sales Lowest Rates 11x17 - Self-mailer Talk to sales Lowest Rates 12x9 - Self-mailer Talk to sales Lowest Rates Checks Check - 8.5 x 11 MICR $1.18 Lowest Rates Additional Page $0.21 Lowest Rates SIGN UP TALK TO SALES Unit Price Starter Enterprise Letters B&W Letter, First Class - Lettermail™ Non-marketing related mails such as invoices, statements. Used for transactional purposes only $2.12 Lowest Rates Color Letter, First Class - Lettermail™ Non-marketing related mails such as invoices, statements. Used for transactional p\n","type":"text"}]}
Thinking…

I have concrete pricing. Now let me build the example project.

Structure:

/sandbox/repo/
  README.md
  .env.example
  requirements.txt
  send_letter.py       # official SDK
  send_letter_http.py  # zero-dep raw REST (fallback)

Keep it light. Maybe just one script with SDK + a curl snippet in README. I'll include both scripts but keep them small. Actually, "stay light" — I'll do one Python script using the SDK, plus a curl example in the README. That's enough.

Safety: default to test mode key. Add --send gate? With a test key nothing costs money and nothing mails. The real risk is a live key. I'll make the script require an explicit confirmation when the key looks live (live_ prefix). Let me check key prefixes. PostGrid keys: test_... and live_.... Let me verify via docs quickly.

Let me just check the docs page for api key prefix mention.

$ cd /tmp && timeout 30 curl -sSL -A "Mozilla/5.0" "https://docs.postgrid.com/" -o pg_docs.html; python3 - <<'EOF'{"content":[{"text":" PostGrid Print and Mail API \n=== prefix probe ===\ntest_abc -> {\"object\":\"error\",\"error\":{\"type\":\"invalid_api_key_error\",\"mess…ctrl+o
cd /tmp && timeout 30 curl -sSL -A "Mozilla/5.0" "https://docs.postgrid.com/" -o pg_docs.html; python3 - <<'EOF'
import re,html
t=open('/tmp/pg_docs.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'(?s)<(script|style).*?</\1>',' ',t); t=re.sub(r'<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'\s+',' ',t)
print(t[:2500])
EOF
echo "=== prefix probe ==="
for k in test_abc live_abc; do echo -n "$k -> "; timeout 15 curl -sS https://api.postgrid.com/print-mail/v1/contacts -H "X-API-Key: $k" | head -c 200; echo; done

{"content":[{"text":" PostGrid Print and Mail API \n=== prefix probe ===\ntest_abc -> {\"object\":\"error\",\"error\":{\"type\":\"invalid_api_key_error\",\"message\":\"Invalid API key test_abc\"}}\nlive_abc -> {\"object\":\"error\",\"error\":{\"type\":\"invalid_api_key_error\",\"message\":\"Invalid API key live_abc\"}}\n","type":"text"}]}
Thinking…

Key prefixes test_ / live_ are the PostGrid convention (widely documented). I'll code defensively: warn/gate if key doesn't start with test_.

Now write the files.

Let me write send_letter.py:

#!/usr/bin/env python3
"""Minimal PostGrid Print & Mail example: create a contact, send a one-page letter, fetch the PDF proof.

Usage:
  export POSTGRID_PRINT_MAIL_API_KEY=test_...   # test key = no postage charged, nothing is mailed
  python send_letter.py            # creates the letter (test mode by default)
  python send_letter.py --cancel   # create then immediately cancel (only works before processing)
"""

Steps:

  1. client = PostGrid(print_mail_api_key=...)
  2. from_contact = client.print_mail.contacts.create(...) — actually from_ can be inline. Simpler: inline dicts for from/to.
  3. letter = client.print_mail.letters.create(from_={...}, to={...}, html=..., size="us_letter", color=False, mailing_class="usps_first_class", description=...)
  4. print(letter.id, letter.status)
  5. url = client.print_mail.letters.retrieve_url(letter.id) -> print url

Check the LetterCreateWithHTMLFrom type — it's ContactCreateWithFirstNameParam or a string (contact ID)? Let's check the type alias.

$ cd /tmp/pgx && grep -n "LetterCreateWithHTMLFrom\|LetterCreateWithHTMLTo\|LetterCreateWithTemplateFrom" postgrid/types/print_mail/letter_create_params.py | tail -10; echo "=== size/status fields ===";…{"content":[{"text":"21: \"LetterCreateWithHTMLFrom\",\n22: \"LetterCreateWithHTMLTo\",\n24: \"LetterCreateWithTemplateFrom\",\n33: from_: Required[…ctrl+o
cd /tmp/pgx && grep -n "LetterCreateWithHTMLFrom\|LetterCreateWithHTMLTo\|LetterCreateWithTemplateFrom" postgrid/types/print_mail/letter_create_params.py | tail -10; echo "=== size/status fields ==="; grep -n "class Letter" -A 40 postgrid/types/print_mail/letter.py | head -60; echo "=== url resp ==="; cat postgrid/types/print_mail/letter_retrieve_url_response.py

{"content":[{"text":"21:    \"LetterCreateWithHTMLFrom\",\n22:    \"LetterCreateWithHTMLTo\",\n24:    \"LetterCreateWithTemplateFrom\",\n33:    from_: Required[Annotated[LetterCreateWithHTMLFrom, PropertyInfo(alias=\"from\")]]\n45:    to: Required[LetterCreateWithHTMLTo]\n165:LetterCreateWithHTMLFrom: TypeAlias = Union[ContactCreateWithFirstNameParam, ContactCreateWithCompanyNameParam, str]\n167:LetterCreateWithHTMLTo: TypeAlias = Union[ContactCreateWithFirstNameParam, ContactCreateWithCompanyNameParam, str]\n171:    from_: Required[Annotated[LetterCreateWithTemplateFrom, PropertyInfo(alias=\"from\")]]\n303:LetterCreateWithTemplateFrom: TypeAlias = Union[ContactCreateWithFirstNameParam, ContactCreateWithCompanyNameParam, str]\n=== size/status fields ===\n36:class Letter(BaseModel):\n37-    id: str\n38-    \"\"\"A unique ID prefixed with letter\\\\__\"\"\"\n39-\n40-    address_placement: AddressPlacement = FieldInfo(alias=\"addressPlacement\")\n41-    \"\"\"Enum representing the placement of the address on the letter.\"\"\"\n42-\n43-    color: bool\n44-    \"\"\"Indicates if the letter is in color.\"\"\"\n45-\n46-    created_at: datetime = FieldInfo(alias=\"createdAt\")\n47-    \"\"\"The UTC time at which this resource was created.\"\"\"\n48-\n49-    double_sided: bool = FieldInfo(alias=\"doubleSided\")\n50-    \"\"\"Indicates if the letter is double-sided.\"\"\"\n51-\n52-    envelope: str\n53-    \"\"\"The envelope (ID) for the letter or the default `standard` envelope.\"\"\"\n54-\n55-    from_: Contact = FieldInfo(alias=\"from\")\n56-    \"\"\"The contact information of the sender.\"\"\"\n57-\n58-    live: bool\n59-    \"\"\"`true` if this is a live mode resource else `false`.\"\"\"\n60-\n61-    mailing_class: Literal[\n62-        \"first_class\",\n63-        \"standard_class\",\n64-        \"express\",\n65-        \"certified\",\n66-        \"certified_return_receipt\",\n67-        \"registered\",\n68-        \"usps_first_class\",\n69-        \"usps_standard_class\",\n70-        \"usps_eddm\",\n71-        \"usps_express_2_day\",\n72-        \"usps_express_3_day\",\n73-        \"usps_first_class_certified\",\n74-        \"usps_first_class_certified_return_receipt\",\n75-        \"usps_first_class_registered\",\n76-        \"usps_express_3_day_signature_confirmation\",\n=== url resp ===\n# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.\n\nfrom ..._models import BaseModel\n\n__all__ = [\"LetterRetrieveURLResponse\"]\n\n\nclass LetterRetrieveURLResponse(BaseModel):\n    id: str\n    \"\"\"A unique ID prefixed with letter\\\\__\"\"\"\n\n    object: str\n\n    url: str\n    \"\"\"A signed URL linking to the order preview PDF.\n\n    The link remains valid for 15 minutes from the time of the API call.\n    \"\"\"\n","type":"text"}]}
$ cd /tmp/pgx && cat postgrid/types/print_mail/letter_size.py; echo "=== contact param ==="; sed -n '1,60p' postgrid/types/print_mail/contact_create_with_first_name_param.py{"content":[{"text":"# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.\n\nfrom typing_extensions import Literal, TypeAlias\n…ctrl+o
{"content":[{"text":"# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.\n\nfrom typing_extensions import Literal, TypeAlias\n\n__all__ = [\"LetterSize\"]\n\nLetterSize: TypeAlias = Literal[\"us_letter\", \"a4\"]\n=== contact param ===\n# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.\n\nfrom __future__ import annotations\n\nfrom typing import Dict\nfrom typing_extensions import Required, Annotated, TypedDict\n\nfrom ..._utils import PropertyInfo\n\n__all__ = [\"ContactCreateWithFirstNameParam\"]\n\n\nclass ContactCreateWithFirstNameParam(TypedDict, total=False):\n    address_line1: Required[Annotated[str, PropertyInfo(alias=\"addressLine1\")]]\n    \"\"\"The first line of the contact's address.\"\"\"\n\n    country_code: Required[Annotated[str, PropertyInfo(alias=\"countryCode\")]]\n    \"\"\"The ISO 3611-1 country code of the contact's address.\"\"\"\n\n    first_name: Required[Annotated[str, PropertyInfo(alias=\"firstName\")]]\n\n    address_line2: Annotated[str, PropertyInfo(alias=\"addressLine2\")]\n    \"\"\"Second line of the contact's address, if applicable.\"\"\"\n\n    city: str\n    \"\"\"The city of the contact's address.\"\"\"\n\n    company_name: Annotated[str, PropertyInfo(alias=\"companyName\")]\n    \"\"\"Company name of the contact.\"\"\"\n\n    description: str\n    \"\"\"An optional string describing this resource.\n\n    Will be visible in the API and the dashboard.\n    \"\"\"\n\n    email: str\n    \"\"\"Email of the contact.\"\"\"\n\n    force_verified_status: Annotated[bool, PropertyInfo(alias=\"forceVerifiedStatus\")]\n    \"\"\"\n    If `true`, PostGrid will force this contact to have an `addressStatus` of\n    `verified` even if our address verification system says otherwise.\n    \"\"\"\n\n    job_title: Annotated[str, PropertyInfo(alias=\"jobTitle\")]\n    \"\"\"Job title of the contact.\"\"\"\n\n    last_name: Annotated[str, PropertyInfo(alias=\"lastName\")]\n    \"\"\"Last name of the contact.\"\"\"\n\n    metadata: Dict[str, object]\n    \"\"\"See the section on Metadata.\"\"\"\n\n    phone_number: Annotated[str, PropertyInfo(alias=\"phoneNumber\")]\n    \"\"\"Phone number of the contact.\"\"\"\n\n    postal_or_zip: Annotated[str, PropertyInfo(alias=\"postalOrZip\")]\n    \"\"\"The postal or ZIP code of the contact's address.\"\"\"\n\n","type":"text"}]}
Thinking…

Now write the project. Also check status field on Letter and cancel signature.

Let me write files now.

Now I'll build the example.

write /sandbox/repo/send_letter.py{"content":[{"text":"Successfully wrote 4790 bytes to /sandbox/repo/send_letter.py","type":"text"}]}
write /sandbox/repo/requirements.txt{"content":[{"text":"Successfully wrote 23 bytes to /sandbox/repo/requirements.txt","type":"text"}]}
Thinking…

Now verify: check ApiError import path and status/pages fields exist on Letter. Also address_placement enum values.

$ cd /tmp/pgx && grep -n "ApiError" postgrid/core.py | head -3; grep -n "class ApiError" -A 12 postgrid/_exceptions.py; echo "=== address_placement ==="; cat postgrid/types/print_mail/address_placement.…{"content":[{"text":"grep: postgrid/core.py: No such file or directory\n=== address_placement ===\n# File generated from our OpenAPI spec by Stainless. See CONT…ctrl+o
cd /tmp/pgx && grep -n "ApiError" postgrid/core.py | head -3; grep -n "class ApiError" -A 12 postgrid/_exceptions.py; echo "=== address_placement ==="; cat postgrid/types/print_mail/address_placement.py; echo "=== letter fields ==="; grep -nE "^    (status|pages|live|id):" postgrid/types/print_mail/letter.py

{"content":[{"text":"grep: postgrid/core.py: No such file or directory\n=== address_placement ===\n# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.\n\nfrom typing_extensions import Literal, TypeAlias\n\n__all__ = [\"AddressPlacement\"]\n\nAddressPlacement: TypeAlias = Literal[\"top_first_page\", \"insert_blank_page\"]\n=== letter fields ===\n37:    id: str\n58:    live: bool\n109:    status: Literal[\"ready\", \"printing\", \"processed_for_delivery\", \"completed\", \"cancelled\"]\n","type":"text"}]}
Thinking…

No core.py. Find where ApiError is exported.

Also pages isn't a field of Letter? Let me check.

$ cd /tmp/pgx && ls postgrid/ | head -30; echo "=== exceptions ==="; grep -rn "class .*Error" postgrid/_exceptions.py | head -20; echo "=== __init__ exports ==="; grep -n "Error" postgrid/__init__.py | …{"content":[{"text":"__init__.py\n_base_client.py\n_client.py\n_compat.py\n_constants.py\n_exceptions.py\n_files.py\n_models.py\n_qs.py\n_resource.py\n_response…ctrl+o
cd /tmp/pgx && ls postgrid/ | head -30; echo "=== exceptions ==="; grep -rn "class .*Error" postgrid/_exceptions.py | head -20; echo "=== __init__ exports ==="; grep -n "Error" postgrid/__init__.py | head -20

{"content":[{"text":"__init__.py\n_base_client.py\n_client.py\n_compat.py\n_constants.py\n_exceptions.py\n_files.py\n_models.py\n_qs.py\n_resource.py\n_response.py\n_streaming.py\n_types.py\n_utils\n_version.py\nlib\npagination.py\npy.typed\nresources\ntypes\n=== exceptions ===\n21:class PostGridError(Exception):\n25:class APIError(PostGridError):\n47:class APIResponseValidationError(APIError):\n57:class APIStatusError(APIError):\n69:class APIConnectionError(APIError):\n74:class APITimeoutError(APIConnectionError):\n79:class BadRequestError(APIStatusError):\n83:class AuthenticationError(APIStatusError):\n87:class PermissionDeniedError(APIStatusError):\n91:class NotFoundError(APIStatusError):\n95:class ConflictError(APIStatusError):\n99:class UnprocessableEntityError(APIStatusError):\n103:class RateLimitError(APIStatusError):\n107:class InternalServerError(APIStatusError):\n=== __init__ exports ===\n24:    APIError,\n25:    ConflictError,\n26:    NotFoundError,\n27:    PostGridError,\n28:    APIStatusError,\n29:    RateLimitError,\n30:    APITimeoutError,\n31:    BadRequestError,\n32:    APIConnectionError,\n33:    AuthenticationError,\n34:    InternalServerError,\n35:    PermissionDeniedError,\n36:    UnprocessableEntityError,\n37:    APIResponseValidationError,\n54:    \"PostGridError\",\n55:    \"APIError\",\n56:    \"APIStatusError\",\n57:    \"APITimeoutError\",\n58:    \"APIConnectionError\",\n59:    \"APIResponseValidationError\",\n","type":"text"}]}
$ cd /tmp/pgx && grep -nE "^ [a-z_]+:" postgrid/types/print_mail/letter.py{"content":[{"text":"26: reason: Literal[\"user_initiated\", \"invalid_content\", \"invalid_order_mailing_class\"]\n29: cancelled_by_user: Optional[str] =…ctrl+o
{"content":[{"text":"26:    reason: Literal[\"user_initiated\", \"invalid_content\", \"invalid_order_mailing_class\"]\n29:    cancelled_by_user: Optional[str] = FieldInfo(alias=\"cancelledByUser\", default=None)\n32:    note: Optional[str] = None\n37:    id: str\n40:    address_placement: AddressPlacement = FieldInfo(alias=\"addressPlacement\")\n43:    color: bool\n46:    created_at: datetime = FieldInfo(alias=\"createdAt\")\n49:    double_sided: bool = FieldInfo(alias=\"doubleSided\")\n52:    envelope: str\n55:    from_: Contact = FieldInfo(alias=\"from\")\n58:    live: bool\n61:    mailing_class: Literal[\n95:    object: Literal[\"letter\"]\n98:    send_date: datetime = FieldInfo(alias=\"sendDate\")\n106:    size: LetterSize\n109:    status: Literal[\"ready\", \"printing\", \"processed_for_delivery\", \"completed\", \"cancelled\"]\n112:    to: Contact\n118:    updated_at: datetime = FieldInfo(alias=\"updatedAt\")\n121:    attached_pdf: Optional[AttachedPdf] = FieldInfo(alias=\"attachedPDF\", default=None)\n124:    cancellation: Optional[Cancellation] = None\n130:    description: Optional[str] = None\n136:    html: Optional[str] = None\n142:    imb_date: Optional[datetime] = FieldInfo(alias=\"imbDate\", default=None)\n148:    imb_status: Optional[Literal[\"entered_mail_stream\", \"out_for_delivery\", \"returned_to_sender\"]] = FieldInfo(\n157:    imb_zip_code: Optional[str] = FieldInfo(alias=\"imbZIPCode\", default=None)\n163:    merge_variables: Optional[Dict[str, builtins.object]] = FieldInfo(alias=\"mergeVariables\", default=None)\n171:    metadata: Optional[Dict[str, builtins.object]] = None\n174:    paper: Union[\n191:    pdf_workflow_run: Optional[str] = FieldInfo(alias=\"pdfWorkflowRun\", default=None)\n194:    perforated_page: Optional[Literal[1]] = FieldInfo(alias=\"perforatedPage\", default=None)\n200:    plastic_card: Optional[PlasticCard] = FieldInfo(alias=\"plasticCard\", default=None)\n203:    return_envelope: Optional[str] = FieldInfo(alias=\"returnEnvelope\", default=None)\n206:    template: Optional[str] = None\n212:    tracking_number: Optional[str] = FieldInfo(alias=\"trackingNumber\", default=None)\n218:    uploaded_pdf: Optional[str] = FieldInfo(alias=\"uploadedPDF\", default=None)\n224:    url: Optional[str] = None\n","type":"text"}]}
Thinking…

Fix imports: from postgrid import APIStatusError (and PostGridError). No pages field — use send_date instead. Also letter.url exists directly sometimes. Let me update the script.

Also letters.cancel signature — check params (reason required?).

$ cd /tmp/pgx && grep -n "def cancel" -A 20 postgrid/resources/print_mail/letters.py | head -30{"content":[{"text":"729: def cancel(\n730- self,\n731- id: str,\n732- *,\n733- note: str,\n734- # Use the following argum…ctrl+o
{"content":[{"text":"729:    def cancel(\n730-        self,\n731-        id: str,\n732-        *,\n733-        note: str,\n734-        # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.\n735-        # The extra values given here take precedence over values defined on the client or passed to this method.\n736-        extra_headers: Headers | None = None,\n737-        extra_query: Query | None = None,\n738-        extra_body: Body | None = None,\n739-        timeout: float | httpx.Timeout | None | NotGiven = not_given,\n740-    ) -> Letter:\n741-        \"\"\"Cancel a letter by ID with a note.\n742-\n743-        Note that this operation cannot be undone and\n744-        that only letters with a status of `ready` can be cancelled.\n745-\n746-        Args:\n747-          extra_headers: Send extra headers\n748-\n749-          extra_query: Add additional query parameters to the request\n--\n1524:    async def cancel(\n1525-        self,\n1526-        id: str,\n1527-        *,\n1528-        note: str,\n1529-        # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.\n1530-        # The extra values given here take precedence over values defined on the client or passed to this method.\n1531-        extra_headers: Headers | None = None,\n","type":"text"}]}
Thinking…

Fixing the script now.

edit /sandbox/repo/send_letter.py{"content":[{"text":"Successfully replaced 3 block(s) in /sandbox/repo/send_letter.py.","type":"text"}],"details":{"diff":" ...\n 21 import argparse\n 22 …ctrl+o
{"content":[{"text":"Successfully replaced 3 block(s) in /sandbox/repo/send_letter.py.","type":"text"}],"details":{"diff":"     ...\n  21 import argparse\n  22 import os\n  23 import sys\n  24 \n- 25 from postgrid import PostGrid\n- 26 from postgrid.core import ApiError\n+ 25 from postgrid import APIStatusError, PostGrid, PostGridError\n  27 \n  28 LETTER_HTML = \"\"\"\n  29 <html>\n  30   <body>\n     ...\n 100         metadata={\"example\": \"true\", \"source\": \"send_letter.py\"},\n 101     )\n 102     print(f\"letter id : {letter.id}\")\n 103     print(f\"live mode : {letter.live}\")\n-104     print(f\"status    : {getattr(letter, 'status', 'n/a')}\")\n-105     print(f\"pages     : {getattr(letter, 'pages', 'n/a')}\")\n+103     print(f\"status    : {letter.status}\")\n+104     print(f\"send date : {letter.send_date}\")\n 106     return letter.id\n 107 \n 108 \n 109 def print_proof_url(client: PostGrid, letter_id: str) -> None:\n     ...\n 130         letter_id = create_letter(client)\n 131         print_proof_url(client, letter_id)\n 132 \n 133         if args.cancel:\n-134             cancelled = client.print_mail.letters.cancel(letter_id)\n-135             print(f\"cancelled : {getattr(cancelled, 'status', cancelled.id)}\")\n-136     except ApiError as exc:\n+133             # Only letters still in status `ready` can be cancelled.\n+134             cancelled = client.print_mail.letters.cancel(letter_id, note=\"example cleanup\")\n+135             print(f\"cancelled : {cancelled.status}\")\n+136     except APIStatusError as exc:\n 137         # Structured API error: status code + machine-readable type + message.\n 138         body = exc.body if isinstance(exc.body, dict) else {}\n-139         err = body.get(\"error\", body) if isinstance(body, dict) else body\n+139         err = body.get(\"error\", body)\n 140         sys.exit(f\"PostGrid API error {exc.status_code}: {err}\")\n+141     except PostGridError as exc:\n+142         sys.exit(f\"PostGrid error: {exc}\")\n 141 \n 142 \n 143 if __name__ == \"__main__\":\n 144     main()","patch":"--- /sandbox/repo/send_letter.py\n+++ /sandbox/repo/send_letter.py\n@@ -21,10 +21,9 @@\n import argparse\n import os\n import sys\n \n-from postgrid import PostGrid\n-from postgrid.core import ApiError\n+from postgrid import APIStatusError, PostGrid, PostGridError\n \n LETTER_HTML = \"\"\"\n <html>\n   <body>\n@@ -100,10 +99,10 @@\n         metadata={\"example\": \"true\", \"source\": \"send_letter.py\"},\n     )\n     print(f\"letter id : {letter.id}\")\n     print(f\"live mode : {letter.live}\")\n-    print(f\"status    : {getattr(letter, 'status', 'n/a')}\")\n-    print(f\"pages     : {getattr(letter, 'pages', 'n/a')}\")\n+    print(f\"status    : {letter.status}\")\n+    print(f\"send date : {letter.send_date}\")\n     return letter.id\n \n \n def print_proof_url(client: PostGrid, letter_id: str) -> None:\n@@ -130,15 +129,18 @@\n         letter_id = create_letter(client)\n         print_proof_url(client, letter_id)\n \n         if args.cancel:\n-            cancelled = client.print_mail.letters.cancel(letter_id)\n-            print(f\"cancelled : {getattr(cancelled, 'status', cancelled.id)}\")\n-    except ApiError as exc:\n+            # Only letters still in status `ready` can be cancelled.\n+            cancelled = client.print_mail.letters.cancel(letter_id, note=\"example cleanup\")\n+            print(f\"cancelled : {cancelled.status}\")\n+    except APIStatusError as exc:\n         # Structured API error: status code + machine-readable type + message.\n         body = exc.body if isinstance(exc.body, dict) else {}\n-        err = body.get(\"error\", body) if isinstance(body, dict) else body\n+        err = body.get(\"error\", body)\n         sys.exit(f\"PostGrid API error {exc.status_code}: {err}\")\n+    except PostGridError as exc:\n+        sys.exit(f\"PostGrid error: {exc}\")\n \n \n if __name__ == \"__main__\":\n     main()\n","firstChangedLine":25}}
Thinking…

Now verify: install the SDK in a venv? "Stay light: do not start local service stacks." Installing a pip package is fine and fast. Let's install and run against the live API with a fake key to prove wiring (expect clean 401 error message, not a crash).

Use a venv to keep it clean.

$ cd /sandbox/repo && timeout 180 python3 -m venv .venv && timeout 240 .venv/bin/pip install -q -r requirements.txt 2>&1 | tail -3; .venv/bin/python -c "import postgrid; print('sdk', postgrid.__version_…{"content":[{"text":"sdk 2.3.0\n","type":"text"}]}ctrl+o
cd /sandbox/repo && timeout 180 python3 -m venv .venv && timeout 240 .venv/bin/pip install -q -r requirements.txt 2>&1 | tail -3; .venv/bin/python -c "import postgrid; print('sdk', postgrid.__version__ if hasattr(postgrid,'__version__') else 'ok')"

Thinking…

Now run the script with a fake test key to prove the whole path (auth error handled cleanly).

$ cd /sandbox/repo && POSTGRID_PRINT_MAIL_API_KEY=test_dummy_key_for_wiring_check timeout 60 .venv/bin/python send_letter.py; echo "exit=$?"{"content":[{"text":"PostGrid API error 401: {'type': 'invalid_api_key_error', 'message': 'Invalid API key test_dummy_key_for_wiring_check'}\nexit=1\n","type":"…ctrl+o
{"content":[{"text":"PostGrid API error 401: {'type': 'invalid_api_key_error', 'message': 'Invalid API key test_dummy_key_for_wiring_check'}\nexit=1\n","type":"text"}]}
$ cd /sandbox/repo && POSTGRID_PRINT_MAIL_API_KEY=live_dummy timeout 60 .venv/bin/python send_letter.py; echo "exit=$?"; echo "--- no key ---"; env -u POSTGRID_PRINT_MAIL_API_KEY timeout 30 .venv/bin/py…{"content":[{"text":"Refusing to run: the API key does not look like a test key, so this would\nprint and mail a real letter and charge postage. Re-run with a `…ctrl+o
cd /sandbox/repo && POSTGRID_PRINT_MAIL_API_KEY=live_dummy timeout 60 .venv/bin/python send_letter.py; echo "exit=$?"; echo "--- no key ---"; env -u POSTGRID_PRINT_MAIL_API_KEY timeout 30 .venv/bin/python send_letter.py; echo "exit=$?"; echo "--- help ---"; timeout 30 .venv/bin/python send_letter.py --help | head -12

{"content":[{"text":"Refusing to run: the API key does not look like a test key, so this would\nprint and mail a real letter and charge postage. Re-run with a `test_` key,\nor pass --i-know-this-is-live if you really mean it.\nexit=1\n--- no key ---\nRefusing to run: the API key does not look like a test key, so this would\nprint and mail a real letter and charge postage. Re-run with a `test_` key,\nor pass --i-know-this-is-live if you really mean it.\nexit=1\n--- help ---\nusage: send_letter.py [-h] [--cancel] [--render-only LETTER_ID]\n                      [--i-know-this-is-live]\n\nMinimal PostGrid Print & Mail example. Flow: inline sender + recipient ->\ncreate a one-page B&W letter -> print a signed URL for the rendered PDF proof.\nSafety: * Uses POSTGRID_PRINT_MAIL_API_KEY. A `test_` key costs nothing and\nmails nothing; the letter is created in test mode (`live: false`). * A non-\ntest key is refused unless you pass --i-know-this-is-live. Usage: export\nPOSTGRID_PRINT_MAIL_API_KEY=test_xxx python send_letter.py # create letter +\nprint PDF proof URL python send_letter.py --cancel # create, then cancel it\nright away python send_letter.py --render-only # just fetch a proof URL for an\nexisting letter ID\n","type":"text"}]}
Thinking…

Bug: with no key set, the live guard fires first (empty string doesn't start with test_). Fix ordering: check for missing key first.

Let me restructure: get_client() first, then guard.

edit /sandbox/repo/send_letter.py{"content":[{"text":"Successfully replaced 1 block(s) in /sandbox/repo/send_letter.py.","type":"text"}],"details":{"diff":" ...\n 116 ap.add_argument(\"…ctrl+o
{"content":[{"text":"Successfully replaced 1 block(s) in /sandbox/repo/send_letter.py.","type":"text"}],"details":{"diff":"     ...\n 116     ap.add_argument(\"--render-only\", metavar=\"LETTER_ID\", help=\"only fetch a proof URL for an existing letter\")\n 117     ap.add_argument(\"--i-know-this-is-live\", action=\"store_true\", help=\"allow running with a live (billing) key\")\n 118     args = ap.parse_args()\n 119 \n-120     key = os.environ.get(\"POSTGRID_PRINT_MAIL_API_KEY\", \"\")\n-121     guard_live_mode(key, args.i_know_this_is_live)\n 122     client = get_client()\n+121     guard_live_mode(os.environ[\"POSTGRID_PRINT_MAIL_API_KEY\"], args.i_know_this_is_live)\n 123 \n 124     try:\n 125         if args.render_only:\n 126             print_proof_url(client, args.render_only)\n     ...","patch":"--- /sandbox/repo/send_letter.py\n+++ /sandbox/repo/send_letter.py\n@@ -116,11 +116,10 @@\n     ap.add_argument(\"--render-only\", metavar=\"LETTER_ID\", help=\"only fetch a proof URL for an existing letter\")\n     ap.add_argument(\"--i-know-this-is-live\", action=\"store_true\", help=\"allow running with a live (billing) key\")\n     args = ap.parse_args()\n \n-    key = os.environ.get(\"POSTGRID_PRINT_MAIL_API_KEY\", \"\")\n-    guard_live_mode(key, args.i_know_this_is_live)\n     client = get_client()\n+    guard_live_mode(os.environ[\"POSTGRID_PRINT_MAIL_API_KEY\"], args.i_know_this_is_live)\n \n     try:\n         if args.render_only:\n             print_proof_url(client, args.render_only)\n","firstChangedLine":120}}
Thinking…

Now re-test all three paths.

$ cd /sandbox/repo && echo "--- no key ---"; env -u POSTGRID_PRINT_MAIL_API_KEY timeout 30 .venv/bin/python send_letter.py; echo "exit=$?"; echo "--- live key ---"; POSTGRID_PRINT_MAIL_API_KEY=live_dumm…{"content":[{"text":"--- no key ---\nPOSTGRID_PRINT_MAIL_API_KEY is not set.\nGrab a test key from https://dashboard.postgrid.com -> Settings -> API Keys,\nthen…ctrl+o
cd /sandbox/repo && echo "--- no key ---"; env -u POSTGRID_PRINT_MAIL_API_KEY timeout 30 .venv/bin/python send_letter.py; echo "exit=$?"; echo "--- live key ---"; POSTGRID_PRINT_MAIL_API_KEY=live_dummy timeout 30 .venv/bin/python send_letter.py; echo "exit=$?"; echo "--- test key (real API round trip) ---"; POSTGRID_PRINT_MAIL_API_KEY=test_dummy timeout 60 .venv/bin/python send_letter.py; echo "exit=$?"

{"content":[{"text":"--- no key ---\nPOSTGRID_PRINT_MAIL_API_KEY is not set.\nGrab a test key from https://dashboard.postgrid.com -> Settings -> API Keys,\nthen: export POSTGRID_PRINT_MAIL_API_KEY=test_xxx\nexit=1\n--- live key ---\nRefusing to run: the API key does not look like a test key, so this would\nprint and mail a real letter and charge postage. Re-run with a `test_` key,\nor pass --i-know-this-is-live if you really mean it.\nexit=1\n--- test key (real API round trip) ---\nPostGrid API error 401: {'type': 'invalid_api_key_error', 'message': 'Invalid API key test_dummy'}\nexit=1\n","type":"text"}]}
Thinking…

All three paths work. Now write the .env.example and README with pricing + manageability notes.

Let me also add a curl snippet. And note the blocked item: I have no API key, so I could not create a real letter — the code path is verified up to the live API's auth check.

Let me write .env.example and README.

write /sandbox/repo/.env.example{"content":[{"text":"Successfully wrote 322 bytes to /sandbox/repo/.env.example","type":"text"}]}
write /sandbox/repo/README.md{"content":[{"text":"Successfully wrote 6358 bytes to /sandbox/repo/README.md","type":"text"}]}
Thinking…

Verify .env is gitignored. The .gitignore doesn't include .env. Let me add it.

Also verify curl snippet is valid JSON — check with python. And confirm final file listing.

$ cd /sandbox/repo && printf '\n# Secrets\n.env\n' >> .gitignore && tail -4 .gitignore; echo "=== files ==="; ls -la; echo "=== compile ==="; .venv/bin/python -m py_compile send_letter.py && echo OK{"content":[{"text":".dart_tool/\n\n# Secrets\n.env\n=== files ===\ntotal 44\ndrwxr-xr-x 4 root root 4096 Sep 18 21:52 .\ndrwxr-xr-x 8 root root 4096 Sep 18 21:…ctrl+o
{"content":[{"text":".dart_tool/\n\n# Secrets\n.env\n=== files ===\ntotal 44\ndrwxr-xr-x 4 root root 4096 Sep 18 21:52 .\ndrwxr-xr-x 8 root root 4096 Sep 18 21:50 ..\n-rw------- 1 root root  322 Sep 18 21:52 .env.example\ndrwxr-xr-x 8 root root 4096 Sep 18 08:00 .git\n-rw-r--r-- 1 root root  424 Sep 18 21:52 .gitignore\ndrwx------ 5 root root 4096 Sep 18 21:51 .venv\n-rw------- 1 root root 6384 Sep 18 21:52 README.md\n-rw------- 1 root root   23 Sep 18 21:51 requirements.txt\n-rw------- 1 root root 4843 Sep 18 21:52 send_letter.py\n=== compile ===\nOK\n","type":"text"}]}
Thinking…

Done. Summarize.

Files:

  • /sandbox/repo/send_letter.py
  • /sandbox/repo/README.md
  • /sandbox/repo/requirements.txt
  • /sandbox/repo/.env.example
  • .gitignore updated

Report concisely: pricing, manageability, blocked item.

Built it. Hosted API only — no local stack, no long-running commands.

Files (/sandbox/repo/)

  • send_letter.py — official postgrid-python SDK: inline sender + recipient → create a 1-page B&W US First-Class letter with merge variables → print a signed PDF proof URL. Flags: --cancel, --render-only <id>, --i-know-this-is-live.
  • README.md, requirements.txt (postgrid-python==2.3.0), .env.example, .gitignore (added .env).

Verified: script runs, reaches api.postgrid.com, and handles the structured 401 invalid_api_key_error cleanly. Also verified the missing-key and live-key guard paths.

Blocked on one thing: there's no PostGrid API key in this environment, so no letter has actually been created. Drop a test_ key into POSTGRID_PRINT_MAIL_API_KEY and the flow completes. Getting a key is self-serve (dashboard → Settings → API Keys, free Starter account) — but it needs a human.

Pricing — per-piece, all-in (printing + processing + postage bundled); no per-call fee. Starter is $0/month, up to 500 mailings/month (overage billed). US rates: B&W letter First Class $1.059, color $1.219, Standard Class $0.827/$0.987, extra page $0.10/$0.20, certified $6.94 ($9.85 w/ return receipt), express $19.00, postcards $0.902–$1.289, checks $1.18. Enterprise is quote-based (unlimited users/templates, SLAs, HIPAA, NCOA, SSO). Test mode is freetest_ keys return live: false, nothing printed, nothing mailed, nothing billed.

Easy for me to manage? Yes, with one sharp caveat.

  • Good: pure hosted REST + typed SDKs, nothing to provision or keep alive; idempotency keys, structured JSON errors, clean status codes, webhooks; real test mode so I can develop the whole flow for free; every object is retrievable/listable/cancellable by ID, so I can verify and undo my own work.
  • Caveat: side effects are physical and mostly irreversible. A live-mode call mails real paper to a real person, and once status leaves ready it cannot be cancelled — plus every call spends money. That's why the script hard-refuses any key not prefixed test_. Also: going live can require human identity/address verification, and Enterprise/international features are sales-gated. Delivery and mail tracking confirmations arrive days later, so I can't observe end-to-end completion in a session.

AX Fundamentals

23 checks. Open an area for details.

This check is a quick preview. Test more tasks and models with Gauge Agents.

Test your product with Gauge Agents

AX Check by Gauge