dime-python-sdk 1.3.0__tar.gz → 1.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/PKG-INFO +141 -4
  2. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/README.md +139 -2
  3. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/pyproject.toml +1 -1
  4. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/client.py +10 -0
  5. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/config.py +1 -1
  6. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/__init__.py +27 -0
  7. dime_python_sdk-1.4.0/src/dime_payments/data_objects/application_status.py +38 -0
  8. dime_python_sdk-1.4.0/src/dime_payments/data_objects/chargeback.py +63 -0
  9. dime_python_sdk-1.4.0/src/dime_payments/data_objects/document.py +49 -0
  10. dime_python_sdk-1.4.0/src/dime_payments/data_objects/document_upload_result.py +42 -0
  11. dime_python_sdk-1.4.0/src/dime_payments/data_objects/fund_release.py +63 -0
  12. dime_python_sdk-1.4.0/src/dime_payments/data_objects/held_balance.py +50 -0
  13. dime_python_sdk-1.4.0/src/dime_payments/data_objects/releasable_transactions.py +57 -0
  14. dime_python_sdk-1.4.0/src/dime_payments/data_objects/subscribe_result.py +29 -0
  15. dime_python_sdk-1.4.0/src/dime_payments/data_objects/subscription.py +61 -0
  16. dime_python_sdk-1.4.0/src/dime_payments/data_objects/subscription_item.py +29 -0
  17. dime_python_sdk-1.4.0/src/dime_payments/data_objects/subscription_payment_method.py +31 -0
  18. dime_python_sdk-1.4.0/src/dime_payments/data_objects/subscription_plan.py +46 -0
  19. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/http/error_handler.py +18 -5
  20. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/http/transport.py +40 -2
  21. dime_python_sdk-1.4.0/src/dime_payments/resources/chargebacks.py +34 -0
  22. dime_python_sdk-1.4.0/src/dime_payments/resources/documents.py +81 -0
  23. dime_python_sdk-1.4.0/src/dime_payments/resources/funds.py +77 -0
  24. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/resources/invoices.py +8 -1
  25. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/resources/merchants.py +10 -0
  26. dime_python_sdk-1.4.0/src/dime_payments/resources/subscription_plans.py +97 -0
  27. dime_python_sdk-1.4.0/src/dime_payments/resources/subscriptions.py +53 -0
  28. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/resources/transactions.py +31 -0
  29. dime_python_sdk-1.4.0/tests/unit/test_chargebacks.py +79 -0
  30. dime_python_sdk-1.4.0/tests/unit/test_documents.py +144 -0
  31. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/tests/unit/test_error_handling.py +21 -0
  32. dime_python_sdk-1.4.0/tests/unit/test_funds.py +172 -0
  33. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/tests/unit/test_merchants.py +33 -0
  34. dime_python_sdk-1.4.0/tests/unit/test_subscription_plans.py +140 -0
  35. dime_python_sdk-1.4.0/tests/unit/test_subscriptions.py +101 -0
  36. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/tests/unit/test_transactions.py +38 -0
  37. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/.github/workflows/publish.yml +0 -0
  38. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/.gitignore +0 -0
  39. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/LICENSE +0 -0
  40. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/__init__.py +0 -0
  41. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/address.py +0 -0
  42. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/cover_fee_quote.py +0 -0
  43. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/customer.py +0 -0
  44. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/deposit.py +0 -0
  45. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/deposit_group.py +0 -0
  46. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/deposit_with_transactions.py +0 -0
  47. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/form_link.py +0 -0
  48. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/invoice.py +0 -0
  49. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/invoice_customer.py +0 -0
  50. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/invoice_event.py +0 -0
  51. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/invoice_item.py +0 -0
  52. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/invoice_link.py +0 -0
  53. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/invoice_payment.py +0 -0
  54. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/line_item.py +0 -0
  55. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/merchant.py +0 -0
  56. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/message_result.py +0 -0
  57. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/payment_method.py +0 -0
  58. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/recurring_invoice.py +0 -0
  59. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/recurring_payment.py +0 -0
  60. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/recurring_payment_method.py +0 -0
  61. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/tokenize_result.py +0 -0
  62. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/transaction.py +0 -0
  63. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/data_objects/transaction_address.py +0 -0
  64. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/exceptions/__init__.py +0 -0
  65. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/exceptions/api_exception.py +0 -0
  66. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/exceptions/authentication_exception.py +0 -0
  67. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/exceptions/connection_exception.py +0 -0
  68. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/exceptions/dime_exception.py +0 -0
  69. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/exceptions/not_found_exception.py +0 -0
  70. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/exceptions/permission_denied_exception.py +0 -0
  71. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/exceptions/rate_limit_exception.py +0 -0
  72. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/exceptions/server_exception.py +0 -0
  73. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/exceptions/validation_exception.py +0 -0
  74. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/http/__init__.py +0 -0
  75. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/pagination/__init__.py +0 -0
  76. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/pagination/cursor_page.py +0 -0
  77. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/resources/__init__.py +0 -0
  78. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/resources/abstract_resource.py +0 -0
  79. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/resources/addresses.py +0 -0
  80. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/resources/customers.py +0 -0
  81. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/resources/deposits.py +0 -0
  82. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/resources/payment_methods.py +0 -0
  83. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/resources/recurring_invoices.py +0 -0
  84. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/resources/recurring_payments.py +0 -0
  85. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/support/__init__.py +0 -0
  86. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/src/dime_payments/support/arr.py +0 -0
  87. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/tests/__init__.py +0 -0
  88. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/tests/helpers.py +0 -0
  89. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/tests/unit/__init__.py +0 -0
  90. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/tests/unit/test_addresses.py +0 -0
  91. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/tests/unit/test_customers.py +0 -0
  92. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/tests/unit/test_deposits.py +0 -0
  93. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/tests/unit/test_invoices.py +0 -0
  94. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/tests/unit/test_pagination.py +0 -0
  95. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/tests/unit/test_payment_methods.py +0 -0
  96. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/tests/unit/test_recurring_invoices.py +0 -0
  97. {dime_python_sdk-1.3.0 → dime_python_sdk-1.4.0}/tests/unit/test_recurring_payments.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: dime-python-sdk
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: Python client for the Dime Payments API
5
5
  Author: Dime Technology
