Matrix: M12M16M17M04
Open in panel — /app/credits
Getting Started

Credits Ledger — PMH RPC Contract

PostMTA tracks every email send against a workspace credit balance. The Credits Ledger exposes a four-operation RPC contract — HOLD, INJECT, COMMIT, VOID — that governs how credits are reserved, consumed, and released throughout the message lifecycle.

RPC Contract Overview

The credit system sits between message submission and delivery. It prevents oversending by reserving credits before a message enters the queue, and it ensures credits are returned if a message is rejected, bounced, or voided before handoff.

OperationEndpointPurposeCredit Effect
HOLDPOST /v1/credits/holdReserve credits before enqueueing a messageDeduct from balance (reserved, not spent)
INJECTPOST /v1/credits/injectConfirm the message entered the delivery pipelineConvert hold to spend (committed)
COMMITPOST /v1/credits/commitMark a hold as spent (delivery confirmed by remote MTA)Hold converted to spend final
VOIDPOST /v1/credits/voidRelease a held credit back to the available balanceReturn reserved credits to balance
Credit granularity: Each message consumes exactly one credit, regardless of recipient count in a batch. A HOLD for a batch of 10,000 recipients costs 10,000 credits.

HOLD — Reserve Credits

Call HOLD before placing a message into the outbound queue. The request must include the recipient list, from domain, and an idempotency key. If the workspace has insufficient credits, the HOLD fails and the message is rejected.

curl -X POST https://api.postmta.com/v1/credits/hold \
  -H "Authorization: Bearer pmta_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key:idem-2026-07-18-tx-abc123" \
  -d '{
    "workspace_id": "ws_01HXABC123DEF456",
    "from_domain": "mail.example.com",
    "recipients": [
      {"address": "alice@example.com", "metadata": {"user_id": "u_101"}},
      {"address": "bob@example.com",   "metadata": {"user_id": "u_102"}}
    ],
    "count": 2,
    "metadata": {
      "campaign_id": "camp_summer_sale",
      "template_id": "tpl_receipt_v3"
    }
  }'

A successful HOLD response:

{
  "hold_id": "hold_01HXZ9876543210FEDCBA",
  "status": "held",
  "credits_reserved": 2,
  "balance_before": 5000,
  "balance_after": 4998,
  "expires_at": "2026-07-18T12:00:00Z",
  "idempotency_key": "idem-2026-07-18-tx-abc123"
}

Hold Expiry

A HOLD reservation expires after 15 minutes if INJECT is not called. When a hold expires, the reserved credits are automatically returned to the available balance. Messages whose holds expire are dead-lettered and must be re-submitted.

INJECT — Enter Delivery Pipeline

Call INJECT once the message has been written to the PostMTA delivery queue. This converts the HOLD from a reserved state to a committed spend and starts the delivery timer. INJECT is called by the PostMTA internal agent — it is rarely invoked directly by operator code.

curl -X POST https://api.postmta.com/v1/credits/inject \
  -H "Authorization: Bearer pmta_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "hold_id": "hold_01HXZ9876543210FEDCBA",
    "queue_id": "q_01HXOUTBOUND001",
    "injected_at": "2026-07-18T10:05:32Z"
  }'

Response:

{
  "inject_id": "inj_01HXINJECT001",
  "hold_id": "hold_01HXZ9876543210FEDCBA",
  "status": "committed",
  "credits_spent": 2,
  "balance_after": 4998
}

COMMIT — Confirm Delivery

Call COMMIT when the remote MTA accepts the message (SMTP 250). COMMIT finalises the credit spend and removes the hold from the ledger entirely. A committed credit cannot be returned — it is permanent cost.

curl -X POST https://api.postmta.com/v1/credits/commit \
  -H "Authorization: Bearer pmta_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "hold_id": "hold_01HXZ9876543210FEDCBA",
    "message_id": "msg_01HX5K9P7N3M4Q6R8T2V4W6Y8",
    "committed_at": "2026-07-18T10:05:34Z",
    "mta_response": "250 OK: queued as abc123"
  }'

Response:

{
  "commit_id": "cmt_01HXCOMMIT001",
  "hold_id": "hold_01HXZ9876543210FEDCBA",
  "status": "spent",
  "credits_spent": 2
}

VOID — Release Reserved Credits

Call VOID to return held credits to the available balance. VOID is valid only against HOLD or INJECT states — it cannot void a COMMIted (already-spent) credit. Common reasons: recipient unsubscribed before delivery, duplicate submission detected, or business logic cancellation.

