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.
fromdecisaimportDecisa, cents# Read the key from the environment — never hard-code it.withDecisa(api_key="dcs_ak_your_key_here") asdecisa:
result=decisa.conversions.record(
type="sale",
external_id="order_1001", # your order id — the idempotency keyvalue_cents=cents(19.99), # -> 1999; the wire field is integer centscurrency="BRL",
customer_email="buyer@example.com", # hashed server-side, raw never storedmethod="pix",
)
print(result.id) # "conv_..."print(result.attributed) # True / Falseprint(result.data) # typed conversion dictprint(result.raw) # full { data, meta, error } envelopevalue_cents is integer cents only — passing a float like 19.99 raises a
DecisaErrorbefore 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).
fromdecisaimportDecisawithDecisa(api_key="dcs_ak_your_key_here") asdecisa:
result=decisa.events.record(
event_id="evt_checkout_98f3a1c2", # 8..64 chars — the idempotency keyevent_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:
importasynciofromdecisaimportAsyncDecisa, centsasyncdefmain() ->None:
asyncwithAsyncDecisa(api_key="dcs_ak_your_key_here") asdecisa:
result=awaitdecisa.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:
fromdecisaimport (
Decisa,
DecisaError,
DecisaValidationError,
DecisaAuthError,
DecisaForbiddenError,
DecisaRateLimitError,
DecisaServerError,
)
try:
withDecisa(api_key="dcs_ak_your_key_here") asdecisa:
decisa.conversions.record(type="sale", external_id="order_1001")
exceptDecisaValidationErroraserr: # HTTP 422print(err.fields) # { "currency": ["The currency must be 3 characters."] }exceptDecisaAuthErroraserr: # HTTP 401 — bad/missing/revoked keyprint(err.code) # "API_KEY_INVALID"exceptDecisaForbiddenErroraserr: # 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 attributionprint(err.code)
exceptDecisaRateLimitErroraserr: # HTTP 429print(err.retry_after) # seconds from the Retry-After header, or NoneexceptDecisaServerErroraserr: # HTTP 5xx (already retried)print(err.status_code)
exceptDecisaErroraserr: # anything else, plus input guardsprint(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.
fromdecisaimportCONVERSION_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.