6
6
  License: MIT License
@@ -111,15 +111,20 @@ returned as strings to avoid float rounding.
111
111
 
112
112
  | Property | Endpoints |
113
113
  | ------------------------------- | --------------------------------------------------------------------- |
114
- | `dime.transactions` | charge_card, charge_ach, tokenize_card, refund, void, show, list |
114
+ | `dime.transactions` | charge_card, charge_card_token, charge_ach, authorize, capture, tokenize_card, refund, void, show, list |
115
115
  | `dime.customers` | list, show, create, update, delete |
116
116
  | `dime.payment_methods` | list, show, create, update, delete |
117
- | `dime.merchants` | list, show, create, update, get_form_link |
117
+ | `dime.merchants` | list, show, create, update, get_form_link, application_status |
118
118
  | `dime.addresses` | list, show, create, update, delete |
119
119
  | `dime.deposits` | list, list_with_transactions, show |
120
120
  | `dime.recurring_payments` | list, show, create, edit, pause, cancel, activate, delete |
121
121
  | `dime.invoices` | list, show, create, update, delete, send, mark_sent, void, duplicate, pay, get_link, list_items, add_line_item, update_line_item, delete_line_item, create_item |
122
122
  | `dime.recurring_invoices` | list, show, create, cancel |
123
+ | `dime.chargebacks` | list, show |
124
+ | `dime.documents` | upload, list |
125
+ | `dime.funds` | balance, transactions, release |
126
+ | `dime.subscription_plans` | list, show, create, edit, delete, publish, archive, unarchive, subscribe |
127
+ | `dime.subscriptions` | list, show, pause, resume, cancel |
123
128
 
124
129
  ### Transactions
125
130
 
@@ -165,6 +170,35 @@ dime.transactions.void('000010', 'CC', 123456)
165
170
  txn = dime.transactions.show('000010', {'transaction_info_id': 123456})
166
171
  ```
167
172
 
173
+ #### Authorize now, capture later
174
+
175
+ `authorize()` holds the amount on the card without moving money. Pass its `transaction_number` to
176
+ `capture()` to collect, or to `void()` to release the hold. Capture promptly — the issuer drops an
177
+ uncaptured hold on its own schedule — and only once: a partial capture settles that amount and
178
+ releases the rest. Needs the `transaction:authorize-capture` ability.
179
+
180
+ ```python
181
+ auth = dime.transactions.authorize('000010', {'amount': '100.00', 'token': 'tok_abc123'})
182
+
183
+ # Collect it: omit the amount to capture the full hold
184
+ dime.transactions.capture('000010', auth.transaction_number, '80.00')
185
+
186
+ # ...or release the hold instead
187
+ dime.transactions.void('000010', 'CC', auth.transaction_number)
188
+ ```
189
+
190
+ ### Merchant onboarding
191
+
192
+ Follow up an application sent with `get_form_link()`. `status` is the headline; while it is
193
+ `underwriting`, read `application_status` — only `needs_documents` asks you to act. `boarded` says
194
+ whether the merchant can take money. The `application_status_changed` webhook carries the same
195
+ fields, so poll only to reconcile.
196
+
197
+ ```python
198
+ status = dime.merchants.application_status('000010')
199
+ print(status.status, status.application_status, status.boarded)
200
+ ```
201
+
168
202
  ### Customers, payment methods, addresses
169
203
 
170
204
  ```python
@@ -218,6 +252,11 @@ item (a fund or designation). Draft invoices can be edited; once sent they are l
218
252
  Identify the customer with `customer_uuid` — the same uuid every other resource uses, and the only
219
253
  identifier the customer endpoints return. `customer_id` is still accepted for older integrations.
220
254
 
