{
  "openapi": "3.1.0",
  "info": {
    "title": "unopost x402 API",
    "version": "v1",
    "description": "Post a printed letter to any address in Australia and pay per letter in USDC over x402, with no account or API key. unopost prints the document on standard 80gsm A4 paper as uploaded (colour stays colour) and posts it with tracking by Express Post or regular post, the same business day when paid by 2:30pm Sydney time. Flow: POST /api/x402 with the file name and addresses, PUT the raw file bytes to uploadUrl within 15 minutes (processing starts on its own once the upload lands), poll GET /api/x402/{jobId} until status is ready_to_send, POST /api/x402/{jobId}/send without PAYMENT-SIGNATURE to receive a 402 challenge in the PAYMENT-REQUIRED header, retry the same POST with PAYMENT-SIGNATURE, then poll GET /api/x402/{jobId} until status is posted, delivered or failed. Prices: 20 USDC for an Express Post letter or 10 USDC for a regular post letter, up to 12 pages, then 0.50 USDC for each extra page. Business bulk pricing starts from 2 USDC a letter: email support@unopost.com.au for a bizKey and send it with each letter. Tracking, double-sided printing, folding and signature on delivery are free. Payment is the x402 exact scheme with USDC on Base mainnet (eip155:8453); gas is covered. Quotes last 7 days. If a letter can't be posted, the full amount is refunded in USDC to the wallet that paid. Documents are deleted once the letter is posted."
  },
  "externalDocs": {
    "description": "Guide with examples",
    "url": "https://unopost.com.au/x402"
  },
  "servers": [
    {
      "url": "https://unopost.com.au",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "x402",
      "description": "Post a letter and pay for it in USDC over x402."
    }
  ],
  "paths": {
    "/api/x402": {
      "get": {
        "tags": ["x402"],
        "operationId": "getX402Descriptor",
        "summary": "Describe the x402 API",
        "description": "Returns a small machine-readable descriptor pointing to the create, status, send and OpenAPI endpoints, with the expected flow.",
        "responses": {
          "200": {
            "description": "Machine-readable x402 API descriptor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/X402Descriptor"
                },
                "examples": {
                  "default": {
                    "value": {
                      "name": "unopost x402 API",
                      "version": "v1",
                      "description": "Post a letter anywhere in Australia and pay per letter in USDC over x402.",
                      "openapi": "/api/x402/openapi.json",
                      "create": {
                        "method": "POST",
                        "path": "/api/x402"
                      },
                      "status": {
                        "method": "GET",
                        "path": "/api/x402/{jobId}"
                      },
                      "send": {
                        "method": "POST",
                        "path": "/api/x402/{jobId}/send"
                      },
                      "flow": [
                        "POST /api/x402",
                        "PUT uploadUrl",
                        "GET /api/x402/{jobId} until status=ready_to_send",
                        "POST /api/x402/{jobId}/send",
                        "retry same POST with PAYMENT-SIGNATURE after 402",
                        "GET /api/x402/{jobId} until status=posted, delivered or failed"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": ["x402"],
        "operationId": "createX402Letter",
        "summary": "Create a letter",
        "description": "Creates a letter and returns a signed upload URL, valid for 15 minutes. PUT the raw file bytes to uploadUrl (up to 50 MB and 50 pages). Processing starts automatically once the upload lands; there is no start call. Then poll statusUrl until the letter is ready_to_send.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/X402CreateRequest"
              },
              "examples": {
                "simple": {
                  "summary": "Express Post, posted flat and single-sided",
                  "value": {
                    "fileName": "letter.pdf",
                    "recipient": {
                      "name": "Ruth Mitchell",
                      "line1": "7 High Street",
                      "suburb": "Echuca",
                      "state": "VIC",
                      "postcode": "3564"
                    },
                    "sender": {
                      "name": "Sam Nguyen",
                      "line1": "14 Carnarvon Street",
                      "suburb": "Broome",
                      "state": "WA",
                      "postcode": "6725"
                    }
                  }
                },
                "withOptions": {
                  "summary": "Regular post, double-sided and folded",
                  "value": {
                    "fileName": "notice.docx",
                    "recipient": {
                      "name": "Ruth Mitchell",
                      "company": "Echuca Rentals",
                      "line1": "PO Box 123",
                      "suburb": "Echuca",
                      "state": "VIC",
                      "postcode": "3564"
                    },
                    "sender": {
                      "name": "Sam Nguyen",
                      "line1": "14 Carnarvon Street",
                      "suburb": "Broome",
                      "state": "WA",
                      "postcode": "6725"
                    },
                    "service": "regular",
                    "duplex": true,
                    "fold": true
                  }
                },
                "signature": {
                  "summary": "Express Post with signature on delivery",
                  "value": {
                    "fileName": "contract.pdf",
                    "recipient": {
                      "name": "Leo Tran",
                      "line1": "52 Smith Street",
                      "suburb": "Darwin",
                      "state": "NT",
                      "postcode": "0800"
                    },
                    "sender": {
                      "name": "Sam Nguyen",
                      "line1": "14 Carnarvon Street",
                      "suburb": "Broome",
                      "state": "WA",
                      "postcode": "6725"
                    },
                    "service": "express",
                    "signature": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Letter created, with a signed upload URL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/X402CreateResponse"
                },
                "examples": {
                  "default": {
                    "value": {
                      "jobId": "019d7a7f-a183-752d-9b3b-11511179033b",
                      "status": "awaiting_upload",
                      "uploadUrl": "https://unopost.com.au/uploads/019d7a7f-a183-752d-9b3b-11511179033b/letter.pdf?Expires=...&Signature=...&Key-Pair-Id=...",
                      "statusUrl": "/api/x402/019d7a7f-a183-752d-9b3b-11511179033b",
                      "sendUrl": "/api/x402/019d7a7f-a183-752d-9b3b-11511179033b/send"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid input, for example a missing field, an unsupported file type, an address line over 40 characters, a postcode that isn't in the state, signature with regular post, or an unknown bizKey.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "error": "recipient postcode 3000 is not in NSW"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/x402/openapi.json": {
      "get": {
        "tags": ["x402"],
        "operationId": "getX402OpenApi",
        "summary": "Get this OpenAPI document",
        "responses": {
          "200": {
            "description": "OpenAPI JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "This OpenAPI document."
                }
              }
            }
          }
        }
      }
    },
    "/api/x402/{jobId}": {
      "get": {
        "tags": ["x402"],
        "operationId": "getX402Letter",
        "summary": "Get a letter's status",
        "description": "Poll this after uploading, every few seconds until ready_to_send, then less often until posted, delivered or failed. When status is ready_to_send, payment and sendUrl say how to pay. duplex and fold can come back false after processing: statutory declarations are always single-sided, and documents over 10 pages are posted flat.",
        "parameters": [
          {
            "$ref": "#/components/parameters/JobId"
          }
        ],
        "responses": {
          "200": {
            "description": "The letter's current status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/X402LetterStatus"
                },
                "examples": {
                  "awaitingUpload": {
                    "summary": "Created, waiting for the file.",
                    "value": {
                      "jobId": "019d7a7f-a183-752d-9b3b-11511179033b",
                      "status": "awaiting_upload",
                      "service": "express",
                      "duplex": false,
                      "fold": false,
                      "signature": true,
                      "recipient": {
                        "name": "Leo Tran",
                        "line1": "52 Smith Street",
                        "suburb": "Darwin",
                        "state": "NT",
                        "postcode": "0800"
                      }
                    }
                  },
                  "readyToSend": {
                    "summary": "Laid out on A4 and priced, waiting for payment.",
                    "value": {
                      "jobId": "019d7a7f-a183-752d-9b3b-11511179033b",
                      "status": "ready_to_send",
                      "service": "express",
                      "duplex": false,
                      "fold": false,
                      "signature": true,
                      "recipient": {
                        "name": "Leo Tran",
                        "line1": "52 Smith Street",
                        "suburb": "Darwin",
                        "state": "NT",
                        "postcode": "0800"
                      },
                      "pageCount": 3,
                      "previewUrl": "/preview/019d7a7f-a183-752d-9b3b-11511179033b/contract.pdf?Expires=...&Signature=...&Key-Pair-Id=...",
                      "payment": {
                        "scheme": "exact",
                        "network": "eip155:8453",
                        "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
                        "amountAtomic": "20000000",
                        "amountDisplay": "20 USDC",
                        "payTo": "0x1111111111111111111111111111111111111111",
                        "expiresAt": "2026-09-25T02:15:00Z"
                      },
                      "sendUrl": "/api/x402/019d7a7f-a183-752d-9b3b-11511179033b/send"
                    }
                  },
                  "posted": {
                    "summary": "Lodged with Australia Post.",
                    "value": {
                      "jobId": "019d7a7f-a183-752d-9b3b-11511179033b",
                      "status": "posted",
                      "service": "express",
                      "duplex": false,
                      "fold": false,
                      "signature": true,
                      "recipient": {
                        "name": "Leo Tran",
                        "line1": "52 Smith Street",
                        "suburb": "Darwin",
                        "state": "NT",
                        "postcode": "0800"
                      },
                      "pageCount": 3,
                      "providerReference": "LTR-8F3A21C9"
                    }
                  },
                  "failed": {
                    "summary": "Not posted, for example an unpaid quote that expired.",
                    "value": {
                      "jobId": "019d7a7f-a183-752d-9b3b-11511179033b",
                      "status": "failed",
                      "service": "express",
                      "duplex": false,
                      "fold": false,
                      "signature": true,
                      "recipient": {
                        "name": "Leo Tran",
                        "line1": "52 Smith Street",
                        "suburb": "Darwin",
                        "state": "NT",
                        "postcode": "0800"
                      },
                      "pageCount": 3,
                      "error": "quote expired"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown jobId.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notFound": {
                    "value": {
                      "error": "job not found"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/x402/{jobId}/send": {
      "post": {
        "tags": ["x402"],
        "operationId": "sendX402Letter",
        "summary": "Pay for and post a letter",
        "description": "Call once without PAYMENT-SIGNATURE to receive a 402 with the payment requirements in the PAYMENT-REQUIRED header (x402 version 2, one accepts entry). Sign the payment and retry the same POST with PAYMENT-SIGNATURE. The payment is verified and settled through the facilitator, and the letter goes to print. The call is idempotent: once a letter is paid for, repeating it returns the current status without charging again.",
        "parameters": [
          {
            "$ref": "#/components/parameters/JobId"
          },
          {
            "$ref": "#/components/parameters/PaymentSignature"
          }
        ],
        "responses": {
          "200": {
            "description": "Already paid for and posted or delivered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/X402SendResponse"
                },
                "examples": {
                  "posted": {
                    "value": {
                      "jobId": "019d7a7f-a183-752d-9b3b-11511179033b",
                      "status": "posted"
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Payment settled and the letter is with the print partner, or a repeat of a paid request while it is still on its way.",
            "headers": {
              "PAYMENT-RESPONSE": {
                "$ref": "#/components/headers/PaymentResponse"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/X402SendResponse"
                },
                "examples": {
                  "sending": {
                    "value": {
                      "jobId": "019d7a7f-a183-752d-9b3b-11511179033b",
                      "status": "sending"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Payment required. Expected on the first call, without PAYMENT-SIGNATURE. Also returned when a payment can't be verified or settled, with the reason in error. Read the PAYMENT-REQUIRED header, sign the payment and retry.",
            "headers": {
              "PAYMENT-REQUIRED": {
                "$ref": "#/components/headers/PaymentRequired"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "paymentRequired": {
                    "value": {
                      "error": "payment required"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown jobId.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notFound": {
                    "value": {
                      "error": "job not found"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The letter isn't ready to pay for yet, or its quote expired. \"job not ready to send\" can also come back for a second or two right after the status turns ready_to_send; wait and retry. An expired quote can't be paid: create the letter again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notReady": {
                    "value": {
                      "error": "job not ready to send"
                    }
                  },
                  "quoteExpired": {
                    "value": {
                      "error": "quote expired"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Settlement failed or timed out and it isn't yet known whether the payment went through. Don't sign a new payment: poll GET /api/x402/{jobId} (after Retry-After seconds) until the status is sending, or ready_to_send if the payment was never taken.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "const": "payment_pending"
                    },
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "JobId": {
        "name": "jobId",
        "in": "path",
        "required": true,
        "description": "The jobId returned by POST /api/x402.",
        "schema": {
          "type": "string"
        }
      },
      "PaymentSignature": {
        "name": "PAYMENT-SIGNATURE",
        "in": "header",
        "required": false,
        "description": "Base64-encoded x402 payment payload. Leave it out on the first call to receive the 402 challenge.",
        "schema": {
          "type": "string"
        }
      }
    },
    "headers": {
      "PaymentRequired": {
        "description": "Base64-encoded JSON x402 payment challenge (x402Version 2). Decoded, it has error, resource, accepts (one entry: scheme exact, network eip155:8453, the USDC asset address, amount in atomic units, payTo, maxTimeoutSeconds and the token's EIP-712 domain in extra) and a bazaar discovery extension.",
        "schema": {
          "type": "string"
        }
      },
      "PaymentResponse": {
        "description": "Base64-encoded JSON settlement response, including the transaction hash.",
        "schema": {
          "type": "string"
        }
      }
    },
    "schemas": {
      "ErrorResponse": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "string",
            "description": "What went wrong."
          }
        }
      },
      "PathAction": {
        "type": "object",
        "required": ["method", "path"],
        "properties": {
          "method": {
            "type": "string",
            "enum": ["GET", "POST"]
          },
          "path": {
            "type": "string"
          }
        }
      },
      "X402Descriptor": {
        "type": "object",
        "required": ["name", "version", "openapi", "create", "status", "send", "flow"],
        "properties": {
          "name": {
            "type": "string"
          },
          "version": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "openapi": {
            "type": "string",
            "description": "Relative URL of this OpenAPI document."
          },
          "create": {
            "$ref": "#/components/schemas/PathAction"
          },
          "status": {
            "$ref": "#/components/schemas/PathAction"
          },
          "send": {
            "$ref": "#/components/schemas/PathAction"
          },
          "flow": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The steps of the expected client flow, in order."
          }
        }
      },
      "Address": {
        "type": "object",
        "description": "A postal address. Each line is at most 40 characters so it fits on the envelope. Supply a name or a company, or both. The recipient must be in Australia; a return address can set country, and then state and postcode are free-form and optional.",
        "required": ["line1", "suburb"],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 40
          },
          "company": {
            "type": "string",
            "maxLength": 40
          },
          "line1": {
            "type": "string",
            "maxLength": 40,
            "description": "Street address, or a PO Box such as \"PO Box 123\"."
          },
          "line2": {
            "type": "string",
            "maxLength": 40
          },
          "suburb": {
            "type": "string",
            "maxLength": 40
          },
          "state": {
            "type": "string",
            "maxLength": 40,
            "description": "In Australia, one of ACT, NSW, NT, QLD, SA, TAS, VIC or WA."
          },
          "postcode": {
            "type": "string",
            "maxLength": 40,
            "description": "In Australia, four digits that belong to the state."
          },
          "country": {
            "type": "string",
            "maxLength": 40,
            "description": "Return address only. Leave it out, or set it to Australia, for an Australian address."
          }
        }
      },
      "Service": {
        "type": "string",
        "enum": ["express", "regular"],
        "description": "express is Australia Post Express Post, usually next business day within the Express Post network. regular is regular post, usually 3 to 7 business days. Both are tracked."
      },
      "X402CreateRequest": {
        "type": "object",
        "required": ["fileName", "recipient", "sender"],
        "properties": {
          "fileName": {
            "type": "string",
            "description": "File name with extension: .pdf, .doc, .docx, .jpg, .jpeg or .png."
          },
          "recipient": {
            "$ref": "#/components/schemas/Address"
          },
          "sender": {
            "$ref": "#/components/schemas/Address",
            "description": "Return address, printed on the envelope so undeliverable mail comes back. Required."
          },
          "service": {
            "$ref": "#/components/schemas/Service",
            "default": "express"
          },
          "duplex": {
            "type": "boolean",
            "default": false,
            "description": "Print double-sided. Statutory declarations are always single-sided."
          },
          "fold": {
            "type": "boolean",
            "default": false,
            "description": "Fold to fit a letterbox. Only documents of up to 10 pages are folded; longer ones are posted flat in an A4 envelope."
          },
          "signature": {
            "type": "boolean",
            "default": false,
            "description": "Signature on delivery. Express Post only: 400 with regular post."
          },
          "bizKey": {
            "type": "string",
            "description": "A business's bulk-pricing key, from support@unopost.com.au. The letter is quoted at the business's price, from 2 USDC a letter. An unknown key is a 400."
          }
        }
      },
      "X402CreateResponse": {
        "type": "object",
        "required": ["jobId", "status", "uploadUrl", "statusUrl", "sendUrl"],
        "properties": {
          "jobId": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": ["awaiting_upload"]
          },
          "uploadUrl": {
            "type": "string",
            "description": "Signed URL, valid for 15 minutes. PUT the raw file bytes here, up to 50 MB."
          },
          "statusUrl": {
            "type": "string",
            "description": "Relative URL to poll for status."
          },
          "sendUrl": {
            "type": "string",
            "description": "Relative URL to pay for and post the letter once it is ready_to_send."
          }
        }
      },
      "X402Payment": {
        "type": "object",
        "required": ["scheme", "network", "asset", "amountAtomic", "amountDisplay", "payTo", "expiresAt"],
        "properties": {
          "scheme": {
            "type": "string",
            "enum": ["exact"]
          },
          "network": {
            "type": "string",
            "description": "CAIP-2 network, eip155:8453 for Base mainnet."
          },
          "asset": {
            "type": "string",
            "description": "USDC contract address on Base."
          },
          "amountAtomic": {
            "type": "string",
            "description": "Amount in USDC's smallest unit (6 decimals), for example 20000000 for 20 USDC."
          },
          "amountDisplay": {
            "type": "string",
            "description": "Human-readable amount, for example 20 USDC."
          },
          "payTo": {
            "type": "string",
            "description": "Receiving wallet address."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the quote expires, 7 days after it was made. An unpaid letter then fails."
          }
        }
      },
      "X402Status": {
        "type": "string",
        "enum": ["awaiting_upload", "processing", "ready_to_send", "sending", "retrying", "posted", "delivered", "failed", "refunded"],
        "description": "awaiting_upload: waiting for the file. processing: laying the document out on A4 and pricing it. ready_to_send: waiting for payment; payment and sendUrl are set. sending: paid, with the print partner. retrying: the print partner hit a problem and the letter is being retried automatically. posted: lodged with Australia Post; providerReference is set. delivered: Australia Post has delivered it. failed: not posted, with the reason in error (including an unpaid quote that expired); a paid letter that can't be posted is refunded in full in USDC. refunded: the payment has been sent back to the wallet that paid, whether or not the letter was posted."
      },
      "X402LetterStatus": {
        "type": "object",
        "required": ["jobId", "status", "service", "duplex", "fold", "signature", "recipient"],
        "properties": {
          "jobId": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/X402Status"
          },
          "service": {
            "$ref": "#/components/schemas/Service"
          },
          "duplex": {
            "type": "boolean",
            "description": "Can turn false after processing for a statutory declaration."
          },
          "fold": {
            "type": "boolean",
            "description": "Can turn false after processing for a document over 10 pages."
          },
          "signature": {
            "type": "boolean"
          },
          "recipient": {
            "$ref": "#/components/schemas/Address",
            "description": "Once the letter is posted, delivered, failed or expired, only the name is returned."
          },
          "sender": {
            "$ref": "#/components/schemas/Address",
            "description": "Omitted once the letter is posted, delivered, failed or expired."
          },
          "pageCount": {
            "type": "integer",
            "description": "Pages as printed, once processed."
          },
          "previewUrl": {
            "type": "string",
            "description": "Relative, signed URL of the print-ready PDF, valid for 15 minutes; poll again for a fresh one. Only while ready_to_send."
          },
          "payment": {
            "$ref": "#/components/schemas/X402Payment"
          },
          "sendUrl": {
            "type": "string",
            "description": "Relative URL to pay for and post the letter. Present when ready_to_send."
          },
          "providerReference": {
            "type": "string",
            "description": "Our print partner's reference for the letter, once posted."
          },
          "error": {
            "type": "string",
            "description": "Why the letter failed."
          }
        }
      },
      "X402SendResponse": {
        "type": "object",
        "required": ["jobId", "status"],
        "properties": {
          "jobId": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/X402Status"
          }
        }
      }
    }
  }
}
