Skip to main content

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, record_sequence, issue_test_invoice, issue_invoice_key, checkout_policy.retrieve, checkout_policy.update, certification.retrieve, certification.start, certification.submit_evidence, certification.upload_test_set, certification.retrieve_test_set
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() takes the business's one certificate and password, names no mode, and returns MerchantBusiness; Timbro uses the certificate in every environment the business holds, including DGII certification. Timbro's protected Portal handoff is the recommended place for a person to upload the certificate and password. Never send either through an agent conversation.

Upload the DGII certification workbook

After DGII provides the e-CF test workbook, upload its original bytes and filename. The file must be between 1 byte and 3 MiB. The API retains it for review; uploading does not submit fiscal documents, run workbook cases, or advance the certification process.

from pathlib import Path
from timbro_payments import UploadCertificationTestSetCommand

workbook_upload: UploadCertificationTestSetCommand = {
    "kind": "ecf",
    "file": {
        "name": "DGII.xlsx",
        "bytes": Path("DGII.xlsx").read_bytes(),
    },
}

uploaded = timbro.business.certification.upload_test_set(
    workbook_upload,
    idempotency_key="merchant-onboarding-123-dgii-ecf-workbook-v1",
)
test_set_snapshot = timbro.business.certification.retrieve_test_set()
if test_set_snapshot["current"] is not None:
    print(test_set_snapshot["current"]["review"]["status"])

The initial review is review_required; case_mapping_pending means the workbook still needs its cases mapped. Keep the idempotency key with the uploaded bytes and filename. If the result is unknown, retry with that same key and exactly the same filename and bytes. Use a new key when uploading a corrected replacement. AsyncTimbroPayments exposes the same methods; await each call.

Record the e-NCF sequences DGII granted

Live invoicing numbers each e-CF from a sequence DGII granted the business in its Oficina Virtual. Test sequences are automatic. Record each live sequence with the numbers and expiry DGII shows:

business = timbro.business.record_sequence(
    {
        "documentType": "E31",
        "firstNumber": 1,
        "lastNumber": 500,
        "expiresOn": "2027-12-31",
    }
)
live = business["invoicing"]["live"]
if live["status"] == "available":
    e31 = next(
        (entry for entry in live["documentTypes"] or [] if entry["type"] == "E31"), None
    )

nextNumber defaults to firstNumber; send it when another system already used numbers from the sequence. expiresOn is required except for E32 and E34, which never expire and take no date. Recording the same first and last number again, or a sequence DGII has not confirmed, replaces it; a sequence DGII has confirmed cannot be overlapped. Timbro stores the sequence as recorded and DGII's answers confirm it, so numbering.confirmation reads not_yet_confirmed until DGII accepts an e-CF numbered from it. The answer is the business: documentTypes shows the type ready or the reason it is not, and sequences lists what is recorded. Only an owner or administrator key records or reads sequences. AsyncTimbroPayments exposes the same method; await it.

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. declined is gone from the status values: a declined attempt returns the Payment to requires_customer_action and sets lastDecline (code, message, attemptedAt), which replaces decline and drops canTryAgain. Only succeeded, canceled, and expired end a Payment. Subscribe to payment.declined to hear about each declined attempt.
  • invoicing.deliveryEmail on creation is now invoicing.recipientEmail, and the Payment reports it back.
  • Creation accepts expiresAfterSeconds (300 to 86400) to set the checkout window.
  • A succeeded Payment carries refundable; read refundable.automaticAmount before 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.14.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for timbro-payments 0.14.0
File Size Uploaded
timbro_payments-0.14.0.tar.gz 72.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for timbro-payments 0.14.0
File Interpreter ABI Platform
timbro_payments-0.14.0-py3-none-any.whl Python 3 none any Details

Total release size: 133.8 kB

Release files / timbro_payments-0.14.0.tar.gz

Download URL timbro_payments-0.14.0.tar.gz
Size 72.4 kB
Tags Source
SHA-256 checksum
How to use checksums
9cb6a4f6be6e433776b101bbc4211c6fe8b972edc261f4fefe3db6894e2d7322
BLAKE2b-256 checksum
How to use checksums
10e4a441e379c3bc13ab52e85a1582001111cb07526ee56e5f3f2633dd3f005d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.4

Release files / timbro_payments-0.14.0-py3-none-any.whl

Download URL timbro_payments-0.14.0-py3-none-any.whl
Size 61.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9a0260395dfe42119d3ce9a4fad9faa4b0fc1523a66ed9120dd7b7887691fecc
BLAKE2b-256 checksum
How to use checksums
a0e31802075c1a04e04ca8fc8273093e18bf4f3057d89168420ecdec72028218
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.4

Release history Release notifications | RSS feed

0.20.0

2 release files

0.19.1

2 release files

0.19.0

2 release files

0.18.0

2 release files

0.16.0

1 release file

This release

0.14.0 This release

2 release files

0.13.0

1 release file

0.12.0

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page