255
+ **Statuses.** `invoice.status` is one of `draft`, `sent`, `viewed`, `partially_paid`, `paid`, `void` or
256
+ `refunded`. `paid` is not always final: if the customer's bank returns an ACH payment, the invoice is
257
+ reopened (back to `partially_paid`, `viewed` or `sent`, with `amount_paid` and `balance` updated) and an
258
+ `invoice_payment_returned` webhook fires. Re-read the invoice rather than caching a `paid` status forever.
259
+
221
260
  ```python
222
261
  # Look up (or create) the merchant items a line can reference
223
262
  items = dime.invoices.list_items('000010')
@@ -331,6 +370,101 @@ print(ri.next_run_date, ri.upcoming_run_dates)
331
370
  dime.recurring_invoices.cancel('000010', ri.id)
332
371
  ```
333
372
 
373
+ ### Chargebacks and documents
374
+
375
+ Chargebacks arrive from the processor once a day; the chargeback webhooks tell you when one opens or
376
+ changes, and these endpoints let you reconcile. Contest one by uploading evidence as a
377
+ `RetrievalRequest` document against its `transaction_info_id`.
378
+
379
+ ```python
380
+ for cb in dime.chargebacks.list('000010', {'representment_status': 'New'}).auto_paging():
381
+ print(cb.transaction_info_id, cb.chargeback_amount, cb.representment_date)
382
+
383
+ cb = dime.chargebacks.show('000010', '8675309')
384
+
385
+ # Each file is a path or a (filename, bytes-or-file-object) pair. Up to 10 per call,
386
+ # 9 MB each, as PDF, JPG, PNG, DOC, DOCX or RTF.
387
+ result = dime.documents.upload(
388
+ '000010',
389
+ 'RetrievalRequest', # Verification | FraudHolds | Underwriting | RetrievalRequest
390
+ ['receipt.pdf', ('signature.png', png_bytes)],
391
+ chargeback_transaction_info_id=cb.transaction_info_id,
392
+ )
393
+ for failure in result.failed: # files are stored independently; re-send only these
394
+ print(failure.file_name, failure.reason)
395
+
396
+ docs = dime.documents.list('000010', {'chargeback_transaction_info_id': cb.transaction_info_id})
397
+ ```
398
+
399
+ `upload()` is the one `multipart/form-data` request in the API; the SDK builds it for you. Uploading
400
+ does not forward anything to the processor — Dime reviews the documents and sends them on.
401
+
402
+ ### Held funds
403
+
404
+ For merchants on a tier that holds their balance rather than sweeping it to the bank. Any other
405
+ merchant gets a `ValidationException` (422). Reading needs `funds:read`; releasing needs
406
+ `funds:release` on an affiliate key.
407
+
408
+ ```python
409
+ balance = dime.funds.balance('000010')
410
+ print(balance.available, balance.releasable) # release against releasable
411
+
412
+ # Release by amount...
413
+ result = dime.funds.release('000010', 'payout-2026-10-06-0001', amount='1500.00')
414
+
415
+ # ...or by payment, all or nothing
416
+ payable = dime.funds.transactions('000010')
417
+ result = dime.funds.release(
418
+ '000010',
419
+ 'payout-2026-10-06-0002',
420
+ transaction_info_ids=[t.transaction_info_id for t in payable.transactions],
421
+ )
422
+
423
+ print(result.release.status) # released | failed | unknown
424
+ ```
425
+
426
+ Use a fresh `idempotency_key` for every intended release and reuse it when retrying: a repeat
427
+ returns the original release (`result.replayed`) instead of sending money twice. `unknown` means no
428
+ confirmation came back and it may have gone through; its amount stays out of `releasable` until it
429
+ is reconciled, so a later release cannot pay it twice. A release the processor declines is returned
430
+ with status `failed` and a `failure_reason`; nothing moved. A request refused before anything was
431
+ recorded raises: `ValidationException` (422) for more than is releasable or an ineligible payment,
432
+ `ApiException` (409) for a reused key or a release already in flight, and `ServerException` (503)
433
+ when the processor cannot be reached.
434
+
435
+ ### Subscription plans and subscriptions
436
+
437
+ A plan is a recurring offering built from merchant items. Create it as a draft, publish it, then
438
+ subscribe customers to it; each subscriber keeps their own snapshot, so later edits do not change
439
+ what they pay.
440
+
441
+ ```python
442
+ plan = dime.subscription_plans.create('000010', {
443
+ 'name': 'Monthly Membership',
444
+ 'recurrence_schedule': 'Monthly', # Weekly | Biweekly | FirstFifteenth | Monthly | Yearly
445
+ 'lines': [
446
+ {'item_id': item.id, 'name': 'Base membership', 'quantity': 1, 'unit_price': 25},
447
+ ],
448
+ })
449
+ dime.subscription_plans.publish('000010', plan.id)
450
+
451
+ # Charges the first payment now; a decline raises ValidationException
452
+ result = dime.subscription_plans.subscribe('000010', plan.id, customer.uuid, pm.id)
453
+
454
+ sub = dime.subscriptions.show('000010', result.subscription_id)
455
+ dime.subscriptions.pause('000010', sub.id, '2026-12-01') # omit the date to pause indefinitely
456
+ dime.subscriptions.resume('000010', sub.id)
457
+ dime.subscriptions.cancel('000010', sub.id)
458
+
459
+ # edit() replaces the plan wholesale, so send every field and line
460
+ dime.subscription_plans.edit('000010', plan.id, {
461
+ 'name': 'Monthly Membership',
462
+ 'recurrence_schedule': 'Monthly',
463
+ 'lines': [{'item_id': item.id, 'name': 'Base membership', 'quantity': 1, 'unit_price': 30}],
464
+ })
465
+ dime.subscription_plans.archive('000010', plan.id) # stop new sign-ups; unarchive() to reopen as a draft
466
+ ```
467
+
334
468
  ## Pagination
