Test Mode · Test payments only · No physical mail is sent · Synthetic letters only
AgentPostage

Developer documentation / V1

Text in.
A letter to review.

Prepare a US letter through the API. Have a human review its PDF and approve the quote. Then request a test submission.

This is staging. No physical mail is sent. All payments use test mode and all balances are test credit. Local simulated results and provider_test_held jobs are not evidence of mailing, receipt, or delivery.

The HTTP API is under /v1 on the same origin as this site. Responses are JSON resource objects; errors use { "error": { "code", "message", "next_action"? } }. Monetary values are integer USD cents.

1. Create an account and an agent key

Create an account in the dashboard, enter the numeric code from your verification email, then sign in. Add test credit using test card details, and create a named key with a cumulative spending cap. Copy the key when it appears; it is returned once. Store it in your agent's secret storage.

An agent authenticates with Authorization: Bearer <key>. The browser uses a session cookie. An agent key cannot approve a letter, manage keys, or buy credit. Do not give your agent your browser session.

export BASE='https://staging.agentpostage.com'
# Supply AGENTPOSTAGE_KEY through your secret manager or environment.

curl --fail-with-body "$BASE/v1/balance" \
  -H "Authorization: Bearer $AGENTPOSTAGE_KEY"

Example balance response:

{
  "available_cents": 2500,
  "reserved_cents": 0,
  "currency": "USD"
}

Sign-up and sign-in use POST /auth/sign-up/email with {name,email,password} and POST /auth/sign-in/email with {email,password}. Sign-out uses POST /auth/sign-out. Sign-up returns no session. Verification uses POST /auth/email-otp/verify-email with {email,otp}. Request a new code with POST /auth/email-otp/send-verification-otp and {email,type:"email-verification"}. Never log verification codes or give them to agents. Enter the emailed code in the dashboard, then sign in. Browser requests include credentials. Browser mutations must come from the application origin; agent requests use a bearer key.

2. Prepare an immutable letter

Submit both addresses and the letter text. Use a unique Idempotency-Key for this preparation. Reuse it with the same body if you need to retry.

curl --fail-with-body "$BASE/v1/letters" \
  -H "Authorization: Bearer $AGENTPOSTAGE_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: example-prepare-001' \
  --data '{
    "sender": {
      "name": "Example Sender",
      "address_line1": "100 Example Avenue",
      "city": "Austin",
      "state": "TX",
      "postal_code": "78701",
      "country": "US"
    },
    "recipient": {
      "name": "Example Recipient",
      "address_line1": "200 Sample Street",
      "address_line2": "Suite 10",
      "city": "Austin",
      "state": "TX",
      "postal_code": "78702",
      "country": "US"
    },
    "text": "Hello,\n\nThis is a sample letter for a staging test.\n\nThank you.",
    "service": "first_class"
  }'

Illustrative response (IDs, timestamps, and quote values vary):

{
  "id": "00000000-0000-4000-8000-000000000001",
  "status": "awaiting_approval",
  "mode": "letterstream_test",
  "service": "first_class",
  "created_at": "2026-10-01T12:00:00Z",
  "quote": {
    "amount_cents": 349,
    "currency": "USD",
    "expires_at": "2026-10-01T13:00:00Z"
  },
  "page_count": 1,
  "preview_url": "/v1/letters/00000000-0000-4000-8000-000000000001/pdf",
  "approval_url": "/app/?letter=00000000-0000-4000-8000-000000000001",
  "next_action": "Review the PDF and approve the quote in the dashboard."
}

Save the returned id, approval_url, and quote. These sample addresses are illustrative; no mailing-address validation is implied. V1 accepts first_class or certified_return_receipt. Both run in Test Mode. The server records each letter’s mode: simulation for a local simulation, or letterstream_test for a LetterStream job held for authorization. The prepare request does not select or override this mode.

3. Ask a human to review and approve

Give the human the returned approval_url on the staging origin. They sign in, open the authenticated PDF, review every page and both addresses, and approve the exact quoted amount. Approval does not enqueue a send.