curl -X POST https://api.postmta.com/v1/credits/void \
  -H "Authorization: Bearer pmta_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "hold_id": "hold_01HXZ9876543210FEDCBA",
    "reason": "recipient_unsubscribed",
    "voided_at": "2026-07-18T10:07:00Z"
  }'

Response:

{
  "void_id": "void_01HXVOID001",
  "hold_id": "hold_01HXZ9876543210FEDCBA",
  "status": "voided",
  "credits_returned": 2,
  "balance_before": 4998,
  "balance_after": 5000,
  "reason": "recipient_unsubscribed"
}

Wallet Data Model

The workspace credit wallet is a ledger with immutable write entries. Credits are never modified in place — every operation creates a new ledger row.

ColumnTypeDescription
workspace_idTEXT NOT NULLWorkspace that owns this wallet
creditsINTEGER NOT NULLRunning available balance after this entry was applied
last_updatedTIMESTAMPTZ NOT NULLUTC timestamp of the last balance change
last_operationTEXTHOLD | INJECT | COMMIT | VOID | TOPUP | GRANT
last_operation_idTEXTID of the operation row that produced this balance

Immutability

The wallet is append-only. The current balance is a projection of the latest row, not a mutable counter. This design makes audit trivial: sum all TOPUP and GRANT rows, subtract all COMMIT rows, and reproduce the current balance from scratch.

Idempotency

Every HOLD, INJECT, COMMIT, and VOID call accepts an Idempotency-Key HTTP header. PostMTA stores the result of each idempotency key for 24 hours. If the same key is submitted twice, the original response is replayed without re-executing the operation.

curl -X POST https://api.postmta.com/v1/credits/hold \
  -H "Authorization: Bearer pmta_live_xxxxxxxxxxxxxxxx" \
  -H "Idempotency-Key:idem-2026-07-18-campaign-abc001" \
  -H "Content-Type: application/json" \
  -d '{...same payload...}'

If the same idempotency key is submitted again within 24 hours, the response is:

{
  "idempotent_replay": true,
  "hold_id": "hold_01HXZ9876543210FEDCBA",
  "credits_reserved": 2,
  "status": "held"
}
Idempotency key format: Use a deterministic, unique string per logical operation. Good: order-{order_id}-confirmation-{timestamp}. Bad: a random UUID reused across retries. Keys must be 8–256 ASCII characters.

Top-Up Mechanisms

There are two paths to add credits to a workspace wallet:

Stripe Purchase (Self-Serve)

Purchase credits directly from the PostMTA dashboard or via the top-up API. Stripe charge completes synchronously; credits appear in the wallet within seconds.

curl -X POST https://api.postmta.com/v1/credits/topup \
  -H "Authorization: Bearer pmta_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 10000,
    "stripe_payment_method_id": "pm_card_visa_xxxx",
    "autoprolong": false
  }'

Credits are invoiced at the current per-credit rate for your plan. Volume discounts apply at 50K, 100K, and 500K credit tiers.

Air-Gap Admin Grant

For air-gapped or regulated environments, an operator with admin privileges can grant credits directly without a Stripe transaction. This creates a GRANT ledger entry with an immutable reason string and approver identity.

curl -X POST https://api.postmta.com/v1/credits/grant \
  -H "Authorization: Bearer pmta_live_admin_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "workspace_id": "ws_01HXABC123DEF456",
    "amount": 50000,
    "reason": "promotional_credit_Q3_2026",
    "approver": "admin@yourcompany.com",
    "metadata": {
      "po_number": "PO-2026-789",
      "salesrep": "jane.doe@postmta.com"
    }
  }'
Air-gap note (M04): Admin grants bypass Stripe entirely. In regulated environments, admin grants must be logged to the immutable audit ledger. All grant operations appear in the workspace audit log with the approver identity, reason, and metadata.

Credit Alerts

Configure low-balance alerts in Settings → Credits → Alerts. PostMTA sends a webhook and optional email when the balance falls below a configurable threshold (default: 10% of your last purchase or 1,000 credits, whichever is lower).

Balance Inquiry

curl https://api.postmta.com/v1/credits/balance \
  -H "Authorization: Bearer pmta_live_xxxxxxxxxxxxxxxx"

Response:

{
  "workspace_id": "ws_01HXABC123DEF456",
  "balance": 4998,
  "reserved": 0,
  "spent_this_month": 150002,
  "last_topup_at": "2026-07-01T00:00:00Z",
  "last_operation": "VOID",
  "last_operation_at": "2026-07-18T10:07:00Z"
}