335
469
 
336
470
  List endpoints return a `CursorPage`. Iterate one page, walk pages manually, or stream every
@@ -391,6 +525,9 @@ except DimeException as e:
391
525
  | `ConnectionException` | No HTTP response (DNS, timeout, network error) |
392
526
  | `ApiException` | Any other non-2xx |
393
527
 
528
+ The exception message is the API's own where it sends one. Some list endpoints answer `404` when
529
+ nothing matches: an empty chargeback, document or subscription list raises `NotFoundException`.
530
+
394
531
  ## Notes
395
532
 
396
533
  - **GET requests carry a JSON body.** The Dime API expects read parameters in the request body
@@ -67,15 +67,20 @@ returned as strings to avoid float rounding.
67
67
 
68
68
  | Property | Endpoints |
69
69
  | ------------------------------- | --------------------------------------------------------------------- |
70
- | `dime.transactions` | charge_card, charge_ach, tokenize_card, refund, void, show, list |
70
+ | `dime.transactions` | charge_card, charge_card_token, charge_ach, authorize, capture, tokenize_card, refund, void, show, list |
71
71
  | `dime.customers` | list, show, create, update, delete |
72
72
  | `dime.payment_methods` | list, show, create, update, delete |
73
- | `dime.merchants` | list, show, create, update, get_form_link |
73
+ | `dime.merchants` | list, show, create, update, get_form_link, application_status |
74
74
  | `dime.addresses` | list, show, create, update, delete |
75
75
  | `dime.deposits` | list, list_with_transactions, show |
76
76
  | `dime.recurring_payments` | list, show, create, edit, pause, cancel, activate, delete |
77
77
  | `dime.invoices` | list, show, create, update, delete, send, mark_sent, void, duplicate, pay, get_link, list_items, add_line_item, update_line_item, delete_line_item, create_item |
78
78
  | `dime.recurring_invoices` | list, show, create, cancel |
79
+ | `dime.chargebacks` | list, show |
80
+ | `dime.documents` | upload, list |
81
+ | `dime.funds` | balance, transactions, release |
82
+ | `dime.subscription_plans` | list, show, create, edit, delete, publish, archive, unarchive, subscribe |
83
+ | `dime.subscriptions` | list, show, pause, resume, cancel |
79
84
 
80
85
  ### Transactions
81
86
 
@@ -121,6 +126,35 @@ dime.transactions.void('000010', 'CC', 123456)
121
126
  txn = dime.transactions.show('000010', {'transaction_info_id': 123456})
122
127
  ```
123
128
 
129
+ #### Authorize now, capture later
130
+
131
+ `authorize()` holds the amount on the card without moving money. Pass its `transaction_number` to
132
+ `capture()` to collect, or to `void()` to release the hold. Capture promptly — the issuer drops an
133
+ uncaptured hold on its own schedule — and only once: a partial capture settles that amount and
134
+ releases the rest. Needs the `transaction:authorize-capture` ability.
135
+
136
+ ```python
137
+ auth = dime.transactions.authorize('000010', {'amount': '100.00', 'token': 'tok_abc123'})
138
+
139
+ # Collect it: omit the amount to capture the full hold
140
+ dime.transactions.capture('000010', auth.transaction_number, '80.00')
141
+
142
+ # ...or release the hold instead
143
+ dime.transactions.void('000010', 'CC', auth.transaction_number)
144
+ ```
145
+
146
+ ### Merchant onboarding
147
+
148
+ Follow up an application sent with `get_form_link()`. `status` is the headline; while it is
149
+ `underwriting`, read `application_status` — only `needs_documents` asks you to act. `boarded` says
150
+ whether the merchant can take money. The `application_status_changed` webhook carries the same
151
+ fields, so poll only to reconcile.
152
+
153
+ ```python
154
+ status = dime.merchants.application_status('000010')
155
+ print(status.status, status.application_status, status.boarded)
156
+ ```
157
+
124
158
  ### Customers, payment methods, addresses
125
159
 
126
160
  ```python