The preview is the letter content, not the complete provider print package. In letterstream_test mode, submitting sends the actual synthetic PDF and addresses to LetterStream. With coversheet=Y, the provider adds an address coversheet that is not shown in this preview. The human must understand this before approval. The job remains held for authorization; AgentPostage does not authorize physical dispatch. Local simulation has no provider submission.
Keep this step human. The agent must stop at awaiting_approval. It must not obtain a browser cookie, call the approval endpoint as the customer, or treat its own review as human approval.

The dashboard performs the following request using the human's browser session. This describes the browser operation, not an agent curl step:

POST /v1/letters/{id}/approve
Content-Type: application/json
Origin: <your staging origin>
Cookie: <managed browser session>

{ "confirmed": true, "amount_cents": 349 }

A successful response is the letter resource with status: "approved". Always submit the actual quoted amount. An expired quote requires a new preparation and review. Content is immutable: changing wording, addresses, or service means creating and approving a new letter.

PDF downloads require account authorization and are served privately with no-store. Preview links do not make the document public.

4. Send after approval, then read status

# Replace with the actual id returned by preparation.
LETTER_ID='00000000-0000-4000-8000-000000000001'

# Inspect status before requesting a send.
curl --fail-with-body "$BASE/v1/letters/$LETTER_ID" \
  -H "Authorization: Bearer $AGENTPOSTAGE_KEY"

# Only after status is approved:
curl --fail-with-body "$BASE/v1/letters/$LETTER_ID/send" \
  -H "Authorization: Bearer $AGENTPOSTAGE_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: example-send-001' \
  --data '{}'

# Read the current resource and status events.
curl --fail-with-body "$BASE/v1/letters/$LETTER_ID" \
  -H "Authorization: Bearer $AGENTPOSTAGE_KEY"

A successful send returns the letter resource, normally with status: "queued"; a retry can return its later status. The server reserves funds and enqueues once. Repeating the send does not create a second letter.

Both test modes begin with awaiting_approval → approved → queued → submitting. A local simulation ends in simulated. A confirmed letterstream_test submission ends in provider_test_held: the provider has the test job, held for authorization. Existing local simulation records remain valid. Neither terminal status means mailed or delivered.

LetterStream submission requires a verified Test Mode account with zero provider funds. The integration always uses preauth=1 for submission and never calls the provider’s authorization operation. A failed check stops submission. AgentPostage test credit is separate from provider funds.

Read the test receipt

The letter’s evidence contains only allowlisted receipt fields. The dashboard shows provider-reported cost separately from quote.amount_cents; neither is a universal or live mailing price. Example receipt, with fictitious identifiers and an illustrative cost:

{
  "status": "provider_test_held",
  "mode": "letterstream_test",
  "evidence": {
    "provider": "letterstream",
    "provider_cost_cents": 119,
    "provider_job_id": "example-job",
    "provider_document_id": "example-document",
    "provider_batch_id": "example-batch",
    "test_mode": true,
    "held": true,
    "physical_mail_sent": false
  }
}

This is a partial example, not a submitted job or delivery receipt. Read the actual cost and IDs from the response. Missing receipt fields must not be invented. In submission_unknown, evidence can contain only provider, provider_batch_id, and provider_document_id. These are request identifiers for manual review; they do not confirm provider acceptance or a held job. The provider-added coversheet can affect its reported page count and cost.

A human can cancel in the dashboard before the sender claims submission. Agent keys cannot cancel letters. Cancellation releases a queued reservation once; a claimed submission cannot be cancelled. If status becomes submission_unknown, funds stay reserved while the result is unresolved. Do not send a replacement or retry submission blindly.

Endpoint reference

“Both” means a customer session or a valid agent key. Every account resource is scoped to the authenticated account.

