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.
The states
lifecycle | Meaning | Safe to show user? | Safe to take irreversible action on? |
|---|---|---|---|
unknown | Signature not seen by the validator we polled, or not propagated yet. | No | No |
submitted | Signature reached the cluster but no commitment yet — rare transient state. | "Pending…" | No |
processing | In a recent block, not yet voted on. Equivalent to Solana processed. | "Pending…" | No |
confirmed | Supermajority confirmed. Will reorg only in pathological cases. Equivalent to Solana confirmed. | "Sent" | Most UX should branch here |
finalized | Slot is rooted — irreversible. Equivalent to Solana finalized. | "Settled" | Yes — for irreversible business actions |
failed | On-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
lifecycle is derivedstatus 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:
unknownshortly after submit is normal. Validators propagate at slightly different rates. Don't fail fast on the firstunknown— give it 2–3 polls.- For
mode=signedresponses, the API already waits forconfirmedbefore returning. You'll never seeunknown/submitted/processingfor those signatures — onlyconfirmed/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
- Transaction Modes —
unsignedvssignedrequest shapes - Simulating Transactions — dry-run before submitting
- Retry Policy — when and how to retry on
failed