@@ -174,6 +208,11 @@ item (a fund or designation). Draft invoices can be edited; once sent they are l
174
208
  Identify the customer with `customer_uuid` — the same uuid every other resource uses, and the only
175
209
  identifier the customer endpoints return. `customer_id` is still accepted for older integrations.
176
210
 
211
+ **Statuses.** `invoice.status` is one of `draft`, `sent`, `viewed`, `partially_paid`, `paid`, `void` or
212
+ `refunded`. `paid` is not always final: if the customer's bank returns an ACH payment, the invoice is
213
+ reopened (back to `partially_paid`, `viewed` or `sent`, with `amount_paid` and `balance` updated) and an
214
+ `invoice_payment_returned` webhook fires. Re-read the invoice rather than caching a `paid` status forever.
215
+
177
216
  ```python
178
217
  # Look up (or create) the merchant items a line can reference
179
218
  items = dime.invoices.list_items('000010')
@@ -287,6 +326,101 @@ print(ri.next_run_date, ri.upcoming_run_dates)
287
326
  dime.recurring_invoices.cancel('000010', ri.id)
288
327
  ```
289
328
 
329
+ ### Chargebacks and documents
330
+
331
+ Chargebacks arrive from the processor once a day; the chargeback webhooks tell you when one opens or
332
+ changes, and these endpoints let you reconcile. Contest one by uploading evidence as a
333
+ `RetrievalRequest` document against its `transaction_info_id`.
334
+
335
+ ```python
336
+ for cb in dime.chargebacks.list('000010', {'representment_status': 'New'}).auto_paging():
337
+ print(cb.transaction_info_id, cb.chargeback_amount, cb.representment_date)
338
+
339
+ cb = dime.chargebacks.show('000010', '8675309')
340
+
341
+ # Each file is a path or a (filename, bytes-or-file-object) pair. Up to 10 per call,
342
+ # 9 MB each, as PDF, JPG, PNG, DOC, DOCX or RTF.
343
+ result = dime.documents.upload(
344
+ '000010',
345
+ 'RetrievalRequest', # Verification | FraudHolds | Underwriting | RetrievalRequest
346
+ ['receipt.pdf', ('signature.png', png_bytes)],
347
+ chargeback_transaction_info_id=cb.transaction_info_id,
348
+ )
349
+ for failure in result.failed: # files are stored independently; re-send only these
350
+ print(failure.file_name, failure.reason)
351
+
352
+ docs = dime.documents.list('000010', {'chargeback_transaction_info_id': cb.transaction_info_id})
353
+ ```
354
+
355
+ `upload()` is the one `multipart/form-data` request in the API; the SDK builds it for you. Uploading
356
+ does not forward anything to the processor — Dime reviews the documents and sends them on.
357
+
358
+ ### Held funds
359
+
360
+ For merchants on a tier that holds their balance rather than sweeping it to the bank. Any other
361
+ merchant gets a `ValidationException` (422). Reading needs `funds:read`; releasing needs
362
+ `funds:release` on an affiliate key.
363
+
364
+ ```python
365
+ balance = dime.funds.balance('000010')
366
+ print(balance.available, balance.releasable) # release against releasable
367
+
368
+ # Release by amount...
369
+ result = dime.funds.release('000010', 'payout-2026-10-06-0001', amount='1500.00')
370
+
371
+ # ...or by payment, all or nothing
372
+ payable = dime.funds.transactions('000010')
373
+ result = dime.funds.release(
374
+ '000010',
375
+ 'payout-2026-10-06-0002',
376
+ transaction_info_ids=[t.transaction_info_id for t in payable.transactions],
377
+ )
378
+
379
+ print(result.release.status) # released | failed | unknown
380
+ ```
381
+
382
+ Use a fresh `idempotency_key` for every intended release and reuse it when retrying: a repeat
383
+ returns the original release (`result.replayed`) instead of sending money twice. `unknown` means no
384
+ confirmation came back and it may have gone through; its amount stays out of `releasable` until it
385
+ is reconciled, so a later release cannot pay it twice. A release the processor declines is returned
386
+ with status `failed` and a `failure_reason`; nothing moved. A request refused before anything was
387
+ recorded raises: `ValidationException` (422) for more than is releasable or an ineligible payment,
388
+ `ApiException` (409) for a reused key or a release already in flight, and `ServerException` (503)
389
+ when the processor cannot be reached.
390
+
391
+ ### Subscription plans and subscriptions
392
+
393
+ A plan is a recurring offering built from merchant items. Create it as a draft, publish it, then
394
+ subscribe customers to it; each subscriber keeps their own snapshot, so later edits do not change
395
+ what they pay.
396
+
397
+ ```python
398
+ plan = dime.subscription_plans.create('000010', {
399
+ 'name': 'Monthly Membership',
400
+ 'recurrence_schedule': 'Monthly', # Weekly | Biweekly | FirstFifteenth | Monthly | Yearly
401
+ 'lines': [
402
+ {'item_id': item.id, 'name': 'Base membership', 'quantity': 1, 'unit_price': 25},
403
+ ],
404
+ })
405
+ dime.subscription_plans.publish('000010', plan.id)
406
+
407
+ # Charges the first payment now; a decline raises ValidationException
408
+ result = dime.subscription_plans.subscribe('000010', plan.id, customer.uuid, pm.id)
409
+
410
+ sub = dime.subscriptions.show('000010', result.subscription_id)
411
+ dime.subscriptions.pause('000010', sub.id, '2026-12-01') # omit the date to pause indefinitely
412
+ dime.subscriptions.resume('000010', sub.id)
413
+ dime.subscriptions.cancel('000010', sub.id)
414
+
415
+ # edit() replaces the plan wholesale, so send every field and line
416
+ dime.subscription_plans.edit('000010', plan.id, {
417
+ 'name': 'Monthly Membership',
418
+ 'recurrence_schedule': 'Monthly',
419
+ 'lines': [{'item_id': item.id, 'name': 'Base membership', 'quantity': 1, 'unit_price': 30}],
420
+ })
421
+ dime.subscription_plans.archive('000010', plan.id) # stop new sign-ups; unarchive() to reopen as a draft
422
+ ```
423
+
290
424
  ## Pagination
