Transaction Lifecycle

Transaction Lifecycle

Every transaction submitted through the API moves through a small fixed set of states. GET /v2/transactions/{signature} returns the current state under lifecycle. Branch on lifecycle rather than raw confirmation_status — it collapses three Solana commitment levels plus error and unknown states into one enum that's safe for product UX.

Transaction lifecycle states

The states

lifecycleMeaningSafe to show user?Safe to take irreversible action on?
unknownSignature not seen by the validator we polled, or not propagated yet.NoNo
submittedSignature reached the cluster but no commitment yet — rare transient state."Pending…"No
processingIn a recent block, not yet voted on. Equivalent to Solana processed."Pending…"No
confirmedSupermajority confirmed. Will reorg only in pathological cases. Equivalent to Solana confirmed."Sent"Most UX should branch here
finalizedSlot is rooted — irreversible. Equivalent to Solana finalized."Settled"Yes — for irreversible business actions
failedOn-chain error. err field has the program error."Failed"No retry without inspecting err

The two states you'll branch on 99% of the time are confirmed and finalized.

Why two "success" states

The split mirrors the same distinction Newline draws between settled (funds moved) and posted (appears on the bank statement, irreversible). On Solana:

  • confirmed ≈ "settled" — supermajority of validators voted on the block. UI can flip from "Pending…" to "Sent". A reorg here would take a network-level fault.
  • finalized ≈ "posted" — the block has been rooted. By construction, this cannot be reverted by anything short of a network restart.

If a customer is buying a coffee with your token, confirmed is enough to ship the drink. If you're crediting a user's $10,000 fee payout in your internal ledger, wait for finalized.

How lifecycle is derived

status from getSignatureStatuses:
  null              → lifecycle: "unknown"
  err set            → lifecycle: "failed"
  confirmationStatus = "processed" → lifecycle: "processing"
  confirmationStatus = "confirmed" → lifecycle: "confirmed"
  confirmationStatus = "finalized" → lifecycle: "finalized"
  otherwise          → lifecycle: "submitted"

We call getSignatureStatuses with searchTransactionHistory: true, so old signatures resolve correctly instead of returning null once they age out of the recent-status cache.

Response shape

curl https://api.piratecrew.fun/v2/transactions/$SIGNATURE \
  -H "Authorization: Bearer $PIRATE_API_KEY"
{
  "data": {
    "signature": "5kJ2…",
    "lifecycle": "confirmed",
    "confirmation_status": "confirmed",
    "err": null,
    "slot": 271234567,
    "confirmations": 12
  },
  "meta": { "request_id": "req_…" }
}

confirmation_status is the raw Solana value (processed | confirmed | finalized | null). Most clients can ignore it and branch on lifecycle. It's surfaced for clients that need the unmapped value (e.g. analytics that compute "time-to-finalized").

Polling pattern

async function waitForFinalized(sig: string, timeoutMs = 60_000) {
  const start = Date.now();
  let delay = 1_000;
  while (Date.now() - start < timeoutMs) {
    const { data } = await fetch(`/v2/transactions/${sig}`, {
      headers: { Authorization: `Bearer ${process.env.PIRATE_API_KEY}` },
    }).then((x) => x.json());
    if (data.lifecycle === "finalized") return data;
    if (data.lifecycle === "failed") throw new Error(`tx failed: ${JSON.stringify(data.err)}`);
    await new Promise((res) => setTimeout(res, delay));
    delay = Math.min(delay * 1.5, 10_000);   // gentle backoff
  }
  throw new Error("timeout waiting for finalization");
}

Two practical notes:

  • unknown shortly after submit is normal. Validators propagate at slightly different rates. Don't fail fast on the first unknown — give it 2–3 polls.
  • For mode=signed responses, the API already waits for confirmed before returning. You'll never see unknown / submitted / processing for those signatures — only confirmed / finalized / failed.

Lifecycle and webhooks

The transaction.confirmed, transaction.finalized, and transaction.failed webhook events fire as the lifecycle transitions — so you can replace polling with push delivery. The lifecycle enum is identical between this REST response and the webhook payload, so callers can share branching logic across both.

See also