Paratro MPC Wallet Gateway Python SDK
Official Python SDK for the Paratro MPC Wallet Gateway.
1.9.0 is a breaking release: the gateway base URL is now required.
Config.sandbox()/Config.production()/Config.custom()are gone; pass the gateway explicitly asConfig("https://<gateway-host>")— see Configuration and Migrating from 1.8. 1.8.1 already retiredPOST /api/v1/transferandPOST /api/v1/x402/sign(HTTP 410): every transaction goes throughPOST /api/v1/transactionswith anoperationfield, see Migrating from 1.6 and earlier and the ⚠️ Breaking sections of CHANGELOG.md.
Features
- MPC Wallets / Accounts / Assets — create and manage wallets, per-chain accounts and tokens
- Unified transactions entry —
client.transactions.create()with three operations:TRANSFER— asynchronous transfer signed by the MPC enginePROGRAM_CALL— co-sign a counterparty-signed Solana transaction (xChange swap shape)CONTRACT_CALL— EVM AtomicSwapexecuteSwapwith an EIP-2612 permit
- Typed errors —
{code,type,message}parsed intoRejectedError/ForbiddenError/NotFoundError/ServiceUnavailableError/EndpointRetiredError…, withreason_tagfor400 "Rejected: <tag>: …" - x402 facilitator —
verify/settle/settle_status/list_settlements - Webhooks — HMAC signature verification + event parsing
- Auth handled for you — JWT from
X-API-Key/X-API-Secret, refreshed before expiry; a401 token_expiredre-authenticates once and retries once. Thread-safe.
Installation
pip install paratro-sdk
Requirements: Python 3.9+, requests.
Quick Start
from paratro import MPCClient, Config, TransferRequest, APIError
client = MPCClient("your-api-key", "your-api-secret", Config("https://<gateway-host>"))
result = client.transactions.create(TransferRequest(
from_address="0xYourVaultAddress",
to_address="0xRecipient",
chain="ethereum",
token_symbol="USDC",
amount="10.5", # human-readable decimal string, max 18 decimals
reference_id="order-1001", # your business id; reusing it -> 400 Duplicate reference_id
))
print(result.tx_id, result.status) # status is PENDING: the engine signs asynchronously
tx = client.transactions.get(result.tx_id)
print(tx.status, tx.tx_hash)
Runnable versions of every snippet below live in examples/ and are executed by the test
suite against a fake gateway (tests/test_examples.py). They read PARATRO_API_KEY, PARATRO_API_SECRET
and PARATRO_BASE_URL from the environment (examples/_env.py).
Configuration
from paratro import Config
Config("https://<gateway-host>") # the gateway base URL — always explicit, no default, no presets
Config takes exactly one argument: the base URL of the gateway you were given. It must be an absolute
http:// / https:// URL (a trailing slash is dropped); anything else makes MPCClient(...) raise
ValueError("base URL must be an absolute http(s) URL, …") before any request is sent. Building the
Config itself never raises. Paratro cloud and private deployments are configured the same way:
| Deployment | Base URL |
|---|---|
| Paratro cloud — sandbox | https://api-sandbox.paratro.com |
| Paratro cloud — production | https://api.paratro.com |
| Private deployment | the gateway address your operations team gives you |
The SDK itself carries no gateway address; this table is the only place the Paratro cloud endpoints appear.
HTTP timeout
MPCClient(api_key, api_secret, config, timeout=200) — the timeout (seconds) bounds every HTTP
exchange, the auth call included, and defaults to paratro.DEFAULT_TIMEOUT = 200 s.
PROGRAM_CALL / CONTRACT_CALL are synchronous: the gateway holds the connection while the engine
signs and broadcasts — engine budget 120 s, it waits 150 s (budget + 30 s margin) for the engine
before answering 202, and its server write timeout closes the connection at 180 s. The default
sits above the 180 s write timeout so the gateway, never the SDK, is the side that gives up: a
slow engine ends in a 202 with a tx_id instead of a client-side timeout. If the SDK gave up
first you would lose the tx_id while the row already exists under your reference_id (a resend
answers 400 Duplicate reference_id, and the API cannot search by reference_id); 150 s is
exactly when the gateway answers 202, so it is not enough. Keep the default for clients that
send those two operations; a shorter value is fine for a client that only reads or sends
TRANSFER. Same default as the Go (paratro.DefaultTimeout) and Rust
(config::DEFAULT_TIMEOUT) SDKs.
Authentication
- The first call sends
POST /api/v1/auth/tokenwith headersX-API-Key/X-API-Secretand receives{"token", "expires_in", "token_type", "client": {...}}. - The JWT is cached and sent as
Authorization: Bearer <jwt>. It is refreshed 2 minutes before theexpires_inthe gateway returned (do not hard-code the lifetime; the gateway decides it). - A
401 token_expiredanswer triggers exactly one re-authentication and one retry of the failed request. Any other 401 (unauthorized,invalid_token) is raised asAuthenticationErrorwithout retry. - The caller's IP must be in the client's IP allowlist, otherwise the token endpoint answers
403. - The client never follows HTTP redirects (credentials and signed bodies are not forwarded).
Transactions — POST /api/v1/transactions
One endpoint; the operation field selects what happens. Common fields: operation, reference_id
(idempotency — the same client may not reuse it), from_address, chain, memo (≤ 100 chars).
| operation | request type | success |
|---|---|---|
TRANSFER |
TransferRequest |
200 {tx_id, status:"PENDING", message} |
PROGRAM_CALL |
ProgramCallRequest |
200 {tx_id, status:"BROADCAST", message, tx_hash} or 202 {tx_id, status:"PENDING", message} |
CONTRACT_CALL |
ContractCallRequest |
same as PROGRAM_CALL |
create() returns CreateTransactionResponse(tx_id, status, message, tx_hash, http_status, operation)
with .accepted (http_status == 202, or a PROGRAM_CALL / CONTRACT_CALL with status == "PENDING" —
see Handling 202) and .broadcast (status == "BROADCAST").
operation is filled in by the SDK from the request; the gateway does not echo it.
TRANSFER
from paratro import TransferRequest
result = client.transactions.create(TransferRequest(
from_address="0xYourVaultAddress",
to_address="0xRecipient",
chain="ethereum",
token_symbol="USDC",
amount="10.5",
memo="Invoice #1234",
reference_id="order-1001",
))
# result.status == "PENDING", result.tx_hash == "" -> poll transactions.get() or use webhooks
create_transfer(CreateTransferRequest(...)) from 1.6 still exists and sends exactly this request.
PROGRAM_CALL (Solana)
from paratro import ProgramCallRequest
result = client.transactions.create(ProgramCallRequest(
from_address="YourSolanaPayerWallet", # must be in the fee-payer slot with an empty signature
chain="solana",
signed_transaction="<base64 or 0x-hex>", # counterparty-signed, exactly Memo + 2x TransferChecked, no ALT
receive_address="", # optional; default = from_address
reference_id="quote-42",
))
if result.accepted:
print("engine outcome unknown, poll", result.tx_id) # HTTP 202 — do NOT resend with the same reference_id
else:
print("broadcast", result.tx_hash) # tx_hash = signatures[0]
CONTRACT_CALL (EVM executeSwap)
from paratro import (ContractCall, ContractCallIncomingLeg, ContractCallOutgoingLeg,
ContractCallRequest)
OUR_WALLET = "0x96586e99CE724F45bAb65cf963533b810147c1F4"
COUNTERPARTY = "0xcf8a9d1e489c58f4c3d69b45380fb4a6c03ada47"
result = client.transactions.create(ContractCallRequest(
from_address=OUR_WALLET, # our paying wallet = incoming payer = permit owner = tx sender
chain="ethereum",
receive_address=OUR_WALLET, # our receiving wallet = outgoing.to (default: from_address)
reference_id="quote-0xabc…",
contract_call=ContractCall(
quote_id="0x" + "ab" * 32, # bytes32 hex
expiration=1789449058, # unix seconds, must be in the future
incoming=ContractCallIncomingLeg( # we pay the counterparty
to=COUNTERPARTY, token="0x59fb…(bUSDC)", amount="10000000"), # smallest-unit integer strings
outgoing=ContractCallOutgoingLeg( # the counterparty pays us
from_=COUNTERPARTY, to=OUR_WALLET, token="0xa55a…(AAPLx)", amount="42000000000000000"),
counterparty_signature="0x<65 bytes>", # EIP-712 signature, verified by the contract
permit_deadline=None, # optional; default now + min(max_permit_lifetime_seconds, 300)
),
))
The contract address is not a request field — the gateway takes it from the OPERATION_RULES policy
(call_rules.allowed_contracts[chain]). Native value is always 0.
Request amounts are smallest-unit integer strings (only TRANSFER amounts are in token units). Policy
limits are authored per token in token units (asset_rules.limits[chain][token], e.g. "0.5"); the
gateway converts them with the token's registered decimals, checks registration before limits, and limit
rejections quote both sides in token units (limit_per_transaction: 1 CORZx exceeds per-transaction limit 0.5 CORZx). limits is the only place limits live - one entry per token you pay with, single and
daily both required; a token without an entry is limit_not_configured. The leg that is limit-checked is
the one you pay: incoming for CONTRACT_CALL, your outgoing TransferChecked for PROGRAM_CALL.
Wire format sent for the request above:
{
"operation": "CONTRACT_CALL",
"reference_id": "quote-0xabc…",
"from_address": "0x9658…",
"chain": "ethereum",
"receive_address": "0x9658…",
"contract_call": {
"quote_id": "0xabab…",
"expiration": 1789449058,
"incoming": { "to": "0xcf8a…", "token": "0x59fb…", "amount": "10000000" },
"outgoing": { "from": "0xcf8a…", "to": "0x9658…", "token": "0xa55a…", "amount": "42000000000000000" },
"counterparty_signature": "0x…"
}
}
Handling 202 (engine outcome unknown)
For PROGRAM_CALL / CONTRACT_CALL the gateway may answer HTTP 202 with status="PENDING": the
signing engine did not answer in time and the transaction may or may not have been broadcast. The row
exists, so:
result = client.transactions.create(request)
if result.accepted: # 202, or its Idempotency-Key replay as 200 PENDING
tx = client.transactions.get(result.tx_id) # poll while TransactionStatus.is_in_flight(tx.status)
# do NOT call create() again with the same reference_id — it would be rejected as a duplicate
accepted is True for HTTP 202, and also for a PROGRAM_CALL / CONTRACT_CALL answered
200 PENDING — those operations never answer PENDING otherwise, and that is exactly how an
Idempotency-Key replay of a 202 arrives (the gateway replays cached bodies with HTTP 200). A
TRANSFER's normal 200 PENDING is not "accepted". Same rule as Accepted() in the Go SDK and
is_accepted() in the Rust SDK.
Which errors consume reference_id
The gateway inserts the transaction row before it talks to the signing engine, and the unique key
behind 400 Duplicate reference_id (uk_client_reference) also covers FAILED rows. So whether you may
resend with the same reference_id depends on where the request failed, not on the HTTP status alone:
| Answer | Row | reference_id |
|---|---|---|
400 "Rejected: …" from the pre-checks, 400 insufficient_balance, 403, 404, 503 "Chain RPC unavailable; cannot verify request", 503 "<OP> is not enabled on this gateway" |
not written | free — retry with the same value |
400 "Rejected: …" from the CONTRACT_CALL post-sign re-verification (gateway contract_call.go re-runs VerifyExecuteSwap after the permit was signed and the row inserted) |
PENDING, held until the permit deadline | taken — indistinguishable from a pre-check rejection in the response, so after a rejected CONTRACT_CALL retry, if at all, with a new reference_id; the retry can see insufficient_balance while the reservation is held |
202 |
PENDING | taken — poll tx_id, never resubmit |
400 transaction_failed ("<OP> failed: <tag>") |
FAILED (PENDING, held until the permit deadline, if the CONTRACT_CALL permit was already signed) | taken — submit again with a new reference_id |
503 "Signing service is busy, retry later" (ServiceUnavailableError.is_engine_busy) |
FAILED (PENDING, held until the permit deadline, if the CONTRACT_CALL permit was already signed) | taken — submit again with a new reference_id |
500 |
unknown (some are raised after the insert) | resend with the same value; a 400 Duplicate reference_id tells you the first attempt did create a row |
The engine-busy 503 body carries no tx_id, and GET /transactions has no reference_id filter (the
field is not returned either), so the leftover row can only be located by listing the wallet's recent
transactions. "Held" means the gateway keeps the row PENDING with its reservation locked because a signed
EIP-2612 permit for those funds is still valid; the reconciler releases it (→ FAILED) once the permit
deadline has passed and the on-chain nonce shows the permit was not used, so a retry with a new
reference_id can see insufficient_balance until then. TRANSFER never answers 503.
Idempotency-Key (optional)
create(request, idempotency_key="...") sets the Idempotency-Key header. The gateway caches a 2xx
response for 24 h per (client, key) and replays it — always as HTTP 200, even if the original answer was
202 — without re-processing. accepted stays True on such a replay because it also looks at status
and operation, not only at http_status. reference_id remains the business-level guard
(400 Duplicate reference_id).
Querying
from paratro import ListTransactionsRequest
tx = client.transactions.get("tx_id")
page = client.transactions.list(ListTransactionsRequest(wallet_id="…", account_id="…", chain="ethereum",
page=1, page_size=20))
for tx in page.items:
print(tx.tx_id, tx.transaction_type, tx.status, tx.tx_hash, tx.risk_level)
Transaction fields: tx_id, wallet_id, client_id, chain, transaction_type, from_address,
to_address, token_symbol, amount, status, tx_hash, risk_score, risk_level, created_at
Statuses (TransactionStatus): the API create path moves through PENDING → SIGNED → BROADCAST →
CONFIRMING → CONFIRMED | FAILED. The list is not closed — GET /transactions returns
pto_transactions.status verbatim and the same rows are also written by the portal and the chain watcher,
so PENDING_APPROVAL, REJECTED, CANCELLED (portal approval flow) and COMPLIANCE_BLOCKED (held inbound
deposits) can appear. Poll while TransactionStatus.is_in_flight(status) (PENDING / SIGNED /
PENDING_APPROVAL) instead of switching on an exhaustive set of "done" values; on-chain finality is
CONFIRMED / FAILED (or the webhooks).
Error Handling
Every error body is {"code", "type", "message"}; the SDK raises APIError (or a subclass) carrying
http_status, code, error_type, message.
| HTTP | Exception | When |
|---|---|---|
400 "Rejected: <tag>: …" |
RejectedError |
policy / verifier rejection — inspect reason_tag. Normally pre-insert; the CONTRACT_CALL post-sign re-verification also rejects after the row exists (row held PENDING until the permit deadline, reference_id consumed) |
| 400 other | BadRequestError |
validation, Duplicate reference_id (is_duplicate_reference), insufficient_balance, transaction_failed (row FAILED — or held PENDING after a signed CONTRACT_CALL permit — reference_id consumed, use a new one), Unsupported operation (X402) |
| 401 | AuthenticationError |
token_expired is retried once automatically; unauthorized / invalid_token are not |
| 403 | ForbiddenError |
PROGRAM_CALL / CONTRACT_CALL: no OPERATION_RULES policy authorises this operation/chain (code=forbidden); TRANSFER: destination address blacklisted (code=address_blacklisted, is_address_blacklisted); token endpoint: IP not allowed / client suspended |
| 404 | NotFoundError |
address / asset not this client's, token not credited yet, unknown tx_id |
| 409 | ConflictError |
conflict / resource_exists |
| 410 | EndpointRetiredError |
POST /api/v1/transfer, POST /api/v1/x402/sign (type=endpoint_retired) |
| 429 | RateLimitError |
too_many_requests / api_quota_exceeded |
| 503 | ServiceUnavailableError |
"Chain RPC unavailable; cannot verify request" / "<OP> is not enabled on this gateway": nothing recorded, retry later with the same reference_id. "Signing service is busy, retry later" (is_engine_busy): the row exists (FAILED, or held PENDING after a signed CONTRACT_CALL permit) and keeps your reference_id, retry with a new one. See Which errors consume reference_id |
from paratro import (APIError, RejectedError, ForbiddenError, NotFoundError,
ServiceUnavailableError, RejectionReason)
try:
result = client.transactions.create(request)
except RejectedError as e:
# e.reason_tag is the machine-readable tag, e.g. "expiration_passed"
if e.reason_tag in (RejectionReason.EXPIRATION_PASSED, RejectionReason.EXPIRATION_TOO_FAR):
... # get a fresh quote
elif e.reason_tag in (RejectionReason.LIMIT_PER_TRANSACTION, RejectionReason.LIMIT_DAILY):
... # the leg you pay (CONTRACT_CALL incoming / PROGRAM_CALL outgoing) is over the token's limit
else:
raise
except ForbiddenError:
... # configure an OPERATION_RULES policy (AUTO_APPROVE) in the portal
except NotFoundError as e:
... # e.message, e.g. "no asset registered for token 0x…"
except ServiceUnavailableError as e:
if e.is_engine_busy:
... # row exists (FAILED, or held PENDING) and holds this reference_id: back off, then resubmit with a NEW reference_id
else:
... # chain RPC down / operation not enabled: nothing recorded, back off and retry as is
except APIError as e:
print(e.http_status, e.code, e.error_type, e.message)
APIError.reason_tag also parses 400 code=transaction_failed "<OPERATION> failed: <tag>" (the engine
rejected after the row was created); it is None when the gateway collapsed the reason to
"engine rejected the transaction".
Rejection tags (RejectionReason, verified against the gateway and verifier sources; REJECTED_TAGS
is the union of the two lists below plus the retired limit_decimals_ambiguous, ENGINE_FAILURE_TAGS the
engine verdicts, ALL_TAGS everything):
- CONTRACT_CALL:
abiamount_not_positivecalldatacalldata_not_canonicalcontract_addresscounterparty_not_registeredexpirationexpiration_passedexpiration_too_farincoming_fromlimit_dailylimit_not_configuredlimit_per_transactionoutgoing_topayment_token_not_registeredpermit_deadlinepermit_deadline_passedpermit_deadline_too_farpermit_ownerselectortarget_token_not_registeredvalue_not_zero - PROGRAM_CALL:
account_unresolvablealt_not_allowedata_derivationcounterparty_not_registeredcounterparty_signature_invalidcounterparty_signature_missingfee_payerincoming_destinationlimit_dailylimit_not_configuredlimit_per_transactionmalformedmint_not_registeredmint_program_unknownoutgoing_authorityoutgoing_sourcepayer_signature_presentpolicy_invalidprogram_not_allowedprogram_unresolvableshape 400 transaction_failedengine verdicts (ENGINE_FAILURE_TAGS, 38 values — every literal inmpc-engine internal/syncsettleand, for the EIP-2612 permit a CONTRACT_CALL signs first,internal/syncsign/permit.go; the engine also re-runs the verifiers, so the tags above can appear here too):calldata_invalidcontract_address_invalidcontract_not_registeredcosignature_invaliddaily_allowance_missingdaily_usage_unavailableinternalmint_token_program_unresolvedpayer_invalidpayer_mismatchpermit_digest_mismatchpermit_digest_missingpermit_domain_mismatchpermit_domain_unverifiedpermit_owner_mismatchpermit_params_invalidpermit_spender_mismatchpermit_token_mismatchpermit_token_not_registeredpermit_value_mismatchpolicy_not_authorizedreceiver_invalidreceiver_lookup_failedreceiver_missingreceiver_not_oursrequest_digest_mismatchrequest_digest_missingsigner_slot
The sets are the literals in the code at release time, not a closed vocabulary: a new gateway / engine
release can add tags, and TSS / broadcast failures arrive without one (engine rejected the transaction). Match on the tags you handle and treat an unknown tag as a rejection you have not seen yet -
for example limit_invalid (a per-token asset_rules.limits entry that does not convert to a whole number
of smallest units) is not in RejectionReason yet, but reason_tag still returns it. The opposite also
happens: RejectionReason.LIMIT_DECIMALS_AMBIGUOUS stays for source compatibility, but the gateway no longer
emits it - the policy-wide single_limit / daily_limit it guarded were removed on 2026-09-19, every token
now has its own asset_rules.limits entry (one without an entry is limit_not_configured), and a policy
that still carries the old keys is rejected as a whole with 403.
The pre-1.8 predicates is_not_found, is_rate_limited, is_auth_error remain; is_rejected,
is_forbidden, is_endpoint_retired, is_service_unavailable, is_engine_busy, is_token_expired were added.
Wallets, Accounts, Assets
from paratro import CreateWalletRequest, CreateAccountRequest, CreateAssetRequest
wallet = client.create_wallet(CreateWalletRequest(wallet_name="Treasury", description="Primary"))
# Wallets are created asynchronously: poll get_wallet() until status == key_status == "ACTIVE".
# Signing quorum is 2-of-3 by default; override with threshold= / total_shares= (threshold <= total_shares,
# otherwise 400) and optionally pin the hot signers with signing_party_ids=[...] (len == threshold, unique).
account = client.create_account(CreateAccountRequest(
wallet_id=wallet.wallet_id,
chain="ethereum",
account_type="OUTBOUND", # optional: INBOUND | OUTBOUND
label="Hot wallet", # optional
))
asset = client.create_asset(CreateAssetRequest(
account_id=account.account_id,
symbol="USDC", # ETH TRX USDT USDC BNB MATIC POL BTC SOL MNT
chain="ethereum", # required for EVM accounts
))
client.list_wallets(); client.get_wallet(id)
client.list_accounts(ListAccountsRequest(wallet_id=...)); client.get_account(id)
client.list_assets(ListAssetsRequest(account_id=...)); client.get_asset(id)
Wallet: wallet_id, client_id, wallet_name, description, status, key_status, created_at, updated_at
Account: account_id, wallet_id, client_id, address, network, address_type, label, status, created_at
Asset: asset_id, account_id, wallet_id, client_id, chain, network, symbol, name, contract_address,
decimals, asset_type, balance, locked_balance, is_active, created_at
chain values are the lowercase chain names registered in the gateway's chain registry (e.g. ethereum,
bsc, polygon, arbitrum, optimism, tron, bitcoin, solana). EVM chains share one key
derivation and therefore one address per wallet.
x402 facilitator
The payer-side POST /api/v1/x402/sign has been retired (410) and is not in this SDK. The
facilitator endpoints are:
from paratro import ListX402SettlementsRequest
body = {"x402Version": 1, "paymentPayload": {...}, "paymentRequirements": {...}} # Coinbase-compatible
v = client.x402.verify(body) # POST /api/v1/x402/verify -> is_valid, invalid_reason, payer
s = client.x402.settle(body, idempotency_key="k1")# POST /api/v1/x402/settle -> success, tx_id, transaction, error_reason, payer, network
st = client.x402.settle_status(s.tx_id) # GET /api/v1/x402/settle/{tx_id} -> success, tx_id, status, tx_hash, network
page = client.x402.list_settlements(ListX402SettlementsRequest(status="SETTLED", page=1, page_size=20))
The x402 resource endpoints (GET /api/v1/x402/paid-resources/{resource_id}, GET /api/v1/x402/resources,
GET /api/v1/x402/resources/{resource_id}/receipts) exist on the gateway but are not wrapped by this SDK
yet; call them with client._request("GET", path, params=...) or plain HTTP with the same bearer token.
Webhooks
from paratro import verify_signature, parse_event, WebhookEventType
verify_signature(
secret="your_webhook_secret",
timestamp=request.headers["X-Paratro-Timestamp"],
payload=request.data, # raw body bytes
signature=request.headers["X-Paratro-Signature"], # "v1=<hex HMAC-SHA256 of '{ts}.{body}'>"
)
event = parse_event(request.json)
if event.event_type == WebhookEventType.TRANSACTION_CONFIRMING: ...
elif event.event_type == WebhookEventType.TRANSACTION_CONFIRMED: ...
elif event.event_type == WebhookEventType.TRANSACTION_FAILED: ...
elif event.event_type == WebhookEventType.TRANSFER_CREDITED: ... # internal transfer credited, transaction_type=INTERNAL
elif event.event_type == WebhookEventType.X402_SETTLEMENT_CONFIRMED: ... # x402 settlement credited to the recipient
# event.operation (WebhookOperation: TRANSFER / PROGRAM_CALL / CONTRACT_CALL / DEPOSIT / X402) says what
# kind of movement this is. PROGRAM_CALL / CONTRACT_CALL swaps also carry the counter-asset leg:
leg = event.swap_incoming # SwapIncoming or None
if leg is not None and leg.booked: ... # credit leg.amount (smallest unit) of leg.token_address
elif leg is not None: ... # not credited: see leg.reason / leg.audit_type
| Event | Description |
|---|---|
transaction.confirming |
detected on-chain, awaiting confirmations |
transaction.confirmed |
fully confirmed (balance credited for deposits) |
transaction.failed |
failed on-chain |
transfer.credited |
a Paratro-to-Paratro transfer was credited to the receiving account (transaction_type=INTERNAL) |
x402.settlement.confirmed |
recipient side of an x402 settlement (client.x402.settle) confirmed on-chain and credited (status=CONFIRMED, transaction_type=INBOUND) |
WebhookEvent fields: event_id, event_type, event_time, source_id, wallet_id, account_id,
status, transaction_type, chain, network, txhash, block_number, from_addr (from),
to_addr (to), symbol, contract_address, amount, decimals, confirmations,
required_confirmations, created_at, confirmed_at, risk_checked, risk_score, risk_level, data
Migrating from 1.8
1.9.0 removes the environment presets: the gateway base URL is passed explicitly, the same way for the Paratro cloud and for a private deployment. Nothing else in the API changed.
| 1.8.1 | 1.9.0 |
|---|---|
Config.sandbox() / Config.production() |
Config("https://<gateway-host>") — the address from the table in Configuration |
Config.custom("https://your-gateway") |
Config("https://your-gateway") |
MPCClient(key, secret, Config.custom(url)) |
MPCClient(key, secret, Config(url)) |
| bad base URL → connection error on the first call | MPCClient(...) raises ValueError (empty, or not an absolute http(s) URL) |
Migrating from 1.6 and earlier
The package name and import path do not change (pip install -U paratro-sdk, from paratro import …).
| 1.6 | 1.8.1 |
|---|---|
client.create_transfer(CreateTransferRequest(...)) → POST /api/v1/transfer |
still works, now sends POST /api/v1/transactions with operation=TRANSFER. New code: client.transactions.create(TransferRequest(...)) |
POST /api/v1/x402/sign |
retired by the gateway (410). No replacement in the API; operation=X402 on the unified entry answers 400 Unsupported operation |
Transaction.direction / block_number / confirmations |
removed (never returned by the gateway); risk_score, risk_level added |
list_security_factors / add_security_factor / delete_security_factor / set_security_factor_status |
removed — these are portal (appapi) endpoints authenticated with a portal user session, not gateway API-key endpoints; they never worked against the gateway API, whatever the base URL |
TransferResponse |
alias of CreateTransactionResponse (adds tx_hash, http_status, operation, accepted) |
MPCClient(..., timeout=30) implicit 1.6 default |
default is now 200 (paratro.DEFAULT_TIMEOUT, above the gateway's 180 s write timeout); pass timeout= to change it |
APIError |
unchanged attributes; typed subclasses and reason_tag added |
See CHANGELOG.md for the full list.
Development
python -m venv .venv && . .venv/bin/activate
pip install -e . pytest
python -m pytest -q # unit tests + examples against a loopback fake gateway (no network)
Related SDKs
| Language | Package | Repository |
|---|---|---|
| Go | github.com/paratro/paratro-sdk-go |
paratro-sdk-go |
| Rust | paratro-sdk |
paratro-sdk-rust |
| Python | paratro-sdk |
paratro-sdk-python |
Support
- Documentation: https://docs.paratro.com
- Email: hello@paratro.com
- Issues: https://github.com/paratro/paratro-sdk-python/issues
License
MIT — see LICENSE.
Metadata
Release files for paratro-sdk 1.9.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| paratro_sdk-1.9.0.tar.gz | 72.5 kB | Without provenance | Details for paratro_sdk-1.9.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| paratro_sdk-1.9.0-py3-none-any.whl | Python 3 | none | any | Details for paratro_sdk-1.9.0-py3-none-any.whl |
Total release size: 111.8 kB
Release files / paratro_sdk-1.9.0.tar.gz
| Download URL | paratro_sdk-1.9.0.tar.gz |
|---|---|
| Size | 72.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9510626dedf20c48236ca8fafd83f4b48b76d51ff270fdf55ba14648f0d5e44b
|
|
BLAKE2b-256 checksum How to use checksums |
80f0d1ad9ee4d5fffd79911765da9f7ecf5fd9eae181743dd22769b92ddf5a7e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|
Release files / paratro_sdk-1.9.0-py3-none-any.whl
| Download URL | paratro_sdk-1.9.0-py3-none-any.whl |
|---|---|
| Size | 39.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
41ba7a2ab0c51631da65e1e1dba8e189741ac79748e37b43448550ae079a0b4d
|
|
BLAKE2b-256 checksum How to use checksums |
e8e3a08bcbc7f1d95913872219edeb1cdf464ce60ea66dd09e4d74201dfe7c20
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|
Security for paratro-sdk 1.9.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.