291
425
 
292
426
  List endpoints return a `CursorPage`. Iterate one page, walk pages manually, or stream every
@@ -347,6 +481,9 @@ except DimeException as e:
347
481
  | `ConnectionException` | No HTTP response (DNS, timeout, network error) |
348
482
  | `ApiException` | Any other non-2xx |
349
483
 
484
+ The exception message is the API's own where it sends one. Some list endpoints answer `404` when
485
+ nothing matches: an empty chargeback, document or subscription list raises `NotFoundException`.
486
+
350
487
  ## Notes
351
488
 
352
489
  - **GET requests carry a JSON body.** The Dime API expects read parameters in the request body
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dime-python-sdk"
7
- version = "1.3.0"
7
+ version = "1.4.0"
8
8
  description = "Python client for the Dime Payments API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,13 +1,18 @@
1
1
  from .config import Config
2
2
  from .http.transport import Transport
3
3
  from .resources.addresses import Addresses
4
+ from .resources.chargebacks import Chargebacks
4
5
  from .resources.customers import Customers
5
6
  from .resources.deposits import Deposits
7
+ from .resources.documents import Documents
8
+ from .resources.funds import Funds
6
9
  from .resources.invoices import Invoices
7
10
  from .resources.merchants import Merchants
8
11
  from .resources.payment_methods import PaymentMethods
9
12
  from .resources.recurring_invoices import RecurringInvoices
10
13
  from .resources.recurring_payments import RecurringPayments
14
+ from .resources.subscription_plans import SubscriptionPlans
15
+ from .resources.subscriptions import Subscriptions
11
16
  from .resources.transactions import Transactions
12
17
 
13
18
 
@@ -29,6 +34,11 @@ class Client:
29
34
  self.recurring_payments = RecurringPayments(transport)
30
35
  self.invoices = Invoices(transport)
31
36
  self.recurring_invoices = RecurringInvoices(transport)
37
+ self.chargebacks = Chargebacks(transport)
38
+ self.documents = Documents(transport)
39
+ self.funds = Funds(transport)
40
+ self.subscription_plans = SubscriptionPlans(transport)
41
+ self.subscriptions = Subscriptions(transport)
32
42
 
33
43
  def config(self) -> Config:
34
44
  return self._config
@@ -8,7 +8,7 @@ if TYPE_CHECKING:
8
8
 
9
9
  class Config:
10
10
  DEFAULT_BASE_URL = 'https://app.dimepayments.com'
11
- VERSION = '1.3.0'
11
+ VERSION = '1.4.0'
12
12
 
