timbro-payments
The Python SDK for Timbro Payments: create payments and refunds, read merchant accounts, prepare business certification, manage webhook endpoints, and verify signed webhooks. It needs Python 3.11 or newer and depends only on httpx and timbro-protocol.
pip install timbro-payments
Take a payment
from timbro_payments import TimbroPayments, payment_reference_for_source
with TimbroPayments(api_key=secret_key, application="my-erp/1.0") as timbro:
payment = timbro.payments.create({
"paymentReference": payment_reference_for_source({"id": "order-123", "version": "1"}),
"amount": {"currency": "DOP", "value": "1250.00"},
"description": "Servicio",
"tax": {"includedAmount": "190.68"},
})
redirect_to(payment["checkout"]["url"])
payment = timbro.payments.retrieve(payment["id"])
for refund in timbro.refunds.list_all(payment_id=payment["id"]):
...
Requests and responses are plain dictionaries with the API's camelCase keys, typed as TypedDicts in timbro_payments.types. Money is a decimal string. Fields and enum values added by a later API version pass through unchanged. A declined attempt is not an error and does not end the Payment: it stays requires_customer_action with lastDecline, and each declined attempt sends payment.declined.
AsyncTimbroPayments has the same namespaces: await each call and use async for with list_all.
timbro.mode is "test" for an sk_test_ key, "live" for an sk_live_ key, and None for any other key form, so an integration can show which mode it is configured for without a call.
| Namespace | Methods |
|---|---|
payments |
create, retrieve, list, list_all, cancel, create_invoice, attach_invoice, update_invoice_choice, update_invoice_recipient |
refunds |
create, retrieve, list, list_all, attach_invoice |
merchant_accounts |
retrieve, list, list_all |
webhook_endpoints |
create, retrieve, list, list_all, delete, suspend, reactivate, rotate_secret, list_deliveries, list_all_deliveries, replay |
business |
retrieve, update, configure_invoicing, import_signing_certificate, issue_test_invoice, issue_invoice_key, checkout_policy.retrieve, checkout_policy.update, certification.retrieve, certification.start, certification.submit_evidence |
merchant_onboarding |
verify_rnc |
api_keys |
rotate |
Path identifiers are positional, the request body is the positional request, and filters and options are keyword-only.
Prepare a business for DGII certification
Certification is scoped to the business selected by the API credential. Read its available paths, start the recommended one, then resume the same process by reading it again. Persist each command's idempotency key before sending it and reuse that key only when retrying the same command.
current = timbro.business.certification.retrieve()
if current["status"] == "not_started":
current = timbro.business.certification.start(
{"path": "external_software"},
idempotency_key="merchant-onboarding-123-certification-start",
)
if current["status"] == "in_progress":
if current["requirements"]:
for requirement in current["requirements"]:
print(f"Complete the {requirement['owner']} requirement: {requirement['code']}")
else:
step = next(
(
step
for step in current["steps"]
if step["number"] == 1 and step["status"] == "action_required"
),
None,
)
if step is not None:
print(f"Next step {step['number']}: {step['title']} ({step['actor']})")
# The authorized person completes this action in DGII before reporting it to Timbro.
After the authorized person completes a DGII step, report its real result with
timbro.business.certification.submit_evidence(), using the current revision, the step number,
and the same persisted idempotency key if retrying. The result records the person's report; even an
approved outcome remains user-reported and does not qualify the business for DGII production.
Legal documents and DGII portal actions stay with the authorized person.
business.import_signing_certificate() returns a
MerchantBusinessCertification when its request uses purpose="certification"; requests with
mode="test" or mode="live" return MerchantBusiness. Timbro's protected Portal handoff is the
recommended place for a person to upload the certification certificate and password. Never send
either through an agent conversation.
Retries and idempotency
The client retries at most twice (max_retries=2) after 408, 429, 500, 502, 503, 504, and connection failures. It waits 0.1 seconds, then 0.2, and honors a Retry-After of up to two seconds; a longer Retry-After ends the call with retry_after_seconds set. Reads and other operations the contract marks safe always retry. Other writes retry only with an idempotency key.
payments.create, payments.cancel, payments.create_invoice, payments.attach_invoice, refunds.create, and refunds.attach_invoice derive their idempotency key from the payment reference, the payment, the refund reference, or the attached Invoice and its payment or refund, so repeating a call is safe. Other writes that need a key take idempotency_key= and require it. Derive that key from your durable record of the command, such as the order line it serves, and send the same key on every retry and replay. Never generate a random key per attempt: an ERP that replays a transaction would then run the command twice. An explicit key always wins.
Timeouts
timeout (30 seconds by default) limits each phase of one attempt separately: waiting for a pooled connection, connecting, writing, and each wait for response bytes. When Timbro stops answering, an attempt ends after timeout, and a call makes at most max_retries + 1 attempts with up to two seconds between them. A stalled call therefore lasts about (max_retries + 1) * timeout plus four seconds: about 94 seconds with the defaults. Slow connects and writes can add their own phase limits on top. There is no overall deadline. A worker bound to a request, such as an Odoo worker with a 120-second limit, should pass smaller values, for example TimbroPayments(api_key=key, timeout=10, max_retries=1), which bounds a call near 22 seconds. A call can also pass timeout= to override the default for that call.
httpx logs each request URL, including the references in a list filter, at INFO on the httpx logger. Raise that logger's level to keep them out of your logs:
import logging
logging.getLogger("httpx").setLevel(logging.WARNING)
With your own http_client, that client's timeout applies unless a call passes timeout=. httpx finds CA certificates through SSL_CERT_FILE and SSL_CERT_DIR; it does not read REQUESTS_CA_BUNDLE. A host behind TLS inspection passes an http_client built with its CA bundle, such as httpx.Client(verify=ssl_context).
Errors
Every error is a TimbroError with code, message, request_id, operation, method, retryable, outcome_unknown, recovery, and recovery_method. operation and recovery are contract operation IDs, such as createPayment; method and recovery_method are the SDK methods that send them, such as payments.create and payments.list. The text never contains the API key, a request body, a header, or a Checkout URL. Errors can be pickled, so a job queue can carry them.
timbro-payments and timbro-invoices raise the same classes from timbro-protocol, so one except TimbroError handles both. To treat one package's calls differently, branch on method or operation:
from timbro_payments import TimbroError
try:
timbro.payments.cancel(payment["id"])
except TimbroError as error:
if error.method == "payments.cancel" and error.code == "payment_cancel_not_allowed":
payment = timbro.payments.retrieve(payment["id"])
else:
raise
| Error | When |
|---|---|
InvalidRequestError |
400 or 422; problems lists each field at fault |
AuthenticationError |
401 |
PermissionDeniedError |
403, such as a key without the needed grant |
NotFoundError |
404 |
ConflictError |
409 |
RateLimitError |
429; see retry_after_seconds |
ServerError |
5xx |
APIError |
Any other failure status, and the base of the classes above |
APIConnectionError, APITimeoutError |
No response arrived; code says what that means, below |
UnexpectedResponseError |
A response outside the published contract, including a redirect, or one that names another resource than the one a read or an action addressed (response_identity_mismatch) |
When no response arrives, code separates three cases:
code |
Meaning |
|---|---|
connection_failed |
The request never left, or a read broke off. Nothing changed at Timbro. |
request_timed_out |
A read timed out. Nothing changed at Timbro. |
outcome_unknown |
A write may have reached Timbro. outcome_unknown is true. |
@timbro/payments reports all three as transport_failure with status 0, and does not say whether a write may have run.
When outcome_unknown is true, a write may have taken effect even though the call failed: an earlier attempt or the last one reached Timbro and got no final answer. The error text says what to do next. Repeat the call with the same idempotency key, or find the result with the recovery method. payments.create recovers with payments.list(payment_reference=...) and refunds.create with refunds.list(payment_id=..., refund_reference=...), so a lost create is found without its ID.
Routes outside the published contract
Prefer the typed methods. For a route the published contract does not include yet, such as a Stamp installation's own readiness read, request sends one call through the same client, with the same API key, user agent, redirect refusal, 10 MiB limit, and errors:
readiness = timbro.request("GET", "/v1/stamp/readiness")
path must be a /v1/ path; an absolute URL, a query or fragment in the path, and an empty or dot segment raise ValueError, and query= carries the filters. The answer is the decoded JSON object, or None for an empty one, and nothing checks its shape. A GET is retried when safe; any other method is retried only when you pass idempotency_key=. An error it raises has method "request", operation set to the method and path, such as "GET /v1/stamp/readiness", and no recovery. AsyncTimbroPayments clients have the same method to await.
Webhooks
Verification needs no API key:
from timbro_payments import webhooks
event = webhooks.verify(body=raw_body, headers=request_headers, secret=webhook_secret)
inbox.store(event) # Deduplicate by event["id"], then acknowledge.
if webhooks.is_known_event(event) and event["type"] == "payment.succeeded":
payment = timbro.payments.retrieve(event["data"]["id"])
Pass the raw body bytes and every secret that is valid during a rotation. Events of a type this version does not know verify and return as UnknownWebhookEvent. A failure raises WebhookVerificationError, whose reason names the check.
Testing
timbro_payments.testing builds schema-valid resources and signs webhook fixtures. testing.payment(status) and testing.refund(status) fill the members each status carries: a succeeded Payment has capturedPayment allocations that sum to totalAmount and no nextAction, testing.payment(declined=True) carries the lastDecline of a declined attempt, and a declined Refund has its decline. A create must answer with the identity it sent, so a fake create passes it back as an override. testing.api_error(status, code, method="payments.cancel") builds the APIError subclass that method raises for that status. Fake the API by giving the client an httpx.MockTransport:
import json
import httpx
from timbro_payments import TimbroPayments, testing
def handler(request: httpx.Request) -> httpx.Response:
sent = json.loads(request.content)
payment = testing.payment("succeeded", paymentReference=sent["paymentReference"])
return httpx.Response(201, json=payment)
timbro = TimbroPayments(
api_key="sk_test_123",
http_client=httpx.Client(transport=httpx.MockTransport(handler)),
)
body = b'{"id":"event_...","type":"payment.succeeded"}'
headers = testing.sign_webhook(body, "whsec_...", webhook_id="event_...")
Migrating from 0.10
0.11 is a breaking minor for every Timbro SDK, so upgrade timbro-payments, timbro-invoices, and timbro-protocol together.
- A decline no longer ends a Payment.
declinedis gone from the status values: a declined attempt returns the Payment torequires_customer_actionand setslastDecline(code,message,attemptedAt), which replacesdeclineand dropscanTryAgain. Onlysucceeded,canceled, andexpiredend a Payment. Subscribe topayment.declinedto hear about each declined attempt. invoicing.deliveryEmailon creation is nowinvoicing.recipientEmail, and the Payment reports it back.- Creation accepts
expiresAfterSeconds(300 to 86400) to set the checkout window. - A succeeded Payment carries
refundable; readrefundable.automaticAmountbefore creating a Refund to know whether it runs without an operator.
License
No license has been granted for this package yet. The license will be declared before the first public release.
Metadata
Release files for timbro-payments 0.12.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| timbro_payments-0.12.0-py3-none-any.whl | Python 3 | none | any | Details |
Release files / timbro_payments-0.12.0-py3-none-any.whl
| Download URL | timbro_payments-0.12.0-py3-none-any.whl |
|---|---|
| Size | 55.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f559a7da924421e55c22a2bafee384ed62e4794e00dd82386b733c25958888ab
|
|
BLAKE2b-256 checksum How to use checksums |
026519a3b51141fada57e01cf1f9d3e39c7d78b7834400e955e12954abfdfa4d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.4
|