# Payment SDK failure and recovery contract, RC4 The SDK preserves the supplied payment identity and gives a recovery action when the response is unavailable or invalid. The retained server lineage, reviewed provider contract and connected release paths enforce the right to pay. This is not a guarantee about payment paths outside that boundary. No SDK method generates an operation ID, selects another provider, renews an approval or retries a financial request automatically. HOLD and REFUSE block the step even at HTTP 200. ACCEPT is a retained admission, not settlement. A historical ACCEPT is not another authorization. ## Failure matrix | Invocation | Unknown response | Retained identity | First recovery action | | --- | --- | --- | --- | | Create attempt | UncertainDispatch | OPERATION_ID, obligation and approval revision | LOOKUP_OPERATION | | Resume funding | UncertainDispatch | ATTEMPT_ID and submitted approval revision | LOOKUP_ATTEMPT | | Cancel original | UncertainDispatch | ATTEMPT_ID | LOOKUP_ATTEMPT | | Recover original | UncertainDispatch | ATTEMPT_ID | LOOKUP_ATTEMPT | | Source approval | UncertainDispatch | SOURCE_APPROVAL and exact request fingerprint | REVIEW_SOURCE | | Any read | TransportError | Original operation, attempt or obligation ID | REPEAT_READ | | Invalid local input | ValidationError | Caller retains its input | Correct input; this invocation was not sent | | Valid server rejection | APIError | Recovery context and error code | Resolve that rejection within the original lineage | Timeouts, connection errors, incomplete replies, malformed or duplicate-key JSON, invalid UTF-8, oversized replies, redirects, invalid response contracts, HTTP 408/429 and HTTP 5xx never become permission to send another payment. Even an error before a connection appears to exist is conservatively uncertain for POST. A read failure does not imply a financial dispatch. RecoveryContext is frozen and JSON serializable: api_origin, request_kind, action, identity_kind, identity_value, operation_id, obligation_id, attempt_id, approval_revision, request_fingerprint and request_may_have_executed. `request_may_have_executed` describes the HTTP operation. Recovery POSTs may update retained evidence without creating or funding a payment. ## Caller persistence and recovery Before create, persist the customer/tenant lineage, original operation ID, obligation ID, route, approval revision and exact request. Before resume or cancel, persist the original attempt ID and submitted action. Persist the returned attempt ID before acting on later steps. Process death, task replay, provider switching and key rotation must recover those records. The fingerprint is SHA-256 of UTF-8 method, newline, path/query, newline and exact transmitted body, excluding authorization. Financial request keys are serialized in sorted order. Signed-source strings retain their original bytes. The fingerprint detects request differences; it is not a signature, lookup endpoint or global idempotency key. Retain origin and customer lineage with it. API keys and raw signed payloads are excluded from recovery metadata. After an uncertain create, lookup the saved operation ID using the same origin and authorized customer lineage. Lookup returns a historical decision and `redispatched=false`, with the retained attempt when one exists. A lookup 404 may mean the key has the wrong scope, state is missing or intake was not retained. It proves neither closure nor absence of an earlier payment. HOLD the business workflow and reconcile scope/history; do not mint a new ID or fresh root. After uncertain resume or cancel, read the original attempt. If fresh evidence is needed, recover that attempt. Recover does not replay create or funding. An exposed, unfunded Wise attempt requires an explicit reviewed resume with the current signed revision and a proven unspent step. Repeating resume after a retained funding intent returns HOLD; a missing acknowledgement does not make funding unspent. Cancellation acceptance alone does not establish CLOSED. After uncertain source approval, retain the original signed envelope and fingerprint and review issuer/source history in the existing lineage. There is no SDK source-lookup endpoint and no automatic new approval. A caller may submit the identical envelope only under the source revision/alias contract; it must not treat this as a new payment right. ```python from haltseal_payments_sdk import UncertainDispatch # saved_request is durably stored before the first invocation. try: result = client.create_attempt(**saved_request) except UncertainDispatch as exc: persist_recovery(exc.recovery.to_dict()) # application's durable store # If lookup itself fails, keep this workflow on HOLD. retained = client.lookup_operation(exc.operation_id) # Inspect historical decision and current attempt; do not dispatch a backup. ``` Python exposes snake_case fields; JavaScript also provides camelCase error properties such as recoveryAction and attemptId. JavaScript `error.recovery` uses the same snake_case JSON keys as Python `error.recovery.to_dict()`. The legacy UncertainDispatch.operation_id / operationId remains available for create. Resume and cancel expose attempt_id / attemptId instead. ## Authority after failure UNKNOWN or an original still able to pay retains the full principal and blocks every replacement. PAID permanently consumes the obligation. CLOSED requires authoritative exact original evidence under the reviewed closure contract. Replacement then needs a newer signed approval issued after retained closure, fresh reobservation, unchanged economics and current release validation. Neither an HTTP request ID, a fingerprint nor a newer source revision bypasses those conditions. ## Transport and observation The clients default to HTTPS, with literal loopback HTTP for evaluation. Credentials, paths, queries, fragments, malformed origins, empty keys and invalid deadlines are refused locally. Requests are bounded at 16 KiB and responses at 1 MiB. Invalid identifiers/revisions and unreviewed evaluation routes are refused before IO. Responses must match their endpoint and original attempt/obligation when that identity is known. Python builds its urllib opener once and supports an injected Transport plus close/context-manager lifecycle. Its timeout is a socket timeout, not separate connect/read/write timeouts or a total wall-clock deadline. Node's timeoutMs bounds fetch and streamed body consumption with abort. Node close stops new calls; the owner closes any injected fetch implementation's pools. Neither client cancels an already committed financial step on close. Custom transports must send once, refuse redirects, use certificate verification, respect response/deadline limits and perform no financial IO on close. TLS, proxies and connection pools can be supplied behind this interface. Post-response checks cannot undo a custom transport's hidden retry or forwarded credentials. Each adapter needs its own conformance qualification; this release does not establish production pooling, independent-host availability or GA. Optional on_event / onEvent emits request kind, fingerprint, X-Request-ID, HTTP status, result class and duration. Callback failures cannot change the payment verdict. No built-in logging sends API keys, bodies or raw provider exceptions. Applications remain responsible for redacting their own logs. X-Request-ID is per HTTP request and has no payment-authority meaning. With workflow engines such as Temporal, configure financial activities so their failure does not cause a new create, ID or provider selection. Default activity retries are not a HALTSEAL recovery policy. Persist identity in workflow state and route unknown outcomes to lookup/recovery; admit further financial steps only under the retained kernel contract. This release contains no Temporal adapter. See the [official retry policy documentation](https://docs.temporal.io/encyclopedia/retry-policies). ## Release boundary Python wheel includes type hints and py.typed; Node tarball includes .d.ts. Both packages are MIT, dependency-free at runtime and distributed as repository archives. Public registry publication is not claimed. Server/evaluation terms are separate. See INTEGRATION_ACCEPTANCE.md for the real-customer gates and COMMERCIAL_DELIVERY.md for the API product and optional onboarding service.