Method & pathAccessPurpose
GET /v1/meHumanAccount, profile, balance, environment.
GET /v1/balanceBothAvailable and reserved USD cents.
GET /v1/keysHumanNamed keys; no raw secrets.
POST /v1/keysHuman{name, spend_limit_cents}; returns key once.
DELETE /v1/keys/{id}HumanRevoke an agent key.
POST /v1/lettersBothPrepare; requires Idempotency-Key.
GET /v1/lettersBothRecent letters, with a bounded page size and cursor.
GET /v1/letters/{id}BothLetter, quote, status, events, available evidence metadata.
GET /v1/letters/{id}/pdfBothImmutable PDF; authenticated, private, no-store.
POST /v1/letters/{id}/approveHuman{confirmed:true, amount_cents}; does not send.
POST /v1/letters/{id}/sendBothReserve and queue once; requires Idempotency-Key.
POST /v1/letters/{id}/cancelHumanCancel before submission is claimed; release reservations once.
POST /v1/billing/checkoutHuman{amount_cents}; returns test Checkout url.
GET /healthPublicMinimal health and environment mode.

Test credit and spending limits

Top-up amounts are 1000, 2500, 5000, or 10000 cents. Checkout returns {url, checkout_id, amount_cents, currency, environment, test_mode}. The server credits the account only after a verified payment webhook. A checkout return URL is not evidence of payment. There is no automatic top-up. If checkout is unavailable, the endpoint returns billing_unavailable; no payment is started.

A key's spending cap is cumulative. Approval does not override its remaining cap or the account's available balance. Revoke keys you no longer use in the dashboard.

Machine-readable references

Use OpenAPI JSON for the HTTP contract and llms.txt for agent instructions. The HTTP flow above is authoritative. The stateless POST /mcp interface exposes prepare_letter, get_letter, get_balance, and send_letter; use an MCP client for its protocol handshake. It does not remove human approval.

Errors and safe retries

{
  "error": {
    "code": "customer_required",
    "message": "This action requires a signed-in customer.",
    "next_action": "Open the approval link in the dashboard."
  }
}

Example only; next_action is optional. Show the returned message to the user instead of assuming every error is retryable.

  • 400: Correct invalid fields, unsupported characters, page limits, or the missing idempotency key.
  • 401 / 403: Check authentication and permissions. Human-only actions need a signed-in customer.
  • 402: Available test credit is insufficient. Ask the human to add test credit in the dashboard.
  • 404: Check the letter ID and account. Resources belonging to another account are not accessible.
  • 409: Resolve the conflict first. Reusing a preparation key with different content is an error; use a new key for a new letter.
  • Network failure / 5xx: The request may have succeeded. Read the resource if its ID is known. Retry preparation or sending with the original idempotency key and identical body.

Idempotency keys contain 8–128 ASCII letters, digits, dots, colons, underscores, or hyphens. Keep one key for each logical prepare request and one for its send request. Save them before making the call. Do not generate a fresh key just because the response was lost.

Limits and sample data

LetterStream limits single-file submissions to 50 batches per day. Each provider-mode letter uses one single-file batch; this provider ceiling is separate from your account balance and agent spending cap.

V1 accepts US addresses, printable ASCII text and line breaks, up to 24,000 text characters and 10 PDF pages. Address fields must be single lines. In letterstream_test, address fields must also exclude colons and pipes. Names are limited to 80 characters, address lines to 100, and cities to 60. Use a two-letter US state code and a ZIP or ZIP+4. There are no attachments, arbitrary URL imports, international letters, or bulk campaigns.

Quotes are provisional test USD credit and expire at quote.expires_at. Current test-credit values are 349 cents for First-Class or 1699 cents for Certified + electronic return receipt, plus 25 cents per additional preview page. They are separate from any provider-reported cost. The returned quote is authoritative for that letter; example prices are not live offers. Do not infer legal effectiveness, a mailing date, or guaranteed delivery from a service name.

Use fictitious letter data in staging. Treat PDFs, addresses, account sessions, and API keys as sensitive. Never embed keys in public browser code or publish preview contents. Staging is not a place for confidential correspondence.