TriportRPC

Invoice SSE event stream

GEThttps://api.triport.io/v1/billing/invoices/inv_8f2c1a/events

Subscribe to live state changes for a single invoice over Server-Sent Events (SSE) — from the moment it is created until it is paid, cancelled, or expires.

— (account/billing surface, not chain-specific)— (one long-lived SSE connection per invoice)

This endpoint opens a long-lived text/event-stream (SSE) connection scoped to one invoice. Use it to drive a live payment screen: render a QR code, then react the instant the payment lands instead of polling GET /v1/billing/invoices/{id}.

The first frame is always a snapshot carrying the invoice's current status, so a freshly opened connection immediately tells you where the invoice stands. After the snapshot, named events are pushed as the invoice's state changes.

The stream is reconnect-safe: it carries no resumable cursor or Last-Event-ID semantics, so if the connection drops, simply re-subscribe — you will receive a fresh snapshot reflecting the latest state and can continue from there.

Two important lifecycle behaviours:

  • If the invoice is already in a terminal state (paid, expired, or cancelled) when you connect, the server emits a single snapshot and then closes the stream — there is nothing further to wait for.
  • For a pending invoice, the server holds the stream open and emits a : keep-alive comment every 15 seconds to keep proxies from idling the connection. Once an invoice_paid event for this invoice is delivered, the server closes the stream.

Parameters

Path parameters

idstringrequired
The invoice ID returned by POST /v1/billing/invoices. If no invoice with this ID exists, the endpoint returns 404.

Response

Content-Type: text/event-stream. Each frame is a named SSE event whose data: line is a JSON object. A pending invoice that is then paid produces a stream like:

(After the invoice_paid frame for this invoice, the server closes the connection.)

Event types

The stream defines the following named events. Every invoice_* event shares a common JSON envelope; fields that are zero/empty are omitted.

EventWhen it firesData payload
snapshotAlways, as the first frame after connecting (and the only frame if the invoice is already terminal).{ "status": <status> }
invoice_createdThe invoice has been created.{ "type": "invoice_created", "invoice_id": <string> }
invoice_progressA partial / in-progress payment update for the invoice.invoice envelope (see below)
invoice_late_paymentA payment arrived for an invoice that had already expired.invoice envelope (see below)
invoice_paidThe invoice is fully paid. The server closes the stream right after this frame.invoice envelope (see below)
invoice_cancelledThe invoice was cancelled (e.g. via POST /v1/billing/invoices/{id}/cancel).invoice envelope (see below)
invoice_expired_sweepThe periodic sweeper expired one or more pending invoices. This is a broadcast event and is not scoped to your invoice.{ "type": "invoice_expired_sweep" }

snapshot data fields

FieldTypeDescription
statusstringCurrent invoice status: pending, paid, expired, or cancelled.

Invoice event envelope

invoice_paid (and the other invoice_* events that carry it) use this shape:

FieldTypeDescription
typestringThe event name, e.g. "invoice_paid".
invoice_idstringThe invoice this event refers to.
payer_user_idstring(optional) The user who paid. Present on payment events.
payment_idnumber(optional) The settled payment's ID.
amount_micronumber(optional) Amount in micro-USD (1500000 = 1.50).
activation_failedboolean(optional) true if payment settled but the linked subscription/top-up activation did not complete.

Errors

This endpoint does not return a JSON error envelope on the stream itself — it fails before the stream opens:

CodeMeaningWhen it happens
404 Not FoundUnknown invoiceNo invoice exists for the given {id}.
500 Internal Server ErrorStreaming unsupportedThe server could not establish a streaming connection.

See errors.md for the standard error envelope used by the non-streaming billing endpoints.

Examples

JavaScript (fetch)

EventSource is the idiomatic SSE client in the browser (it sends the same-origin session cookie automatically):

const id = "inv_8f2c1a";
const es = new EventSource(`https://api.triport.io/v1/billing/invoices/${id}/events`);


const onAny = (name) => (e) => {
  const data = e.data ? JSON.parse(e.data) : null;
  console.log(name, data);
};


for (const name of [
  "snapshot",
  "invoice_created",
  "invoice_progress",
  "invoice_late_payment",
  "invoice_paid",
  "invoice_cancelled",
  "invoice_expired_sweep",
]) {
  es.addEventListener(name, onAny(name));
}


es.addEventListener("invoice_paid", () => es.close()); // stream ends after paid

TypeScript SDK (@triport/sdk)

import { TriportClient } from "@triport/sdk";


const client = new TriportClient(); // uses the session cookie in the browser


// Returns an unsubscribe function; re-call to re-subscribe after a disconnect.
const unsubscribe = client.billing.subscribeInvoice(
  "inv_8f2c1a",
  (eventType, data) => {
    if (eventType === "snapshot") console.log("current status:", data.status);
    if (eventType === "invoice_paid") {
      console.log("paid:", data.amount_micro);
      unsubscribe();
    }
  },
);

Python (triport-sdk)

EventSource is browser-only; from Python, read the raw stream:

import json
import httpx


url = "https://api.triport.io/v1/billing/invoices/inv_8f2c1a/events"
cookies = {"session": TRIPORT_SESSION}


with httpx.stream("GET", url, cookies=cookies, timeout=None) as r:
    r.raise_for_status()
    event = None
    for line in r.iter_lines():
        if line.startswith("event: "):
            event = line[len("event: "):]
        elif line.startswith("data: "):
            data = json.loads(line[len("data: "):])
            print(event, data)
            if event == "invoice_paid":
                break  # server closes the stream after this

Notes