Official Python SDK for the Decisa server-side ingest API — record conversions and events from your backend with typed inputs, a built-in money guard, automatic retries, and typed errors.
Server-side only. The API key is a secret. Never put it in client-side code, a mobile app, or a browser bundle — it belongs in your backend environment variables.
pip install decisaRequires Python 3.9+. The only runtime dependency is
httpx.
from decisa import Decisa, cents
# Read the key from the environment — never hard-code it.
with Decisa(api_key="dcs_ak_your_key_here") as decisa:
result = decisa.conversions.record(
type="sale",
external_id="order_1001", # your order id — the idempotency key
value_cents=cents(19.99), # -> 1999; the wire field is integer cents
currency="BRL",
customer_email="buyer@example.com", # hashed server-side, raw never stored
method="pix",
)
print(result.id) # "conv_..."
print(result.attributed) # True / False
print(result.data) # typed conversion dict
print(result.raw) # full { data, meta, error } envelopevalue_cents is integer cents only — passing a float like 19.99 raises a
DecisaError before anything is sent. Use the cents() helper to convert a
decimal amount safely (it rounds half-up and is immune to binary-float
artefacts, so cents(19.99) == 1999).
from decisa import Decisa
with Decisa(api_key="dcs_ak_your_key_here") as decisa:
result = decisa.events.record(
event_id="evt_checkout_98f3a1c2", # 8..64 chars — the idempotency key
event_name="Purchase",
value_cents=1999,
currency="BRL",
# pixel_keys accepts a list; it's joined to a comma-separated string
# on the wire automatically (this differs from conversions).
pixel_keys=["px_main", "px_secondary"],
url="https://shop.example.com/thank-you",
)
print(result.status) # "recorded"
print(result.event_name) # "Purchase"AsyncDecisa mirrors the sync client exactly — same methods, same arguments,
same results — only await-ed:
import asyncio
from decisa import AsyncDecisa, cents
async def main() -> None:
async with AsyncDecisa(api_key="dcs_ak_your_key_here") as decisa:
result = await decisa.conversions.record(
type="sale",
external_id="order_2002",
value_cents=cents(49.90),
currency="USD",
)
print(result.id)
asyncio.run(main())Every failure maps to a typed exception under the DecisaError base class, so
you can handle each case precisely:
from decisa import (
Decisa,
DecisaError,
DecisaValidationError,
DecisaAuthError,
DecisaForbiddenError,
DecisaRateLimitError,
DecisaServerError,
)
try:
with Decisa(api_key="dcs_ak_your_key_here") as decisa:
decisa.conversions.record(type="sale", external_id="order_1001")
except DecisaValidationError as err: # HTTP 422
print(err.fields) # { "currency": ["The currency must be 3 characters."] }
except DecisaAuthError as err: # HTTP 401 — bad/missing/revoked key
print(err.code) # "API_KEY_INVALID"
except DecisaForbiddenError as err: # HTTP 403
# err.code distinguishes the two 403 cases:
# "INSUFFICIENT_SCOPE" -> the key lacks conversions:write / events:write
# "ENTITLEMENT_MISSING" -> the workspace hasn't enabled attribution
print(err.code)
except DecisaRateLimitError as err: # HTTP 429
print(err.retry_after) # seconds from the Retry-After header, or None
except DecisaServerError as err: # HTTP 5xx (already retried)
print(err.status_code)
except DecisaError as err: # anything else, plus input guards
print(err.message)Every error carries status_code, code, message, and request_id (read
from X-Request-Id / X-Decisa-Request-Id, falling back to meta.request_id)
to make support tickets easy. The API key is never included in any error
message or in repr() of the client.
These deterministic mistakes are caught instantly, with no network round-trip:
value_centsis not a realint(afloat,bool, or decimal is rejected — usecents()),external_id/event_idmissing or empty,type/event_namenot in the allowed set,- both
pixel_keyandpixel_keysset (they are mutually exclusive).
Transient failures are retried automatically with exponential backoff and full jitter:
- Retries only on network errors,
5xx, and429. - Never retries other
4xx— those are deterministic client errors. - Honors the
Retry-Afterheader on429. - Defaults:
max_retries=2,timeout=10.0seconds. Tune both per client:
Decisa(api_key="dcs_ak_...", timeout=5.0, max_retries=4)Re-sending a record with the same external_id (conversions) or event_id
(events) is safe. The server dedupes on it, returns the stored record, and
still responds 201 / 202 — a duplicate is not an error. This is what
makes retries safe; you can replay a failed batch without double-counting.
from decisa import CONVERSION_TYPES, EVENT_NAMES
# CONVERSION_TYPES:
# signup, trial_start, subscription_start, sale, lead, app_install, custom, refund
# EVENT_NAMES:
# PageView, ViewContent, Search, AddToCart, AddPaymentInfo, InitiateCheckout,
# Lead, CompleteRegistration, Purchase, StartTrial, Subscribe, AppInstall, CustomConversionType and EventName are exported as typing.Literal unions for
static checking; ConversionInput / EventInput are TypedDicts you can build
and pass positionally (decisa.conversions.record({...})) or spread as keyword
arguments.
MIT — see LICENSE.