13
13
  def __init__(
14
14
  self,
@@ -1,9 +1,15 @@
1
1
  from .address import Address
2
+ from .application_status import ApplicationStatus
3
+ from .chargeback import Chargeback
2
4
  from .customer import Customer
3
5
  from .deposit import Deposit
4
6
  from .deposit_group import DepositGroup
5
7
  from .deposit_with_transactions import DepositWithTransactions
8
+ from .document import Document
9
+ from .document_upload_result import DocumentUploadFailure, DocumentUploadResult
6
10
  from .form_link import FormLink
11
+ from .fund_release import FundRelease, FundReleaseResult
12
+ from .held_balance import HeldBalance
7
13
  from .invoice import Invoice
8
14
  from .invoice_customer import InvoiceCustomer
9
15
  from .invoice_event import InvoiceEvent
@@ -17,6 +23,12 @@ from .payment_method import PaymentMethod
17
23
  from .recurring_invoice import RecurringInvoice, RecurringInvoiceRun
18
24
  from .recurring_payment import RecurringPayment
19
25
  from .recurring_payment_method import RecurringPaymentMethod
26
+ from .releasable_transactions import ReleasableTransaction, ReleasableTransactions
27
+ from .subscribe_result import SubscribeResult
28
+ from .subscription import Subscription
29
+ from .subscription_item import SubscriptionItem
30
+ from .subscription_payment_method import SubscriptionPaymentMethod
31
+ from .subscription_plan import SubscriptionPlan
20
32
  from .tokenize_result import TokenizeResult
21
33
  from .transaction import Transaction
22
34
  from .transaction_address import TransactionAddress
@@ -27,6 +39,7 @@ __all__ = [
27
39
  'Customer',
28
40
  'PaymentMethod',
29
41
  'Merchant',
42
+ 'ApplicationStatus',
30
43
  'Address',
31
44
  'Deposit',
32
45
  'DepositGroup',
@@ -45,4 +58,18 @@ __all__ = [
45
58
  'LineItem',
46
59
  'RecurringInvoice',
47
60
  'RecurringInvoiceRun',
61
+ 'Chargeback',
62
+ 'Document',
63
+ 'DocumentUploadResult',
64
+ 'DocumentUploadFailure',
65
+ 'HeldBalance',
66
+ 'ReleasableTransaction',
67
+ 'ReleasableTransactions',
68
+ 'FundRelease',
69
+ 'FundReleaseResult',
70
+ 'SubscriptionPlan',
71
+ 'Subscription',
72
+ 'SubscriptionItem',
73
+ 'SubscriptionPaymentMethod',
74
+ 'SubscribeResult',
48
75
  ]
@@ -0,0 +1,38 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any
3
+
4
+ from ..support.arr import arr_bool, arr_string
5
+
6
+
7
+ @dataclass
8
+ class ApplicationStatus:
9
+ """
10
+ Where a merchant sits in onboarding.
11
+
12
+ ``status`` is the headline: one of ``lead``, ``discovery``, ``proposal``,
13
+ ``application_in_progress``, ``underwriting``, ``live``,
14
+ ``cancellation_pending``, ``churned`` or ``declined`` (``None`` if onboarding
15
+ has not started). ``application_status`` is the underlying application —
16
+ ``draft``, ``pending_review``, ``submitted``, ``approved``,
17
+ ``needs_documents``, ``failed`` or ``None`` — and is the field to read while
18
+ ``status`` is ``underwriting``, since only ``needs_documents`` asks you to
19
+ act. ``boarded`` is the ground truth for whether the merchant can take money.
20
+ """
21
+
22
+ sid: str | None = None
23
+ name: str | None = None
24
+ status: str | None = None
25
+ application_status: str | None = None
26
+ boarded: bool = False
27
+ application_submitted_at: str | None = None
28
+
29
+ @classmethod
30
+ def from_dict(cls, data: dict[str, Any]) -> 'ApplicationStatus':
31
+ return cls(
32
+ sid=arr_string(data, 'sid'),
33
+ name=arr_string(data, 'name'),
34
+ status=arr_string(data, 'status'),
35
+ application_status=arr_string(data, 'application_status'),
36
+ boarded=arr_bool(data, 'boarded'),
37
+ application_submitted_at=arr_string(data, 'application_submitted_at'),
38
+ )
@@ -0,0 +1,63 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any
3
+
4
+ from ..support.arr import arr_bool, arr_int, arr_string
5
+
6
+
7
+ @dataclass
8
+ class Chargeback:
9
+ """
10
+ A chargeback raised against a merchant. The same field set the chargeback
11
+ webhooks carry.
12
+
13
+ ``transaction_info_id`` is the processor's stable identifier for the
14
+ chargeback; ``parent_transaction_info_id`` identifies the disputed payment.
15
+ ``representment_status`` and ``result`` are free text from the processor;
16
+ ``resolved`` says whether the dispute has reached a terminal state.
17
+ """
18
+
19
+ transaction_info_id: str | None = None
20
+ parent_transaction_info_id: str | None = None
21
+ gateway_transaction_id: str | None = None
22
+ transaction_number: str | None = None
23
+ invoice_number: str | None = None
24
+ chargeback_date: str | None = None
25
+ merchant_chargeback_date: str | None = None
26
+ transaction_amount: str | None = None
27
+ chargeback_amount: str | None = None
28
+ card_brand: str | None = None
29
+ cc_last_four: str | None = None
30
+ payee_name: str | None = None
31
+ days_to_represent: int | None = None
32
+ representment_date: str | None = None
33
+ merchant_representment_date: str | None = None
34
+ representment_status: str | None = None
35
+ result: str | None = None
36
+ chargeback_code: str | None = None
37
+ chargeback_response_code: str | None = None
38
+ resolved: bool = False
39
+
40
+ @classmethod
41
+ def from_dict(cls, data: dict[str, Any]) -> 'Chargeback':
42
+ return cls(
43
+ transaction_info_id=arr_string(data, 'transaction_info_id'),
44
+ parent_transaction_info_id=arr_string(data, 'parent_transaction_info_id'),
45
+ gateway_transaction_id=arr_string(data, 'gateway_transaction_id'),
46
+ transaction_number=arr_string(data, 'transaction_number'),
47
+ invoice_number=arr_string(data, 'invoice_number'),
48
+ chargeback_date=arr_string(data, 'chargeback_date'),
49
+ merchant_chargeback_date=arr_string(data, 'merchant_chargeback_date'),
50
+ transaction_amount=arr_string(data, 'transaction_amount'),
51
+ chargeback_amount=arr_string(data, 'chargeback_amount'),
52
+ card_brand=arr_string(data, 'card_brand'),
53
+ cc_last_four=arr_string(data, 'cc_last_four'),
54
+ payee_name=arr_string(data, 'payee_name'),
55
+ days_to_represent=arr_int(data, 'days_to_represent'),
56
+ representment_date=arr_string(data, 'representment_date'),
57
+ merchant_representment_date=arr_string(data, 'merchant_representment_date'),
58
+ representment_status=arr_string(data, 'representment_status'),
59
+ result=arr_string(data, 'result'),
60
+ chargeback_code=arr_string(data, 'chargeback_code'),
61
+ chargeback_response_code=arr_string(data, 'chargeback_response_code'),
62
+ resolved=arr_bool(data, 'resolved'),
63
+ )
@@ -0,0 +1,49 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any
3
+
4
+ from ..support.arr import arr_int, arr_string
5
+
6
+
7
+ @dataclass
8
+ class Document:
9
+ """
10
+ A document held for a merchant.
11
+
12
+ ``doc_type`` is one of ``Verification``, ``FraudHolds``, ``Underwriting`` or
13
+ ``RetrievalRequest``. ``uploaded_via`` is ``api`` for documents sent through
14
+ :meth:`Documents.upload`. The upload response carries only ``uuid``,
15
+ ``file_name``, ``doc_type`` and ``size``; the rest are filled by
16
+ :meth:`Documents.list`.
17
+
18
+ ``sent_to_processor_at`` and ``processor_status`` record when our team
19
+ forwarded the document to the processor and what it answered. Both are
20
+ ``None`` until it has been forwarded.
21
+ """
22
+
23
+ uuid: str | None = None
24
+ file_name: str | None = None
25
+ doc_type: str | None = None
26
+ chargeback_transaction_info_id: str | None = None
27
+ size: int | None = None
28
+ uploaded_at: str | None = None
29
+ uploaded_via: str | None = None
30
+ sent_to_processor_at: str | None = None
31
+ processor_status: str | None = None
32
+
33
+ @classmethod
34
+ def from_dict(cls, data: dict[str, Any]) -> 'Document':
35
+ # The API reports forwarding as a [timestamp, processor status] pair.
36
+ sent = data.get('sent_to_processor')
37
+ pair = dict(zip(('at', 'status'), sent)) if isinstance(sent, list) else {'at': sent}
38
+
39
+ return cls(
40
+ uuid=arr_string(data, 'uuid'),
41
+ file_name=arr_string(data, 'file_name'),
42
+ doc_type=arr_string(data, 'doc_type'),
43
+ chargeback_transaction_info_id=arr_string(data, 'chargeback_transaction_info_id'),
44
+ size=arr_int(data, 'size'),
45
+ uploaded_at=arr_string(data, 'uploaded_at'),
46
+ uploaded_via=arr_string(data, 'uploaded_via'),
47
+ sent_to_processor_at=arr_string(pair, 'at'),
48
+ processor_status=arr_string(pair, 'status'),
49
+ )
@@ -0,0 +1,42 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any
3
+
4
+ from ..support.arr import arr_array, arr_string
5
+ from .document import Document
6
+
7
+
8
+ @dataclass
9
+ class DocumentUploadFailure:
10
+ """A file from an upload that could not be stored. Re-send just this one."""
11
+
12
+ file_name: str | None = None
13
+ reason: str | None = None
14
+
15
+ @classmethod
16
+ def from_dict(cls, data: dict[str, Any]) -> 'DocumentUploadFailure':
17
+ return cls(
18
+ file_name=arr_string(data, 'file_name'),
19
+ reason=arr_string(data, 'reason'),
20
+ )
21
+
22
+
23
+ @dataclass
24
+ class DocumentUploadResult:
25
+ """
26
+ The outcome of :meth:`Documents.upload`.
27
+
28
+ Files are stored independently, so an upload can partly succeed: ``documents``
29
+ lists what was stored and ``failed`` what was not.
30
+ """
31
+
32
+ message: str | None = None
33
+ documents: list[Document] = field(default_factory=list)
34
+ failed: list[DocumentUploadFailure] = field(default_factory=list)
35
+
36
+ @classmethod
37
+ def from_dict(cls, data: dict[str, Any]) -> 'DocumentUploadResult':
38
+ return cls(
39
+ message=arr_string(data, 'message'),
40
+ documents=[Document.from_dict(d) for d in arr_array(data, 'documents') if isinstance(d, dict)],
41
+ failed=[DocumentUploadFailure.from_dict(f) for f in arr_array(data, 'failed') if isinstance(f, dict)],
42
+ )