codecov[bot]: # [Codecov](https://codecov.io/gh/cuenca-mx/cuenca-python/pull/13?src=pr&el=h1) Report
> Merging [#13](https://codecov.io/gh/cuenca-mx/cuenca-python/pull/13?src=pr&el=desc) into [master](https://codecov.io/gh/cuenca-mx/cuenca-python/commit/fe958c099a4e1a121f022aa4b581511f601d95ad&el=desc) will **not change** coverage.
> The diff coverage is `100.00%`.


```diff
@@            Coverage Diff            @@
##            master       #13   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            9         9           
  Lines          238       244    +6     
=========================================
+ Hits           238       244    +6     
```

| Flag | Coverage Δ | |
|---|---|---|
| #unittests | `100.00% <100.00%> (ø)` | |

| [Impacted Files](https://codecov.io/gh/cuenca-mx/cuenca-python/pull/13?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [cuenca/resources/transfers.py](https://codecov.io/gh/cuenca-mx/cuenca-python/pull/13/diff?src=pr&el=tree#diff-Y3VlbmNhL3Jlc291cmNlcy90cmFuc2ZlcnMucHk=) | `100.00% <100.00%> (ø)` | |
| [cuenca/validators.py](https://codecov.io/gh/cuenca-mx/cuenca-python/pull/13/diff?src=pr&el=tree#diff-Y3VlbmNhL3ZhbGlkYXRvcnMucHk=) | `100.00% <100.00%> (ø)` | |
| [cuenca/version.py](https://codecov.io/gh/cuenca-mx/cuenca-python/pull/13/diff?src=pr&el=tree#diff-Y3VlbmNhL3ZlcnNpb24ucHk=) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/cuenca-mx/cuenca-python/pull/13?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/cuenca-mx/cuenca-python/pull/13?src=pr&el=footer). Last update [fe958c0...18bad51](https://codecov.io/gh/cuenca-mx/cuenca-python/pull/13?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

diff --git a/Makefile b/Makefile
index 5f3d646..d206683 100644
--- a/Makefile
+++ b/Makefile
@@ -2,8 +2,8 @@ SHELL := bash
 PATH := ./venv/bin:${PATH}
 PYTHON = python3.7
 PROJECT = cuenca
-isort = isort -rc -ac $(PROJECT) tests setup.py
-black = black -S -l 79 --target-version py38 $(PROJECT) tests setup.py
+isort = isort -rc -ac $(PROJECT) tests setup.py examples
+black = black -S -l 79 --target-version py38 $(PROJECT) tests setup.py examples
 
 
 all: test
diff --git a/README.md b/README.md
index 618da0c..7a9f676 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,23 @@
 [![codecov](https://codecov.io/gh/cuenca-mx/cuenca-python/branch/master/graph/badge.svg)](https://codecov.io/gh/cuenca-mx/cuenca-python)
 [![PyPI](https://img.shields.io/pypi/v/cuenca.svg)](https://pypi.org/project/cuenca/)
 
+# Installation
+
+`pip install cuenca`
+
+# Authentication
+
+The preferred way to configure the credentials for the client is to set the
+`CUENCA_API_KEY` and `CUENCA_API_SECRET` environment variables. The client
+library will automatically configure based on the values of those variables.
+
+To configure manually:
+```python
+import cuenca
+
+cuenca.configure(api_key='PKxxxx', api_secret='yyyyyy')
+```
+
 ## Transfers
 
 ### Create transfer
@@ -17,7 +34,7 @@ local_transfer_id = '078efdc20bab456285437309c4b75673'
 
 transfer = cuenca.Transfer.create(
     recipient_name='Benito Juárez',
-    account_number='646180157042875763',
+    account_number='646180157042875763',  # CLABE or card number
     amount=12345,  # Mx$123.45
     descriptor='sending money',  # As it'll appear for the customer
     idempotency_key=local_transfer_id
diff --git a/cuenca/resources/transfers.py b/cuenca/resources/transfers.py
index 87bbe30..ba2c5a9 100644
--- a/cuenca/resources/transfers.py
+++ b/cuenca/resources/transfers.py
@@ -1,18 +1,18 @@
 import datetime as dt
-from typing import ClassVar, Optional, cast
+from typing import ClassVar, List, Optional, Union, cast
 
 from clabe import Clabe
 from pydantic import BaseModel, StrictStr
 from pydantic.dataclasses import dataclass
 
 from ..types import Network, Status
-from ..validators import StrictPositiveInt, TransferQuery
+from ..validators import PaymentCardNumber, StrictPositiveInt, TransferQuery
 from .base import Creatable, Queryable, Retrievable
 
 
 class TransferRequest(BaseModel):
     recipient_name: StrictStr
-    account_number: Clabe
+    account_number: Union[Clabe, PaymentCardNumber]
     amount: StrictPositiveInt  # in centavos
     descriptor: StrictStr  # how it'll appear for the recipient
     idempotency_key: str  # must be unique for each transfer
@@ -72,6 +72,12 @@ class Transfer(Creatable, Queryable, Retrievable):
         )
         return cast('Transfer', cls._create(**req.dict()))
 
+    @classmethod
+    def create_many(cls, requests: List[TransferRequest]) -> List['Transfer']:
+        return [
+            cast('Transfer', cls._create(**req.dict())) for req in requests
+        ]
+
     @staticmethod
     def _gen_idempotency_key(account_number: str, amount: int) -> str:
         """
diff --git a/cuenca/validators.py b/cuenca/validators.py
index 782af4f..f888335 100644
--- a/cuenca/validators.py
+++ b/cuenca/validators.py
@@ -1,8 +1,11 @@
 import datetime as dt
-from typing import Optional
+from typing import ClassVar, Optional
 
 from pydantic import BaseModel, Extra, PositiveInt, StrictInt
-from pydantic.types import ConstrainedInt
+from pydantic.types import (
+    ConstrainedInt,
+    PaymentCardNumber as PydanticPaymentCardNumber,
+)
 
 from .types import sanitize_dict
 from .typing import DictStrAny
@@ -48,3 +51,8 @@ class TransferQuery(QueryParams):
 
 class ApiKeyQuery(QueryParams):
     active: Optional[bool] = None
+
+
+class PaymentCardNumber(PydanticPaymentCardNumber):
+    min_length: ClassVar[int] = 16
+    max_length: ClassVar[int] = 16
diff --git a/cuenca/version.py b/cuenca/version.py
index bf18f21..3e495be 100644
--- a/cuenca/version.py
+++ b/cuenca/version.py
@@ -1,3 +1,3 @@
-__version__ = '0.1.4'
+__version__ = '0.1.5'
 CLIENT_VERSION = __version__
 API_VERSION = '2020-03-19'
diff --git a/examples/batch_transfers.py b/examples/batch_transfers.py
new file mode 100644
index 0000000..1d6d0a9
--- /dev/null
+++ b/examples/batch_transfers.py
@@ -0,0 +1,31 @@
+import argparse
+import csv
+from dataclasses import fields
+
+from cuenca.resources.transfers import Transfer, TransferRequest
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description='Process batch transfers file'
+    )
+    parser.add_argument(
+        'input', type=str, help='Path to CSV batch transfers file',
+    )
+    parser.add_argument('output', type=str, help='Path to CSV output file')
+    args = parser.parse_args()
+    with open(args.input) as f:
+        reader = csv.DictReader(f)
+        # offline validation of transfers
+        transfer_requests = [TransferRequest(**line) for line in reader]
+    transfers = Transfer.create_many(transfer_requests)
+    with open(args.output, 'w') as f:
+        fieldnames = [field.name for field in fields(Transfer)]
+        writer = csv.DictWriter(f, fieldnames)
+        writer.writeheader()
+        for tr in transfers:
+            writer.writerow(tr.to_dict())
+
+
+if __name__ == '__main__':
+    main()

add support for transfers using a card number
The API currently only supports a CLABE for transfers. We should also support card numbers (without having to specify the financial institution).

Here's the branch: https://github.com/cuenca-mx/cuenca-python/tree/payment_card
diff --git a/tests/resources/cassettes/test_transfers_create_many.yaml b/tests/resources/cassettes/test_transfers_create_many.yaml
new file mode 100644
index 0000000..5e63787
--- /dev/null
+++ b/tests/resources/cassettes/test_transfers_create_many.yaml
@@ -0,0 +1,108 @@
+interactions:
+- request:
+    body: '{"recipient_name": "Rogelio Lopez", "account_number": "646180157034181180",
+      "amount": 10000, "descriptor": "Mi primer transferencia", "idempotency_key":
+      "081e71c19f8640048090b7cd740205b1"}'
+    headers:
+      Accept:
+      - '*/*'
+      Accept-Encoding:
+      - gzip, deflate
+      Authorization:
+      - DUMMY
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '188'
+      Content-Type:
+      - application/json
+      User-Agent:
+      - cuenca-python/0.1.5.rc0
+      X-Cuenca-Api-Version:
+      - '2020-03-19'
+    method: POST
+    uri: https://sandbox.cuenca.com/transfers
+  response:
+    body:
+      string: '{"id":"TRK2s-yr17S_iAdawNSW0Rjw==","created_at":"2020-05-28T16:22:48.909272","updated_at":"2020-05-28T16:22:50.170473","account_number":"646180157034181180","recipient_name":"Rogelio
+        Lopez","amount":10000,"descriptor":"Mi primer transferencia","idempotency_key":"081e71c19f8640048090b7cd740205b1","status":"submitted","network":"internal","tracking_key":null}'
+    headers:
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '359'
+      Content-Type:
+      - application/json
+      Date:
+      - Thu, 28 May 2020 16:22:50 GMT
+      Via:
+      - 1.1 c90de501a57b4f0090a033690602dc10.cloudfront.net (CloudFront)
+      X-Amz-Cf-Id:
+      - SkktvJVvta9Xgv5LTOvIgxWFKRpjZ3wvwkF5Uwx995yaTdHJZ-R86A==
+      X-Amz-Cf-Pop:
+      - DFW55-C2
+      X-Amzn-Trace-Id:
+      - Root=1-5ecfe555-0835695eb1f7c70c0991840e;Sampled=0
+      X-Cache:
+      - Miss from cloudfront
+      x-amz-apigw-id:
+      - NQDFUFjdoAMFeUw=
+      x-amzn-RequestId:
+      - 4ca5296b-be89-4818-a675-49d66a45d667
+    status:
+      code: 201
+      message: Created
+- request:
+    body: '{"recipient_name": "Rogelio Lopez", "account_number": "646180157034181180",
+      "amount": 10001, "descriptor": "Mi primer transferencia", "idempotency_key":
+      "64850742e0c14bc1b93e4b762f072592"}'
+    headers:
+      Accept:
+      - '*/*'
+      Accept-Encoding:
+      - gzip, deflate
+      Authorization:
+      - DUMMY
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '188'
+      Content-Type:
+      - application/json
+      User-Agent:
+      - cuenca-python/0.1.5.rc0
+      X-Cuenca-Api-Version:
+      - '2020-03-19'
+    method: POST
+    uri: https://sandbox.cuenca.com/transfers
+  response:
+    body:
+      string: '{"id":"TRnaWQjdgQRs2CrrwQnTPfFg==","created_at":"2020-05-28T16:22:50.351720","updated_at":"2020-05-28T16:22:50.674677","account_number":"646180157034181180","recipient_name":"Rogelio
+        Lopez","amount":10001,"descriptor":"Mi primer transferencia","idempotency_key":"64850742e0c14bc1b93e4b762f072592","status":"submitted","network":"internal","tracking_key":null}'
+    headers:
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '359'
+      Content-Type:
+      - application/json
+      Date:
+      - Thu, 28 May 2020 16:22:50 GMT
+      Via:
+      - 1.1 c90de501a57b4f0090a033690602dc10.cloudfront.net (CloudFront)
+      X-Amz-Cf-Id:
+      - Zqd0hwjKpKizNqgSz_JjguVJlvQq-K0PTSH7dqu-2bnQxv8b-UWCYQ==
+      X-Amz-Cf-Pop:
+      - DFW55-C2
+      X-Amzn-Trace-Id:
+      - Root=1-5ecfe55a-2457598049a260809ab5b300;Sampled=0
+      X-Cache:
+      - Miss from cloudfront
+      x-amz-apigw-id:
+      - NQDGIGiQIAMFsxw=
+      x-amzn-RequestId:
+      - b32aceaa-c62f-446d-a984-09d997426ae1
+    status:
+      code: 201
+      message: Created
+version: 1
diff --git a/tests/resources/test_transfers.py b/tests/resources/test_transfers.py
index 6ed600c..9a9077f 100644
--- a/tests/resources/test_transfers.py
+++ b/tests/resources/test_transfers.py
@@ -3,6 +3,7 @@ from pydantic import ValidationError
 
 from cuenca import Transfer
 from cuenca.exc import MultipleResultsFound, NoResultFound
+from cuenca.resources.transfers import TransferRequest
 from cuenca.types import Network, Status
 
 
@@ -18,10 +19,37 @@ def test_transfers_create():
     assert transfer.idempotency_key is not None
     assert transfer.status
     assert transfer.status == Status.submitted
-    # Some seconds latter
+    assert transfer.network == Network.internal
+    # Some seconds later
     transfer.refresh()
     assert transfer.status == Status.succeeded
-    assert transfer.network == Network.internal
+
+
+@pytest.mark.vcr
+def test_transfers_create_many():
+    transfer_requests = [
+        TransferRequest(
+            account_number='646180157034181180',
+            amount=10000,
+            descriptor='Mi primer transferencia',
+            recipient_name='Rogelio Lopez',
+            idempotency_key='081e71c19f8640048090b7cd740205b1',
+        ),
+        TransferRequest(
+            account_number='646180157034181180',
+            amount=10001,
+            descriptor='Mi primer transferencia',
+            recipient_name='Rogelio Lopez',
+            idempotency_key='64850742e0c14bc1b93e4b762f072592',
+        ),
+    ]
+    transfers = Transfer.create_many(transfer_requests)
+    for transfer in transfers:
+        assert transfer.id is not None
+        assert transfer.idempotency_key is not None
+        assert transfer.status
+        assert transfer.status == Status.submitted
+        assert transfer.network == Network.internal
 
 
 @pytest.mark.vcr

pip install -e .[test]
parse_log_pytest
pytest
pytest
3.7
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs @ file:///croot/attrs_1668696182826/work
black==23.3.0
certifi @ file:///croot/certifi_1671487769961/work/certifi
chardet==3.0.4
clabe==1.0.3
click==8.1.8
coverage==4.5.4
-e git+https://github.com/cuenca-mx/cuenca-python.git@fe958c099a4e1a121f022aa4b581511f601d95ad#egg=cuenca
flake8==5.0.4
flit_core @ file:///opt/conda/conda-bld/flit-core_1644941570762/work/source/flit_core
idna==2.8
importlib-metadata==4.2.0
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
isort==5.11.5
mccabe==0.7.0
multidict==6.0.5
mypy==1.4.1
mypy-extensions==1.0.0
packaging @ file:///croot/packaging_1671697413597/work
pathspec==0.11.2
platformdirs==4.0.0
pluggy @ file:///tmp/build/80754af9/pluggy_1648042572264/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pycodestyle==2.9.1
pydantic==1.5.1
pyflakes==2.5.0
pytest==7.1.2
pytest-cov==2.10.1
pytest-vcr==1.0.2
PyYAML==6.0.1
requests==2.21.0
six==1.17.0
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
typed-ast==1.5.5
typing_extensions==4.7.1
urllib3==1.24.3
vcrpy==4.4.0
wrapt==1.16.0
yarl==1.9.4
zipp @ file:///croot/zipp_1672387121353/work

name: cuenca-python
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=22.1.0=py37h06a4308_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - flit-core=3.6.0=pyhd3eb1b0_0
  - importlib_metadata=4.11.3=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=22.0=py37h06a4308_0
  - pip=22.3.1=py37h06a4308_0
  - pluggy=1.0.0=py37h06a4308_1
  - py=1.11.0=pyhd3eb1b0_0
  - pytest=7.1.2=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py37h06a4308_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.11.0=py37h06a4308_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - black==23.3.0
      - chardet==3.0.4
      - clabe==1.0.3
      - click==8.1.8
      - coverage==4.5.4
      - flake8==5.0.4
      - idna==2.8
      - importlib-metadata==4.2.0
      - isort==5.11.5
      - mccabe==0.7.0
      - multidict==6.0.5
      - mypy==1.4.1
      - mypy-extensions==1.0.0
      - pathspec==0.11.2
      - platformdirs==4.0.0
      - pycodestyle==2.9.1
      - pydantic==1.5.1
      - pyflakes==2.5.0
      - pytest-cov==2.10.1
      - pytest-vcr==1.0.2
      - pyyaml==6.0.1
      - requests==2.21.0
      - six==1.17.0
      - typed-ast==1.5.5
      - typing-extensions==4.7.1
      - urllib3==1.24.3
      - vcrpy==4.4.0
      - wrapt==1.16.0
      - yarl==1.9.4
prefix: /opt/conda/envs/cuenca-python

tests/resources/test_transfers.py::test_transfers_create_many
tests/resources/test_transfers.py::test_transfers_create tests/resources/test_transfers.py::test_transfers_retrieve tests/resources/test_transfers.py::test_transfers_one tests/resources/test_transfers.py::test_transfers_one_errors tests/resources/test_transfers.py::test_transfers_first tests/resources/test_transfers.py::test_transfers_all tests/resources/test_transfers.py::test_transfers_count tests/resources/test_transfers.py::test_transfers_count_vs_all tests/resources/test_transfers.py::test_invalid_params
Makefile examples/batch_transfers.py cuenca/resources/transfers.py cuenca/validators.py README.md cuenca/version.py
Makefile examples/batch_transfers.py cuenca/resources/transfers.py cuenca/validators.py README.md cuenca/version.py