├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── Linter.yml │ └── deployment.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── chapa ├── __init__.py ├── api.py └── webhook.py ├── docs.md ├── examples ├── initiate_transaction.py └── verify_transaction.py ├── pyproject.toml ├── requirements.txt └── setup.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/Linter.yml: -------------------------------------------------------------------------------- 1 | # Github action for Linters 2 | name: Linter 3 | 4 | on: 5 | - push 6 | - pull_request 7 | 8 | # write python test workflow for pylint 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ["3.8", "3.9", "3.10", "3.11"] 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v3 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install ruff 25 | pip install -r requirements.txt 26 | 27 | - name: Analysing the code with pylint 28 | run: | 29 | ruff check 30 | -------------------------------------------------------------------------------- /.github/workflows/deployment.yml: -------------------------------------------------------------------------------- 1 | name: Deployment 2 | 3 | # deploy package to pypi on condition of 4 | # push to main branch 5 | # and release published 6 | 7 | on: 8 | release: 9 | types: 10 | - published 11 | 12 | jobs: 13 | Publish-Package: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | 19 | - name: Set up Python 3.10 20 | uses: actions/setup-python@v1 21 | with: 22 | python-version: "3.10" 23 | 24 | - name: Version the package 25 | run: | 26 | echo "__version__ = '${{ github.ref_name }}'" > chapa/__init__.py 27 | export CHAPA_VERSION=${{ github.ref_name }} 28 | 29 | - name: Install pypa/build 30 | run: | 31 | python -m pip install build 32 | python -m pip install wheel twine setuptools 33 | - name: Build a binary wheel and a source tarball 34 | run: python -m build 35 | 36 | - name: Publish distribution 📦 to PyPI 37 | run: python -m twine upload dist/* --username __token__ --password ${{ secrets.PYPI_API_TOKEN }} 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | downloads/ 14 | eggs/ 15 | .eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | dist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # vscode editor 132 | .vscode/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Chapa 2 | 3 | 👍🎉 First off, thanks for taking the time to contribute! 👍🎉 4 | 5 | ## Table of Contents 6 | 7 | - [Table of Contents](#table-of-contents) 8 | - [Introduction](#introduction) 9 | - [Reporting Bugs](#reporting-bugs) 10 | - [How to report bugs](#how-to-report-bugs) 11 | - [Style Guide](#style-guide) 12 | - [Git Commit Messages](#git-commit-messages) 13 | - [Python Style Guide](#python-style-guide) 14 | - [Documentation](#documentation) 15 | - [Contributing](#contributing) 16 | 17 | ## Introduction 18 | 19 | First of all, we are happy you are to contribute to Chapa. We are always looking for new features and improvements. If you have any suggestions, please let us know. We will be happy to hear from you. Fork the project on [Github](https://github.com/chapimenge3/chapa) and open an issue or a pull request. 20 | 21 | ## Reporting Bugs 22 | 23 | ### How to report bugs 24 | 25 | This section is for reporting bugs. Please make sure to include the following: 26 | 27 | - Describe the exact steps to reproduce the bug 28 | - Describe the expected behavior 29 | - Describe the actual behavior 30 | - Describe the parameter data that you are passing to the API 31 | - Use a clear and descriptive title 32 | - Add bug tag to the issue 33 | 34 | ## Style Guide 35 | 36 | So in this section we will see the style guide for the project. 37 | 38 | ### Git Commit Messages 39 | 40 | For Git commit conventions, please follow the [Git Commit Conventions](https://www.conventionalcommits.org) and follow the [Github Linking PR to Issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) 41 | 42 | ### Python Style Guide 43 | 44 | For Python Style Guide, please follow the [PEP8](https://www.python.org/dev/peps/pep-0008/) and [Pylint](https://pypi.org/project/pylint/) 45 | 46 | ### Documentation 47 | 48 | For documentation, please make sure your documentation is clear and concise. Provide a detailed description of the function and its parameters. Also, please make sure that your documentation is consistent with the code. 49 | 50 | Give examples of how to use the function. Put the expected behavior in the description. 51 | 52 | You can make it interactive by adding emojis💯, GIFs, videos and etc. 53 | 54 | ### Contributing 55 | 56 | Creating PR is the best way to contribute to the project. The PR should be clear and concise. The Linting Should be passed. The PR should be well written. We have prepared a PR template for you. Please follow the template. 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Temkin Mengsitu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Issue 2 | 3 | Title: Description of Issue 4 | 5 | ## Description 6 | 7 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 8 | 9 | ## Type of change 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | - [ ] Update Documentation(Updating Readme or any other Typos) 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chapa 2 | 3 | [![Linter](https://github.com/chapimenge3/chapa/actions/workflows/Linter.yml/badge.svg)](https://github.com/chapimenge3/chapa/actions/workflows/Linter.yml) 4 | [![Version](https://img.shields.io/static/v1?label=version&message=0.0.1&color=green)](https://travis-ci.com/chapimenge3/chapa) 5 | [![Build](https://github.com/chapimenge3/chapa/actions/workflows/Linter.yml/badge.svg)](https://travis-ci.com/chapimenge3/chapa) 6 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://choosealicense.com/licenses/mit) 7 | 8 | Unofficial Python SDK for [Chapa API](https://developer.chapa.co/docs). 9 | 10 | ## Introduction 11 | 12 | This document provides a comprehensive guide to integrating and using the Chapa Payment Gateway SDK in your application. Chapa is a powerful payment gateway that supports various payment methods, facilitating seamless transactions for businesses. This SDK simplifies interaction with Chapa’s API, enabling operations such as initiating payments, verifying transactions, and managing subaccounts. 13 | 14 | ## Installation 15 | 16 | To use the Chapa SDK in your project, you need to install it using pip, as it is a dependency for making HTTP requests it will also install `httpx` as a dependency. 17 | 18 | ```bash 19 | pip install chapa 20 | ``` 21 | 22 | ## Usage 23 | 24 | To begin using the SDK, import the `Chapa` class from the module and instantiate it with your secret key. 25 | 26 | ### Initializing the SDK 27 | 28 | ```python 29 | from chapa import Chapa 30 | 31 | # Replace 'your_secret_key' with your actual Chapa secret key 32 | chapa = Chapa('your_secret_key') 33 | ``` 34 | 35 | ### Async Support 36 | 37 | The Chapa SDK implements async support using the `AsyncChapa` class. To use the async version of the SDK, import the `AsyncChapa` class from the module and instantiate it with your secret key. 38 | 39 | ```python 40 | from chapa import AsyncChapa 41 | 42 | # Replace 'your_secret_key' with your actual Chapa secret key 43 | chapa = AsyncChapa('your_secret') 44 | ``` 45 | 46 | All of the below methods are available in the async version of the SDK. So you can just use it as you would use the sync version. 47 | 48 | ```python 49 | response = await chapa.initialize( 50 | ... 51 | ) 52 | ``` 53 | 54 | ### Making Payments 55 | 56 | To initiate a payment, use the `initialize` method. This method requires a set of parameters like the customer's email, amount, first name, last name, and a transaction reference. 57 | 58 | ```python 59 | response = chapa.initialize( 60 | email="customer@example.com", 61 | amount=1000, 62 | first_name="John", 63 | last_name="Doe", 64 | tx_ref="your_unique_transaction_reference", 65 | callback_url="https://yourcallback.url/here" 66 | ) 67 | print(response) 68 | ``` 69 | 70 | ### Verifying Payments 71 | 72 | After initiating a payment, you can verify the transaction status using the `verify` method. 73 | 74 | ```python 75 | transaction_id = "your_transaction_id" 76 | verification_response = chapa.verify(transaction_id) 77 | print(verification_response) 78 | ``` 79 | 80 | ### Creating Subaccounts 81 | 82 | You can create subaccounts for split payments using the `create_subaccount` method. 83 | 84 | ```python 85 | subaccount_response = chapa.create_subaccount( 86 | business_name="My Business", 87 | account_name="My Business Account", 88 | bank_code="12345", 89 | account_number="0012345678", 90 | split_value="0.2", 91 | split_type="percentage" 92 | ) 93 | print(subaccount_response) 94 | ``` 95 | 96 | ### Bank Transfers 97 | 98 | To initiate a bank transfer, use the `transfer_to_bank` method. 99 | 100 | ```python 101 | transfer_response = chapa.transfer_to_bank( 102 | account_name="Recipient Name", 103 | account_number="0987654321", 104 | amount="500", 105 | reference="your_transfer_reference", 106 | bank_code="67890", 107 | currency="ETB" 108 | ) 109 | print(transfer_response) 110 | ``` 111 | 112 | ### Verifying Webhook 113 | 114 | The reason for verifying a webhook is to ensure that the request is coming from Chapa. You can verify a webhook using the `verify_webhook` method. 115 | 116 | ```python 117 | from chapa import verify_webhook 118 | 119 | # request is just an example of a request object 120 | # request.body is the request body 121 | # request.headers.get("Chapa-Signature") is the Chapa-Signature header 122 | 123 | verify_webhook( 124 | secret_key="your_secret_key", 125 | body=request.body, 126 | chapa_signature=request.headers.get("Chapa-Signature") 127 | ) 128 | ``` 129 | 130 | ### Getting Testing Cards and Mobile Numbers 131 | 132 | For testing purposes, you can retrieve a set of test cards and mobile numbers. 133 | 134 | ```python 135 | from chapa import get_testing_cards, get_testing_mobile 136 | 137 | # Get a list of testing cards 138 | test_cards = get_testing_cards() 139 | print(test_cards) 140 | 141 | # Get a list of testing mobile numbers 142 | test_mobiles = get_testing_mobile() 143 | print(test_mobiles) 144 | ``` 145 | 146 | ### Get Webhook Events 147 | 148 | You can get webhook events details with description like below 149 | 150 | ```python 151 | from chapa import WEBHOOK_EVENTS, WEBHOOKS_EVENT_DESCRIPTION 152 | 153 | # Get a list of webhook events 154 | print(WEBHOOK_EVENTS) 155 | 156 | # Get a list of webhook events with description 157 | print(WEBHOOKS_EVENT_DESCRIPTION) 158 | ``` 159 | 160 | ## Conclusion 161 | 162 | The Chapa Payment Gateway SDK is a flexible tool that allows developers to integrate various payment functionalities into their applications easily. By following the steps outlined in this documentation, you can implement features like payment initialization, transaction verification, and sub-account management. Feel free to explore the SDK further to discover all the supported features and functionalities. 163 | 164 | ## Contributing 165 | 166 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. After that free to contribute to this project. Please read the [CONTRIBUTING.md](https://github.com/chapimenge3/chapa/blob/main/CONTRIBUTING.md) file for more information. 167 | 168 | Please make sure to update tests as appropriate. 169 | 170 | ## Run Locally 171 | 172 | Clone the project 173 | 174 | ```bash 175 | git clone https://github.com/chapimenge3/chapa.git 176 | ``` 177 | 178 | Go to the project directory 179 | 180 | ```bash 181 | cd chapa 182 | ``` 183 | 184 | Install dependencies 185 | 186 | ```bash 187 | pip install -r requirements.txt 188 | ``` 189 | 190 | ## License 191 | 192 | [MIT](https://choosealicense.com/licenses/mit/) 193 | 194 | ## Author 195 | 196 | Temkin Mengistu 197 | 198 | [![portfolio](https://img.shields.io/badge/my_portfolio-000?style=for-the-badge&logo=ko-fi&logoColor=white)](https://chapimenge.me/) 199 | [![linkedin](https://img.shields.io/badge/linkedin-0A66C2?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/chapimenge/) 200 | [![twitter](https://img.shields.io/badge/twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://twitter.com/chapimenge3/) 201 | -------------------------------------------------------------------------------- /chapa/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Temkin Mengistu 3 | Email: chapimenge3@gmail.com 4 | Telegram: https://t.me/chapimenge 5 | Linkedin: https://www.linkedin.com/in/chapimenge/ 6 | Github: https://github.com/chapimenge3 7 | 8 | Project Links 9 | Github: https://github.com/chapimenge3/chapa 10 | Issues: https://github.com/chapimenge3/chapa/issues 11 | PR: https://github.com/chapimenge3/chapa/pulls 12 | PyPI: https://pypi.org/project/Chapa/ 13 | """ 14 | 15 | from .api import Chapa, AsyncChapa, get_testing_cards, get_testing_mobile 16 | from .webhook import verify_webhook, WEBHOOK_EVENTS, WEBHOOKS_EVENT_DESCRIPTION 17 | 18 | __all__ = [ 19 | 'Chapa', 20 | 'AsyncChapa', 21 | 'get_testing_cards', 22 | 'get_testing_mobile', 23 | 'verify_webhook', 24 | 'WEBHOOK_EVENTS', 25 | 'WEBHOOKS_EVENT_DESCRIPTION' 26 | ] 27 | -------------------------------------------------------------------------------- /chapa/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | API SDK for Chapa Payment Gateway 3 | """ 4 | 5 | # pylint: disable=too-few-public-methods 6 | # pylint: disable=too-many-branches 7 | # pylint: disable=too-many-arguments 8 | import re 9 | import json 10 | from typing import Dict, Optional 11 | import httpx 12 | 13 | 14 | # TODO: Implement the following methods 15 | # - Direct Charge 16 | # - Initiate Payments 17 | # - Authorize Payments 18 | # - Encryption 19 | 20 | 21 | class Response: 22 | """Custom Response class for SMS handling.""" 23 | 24 | def __init__(self, dict1): 25 | self.__dict__.update(dict1) 26 | 27 | 28 | def convert_response(response: dict) -> Response: 29 | """ 30 | Convert Response data to a Response object 31 | 32 | Args: 33 | response (dict): The response data to convert 34 | 35 | Returns: 36 | Response: The converted response 37 | """ 38 | if not isinstance(response, dict): 39 | return response 40 | 41 | return json.loads(json.dumps(response), object_hook=Response) 42 | 43 | 44 | class Chapa: 45 | """ 46 | Simple SDK for Chapa Payment gateway 47 | """ 48 | 49 | def __init__( 50 | self, 51 | secret, 52 | base_ur="https://api.chapa.co", 53 | api_version="v1", 54 | response_format="json", 55 | ): 56 | self._key = secret 57 | self.base_url = base_ur 58 | self.api_version = api_version 59 | if response_format and response_format in ["json", "obj"]: 60 | self.response_format = response_format 61 | else: 62 | raise ValueError("response_format must be 'json' or 'obj'") 63 | 64 | self.headers = {"Authorization": f"Bearer {self._key}"} 65 | self.client = httpx.Client() 66 | 67 | def send_request(self, url, method, data=None, params=None, headers=None): 68 | """ 69 | Request sender to the api 70 | 71 | Args: 72 | url (str): url for the request to be sent. 73 | method (str): the method for the request. 74 | data (dict, optional): request body. Defaults to None. 75 | 76 | Returns: 77 | response: response of the server. 78 | """ 79 | if params and not isinstance(params, dict): 80 | raise ValueError("params must be a dict") 81 | 82 | if data and not isinstance(data, dict): 83 | raise ValueError("data must be a dict") 84 | 85 | if headers and isinstance(data, dict): 86 | headers.update(self.headers) 87 | elif headers and not isinstance(data, dict): 88 | raise ValueError("headers must be a dict") 89 | else: 90 | headers = self.headers 91 | 92 | func = getattr(self.client, method) 93 | response = func(url, data=data, headers=headers) 94 | return getattr(response, "json", lambda: response.text)() 95 | 96 | def _construct_request(self, *args, **kwargs): 97 | """Construct the request to send to the API""" 98 | 99 | res = self.send_request(*args, **kwargs) 100 | if self.response_format == "obj" and isinstance(res, dict): 101 | return convert_response(res) 102 | 103 | return res 104 | 105 | def initialize( 106 | self, 107 | email: str, 108 | amount: int, 109 | first_name: str, 110 | last_name: str, 111 | tx_ref: str, 112 | currency="ETB", 113 | phone_number=None, 114 | callback_url=None, 115 | return_url=None, 116 | customization=None, 117 | headers=None, 118 | **kwargs, 119 | ) -> dict | Response: 120 | """ 121 | Initialize the Transaction 122 | 123 | Args: 124 | email (str): customer email 125 | amount (int): amount to be paid 126 | first_name (str): first name of the customer 127 | last_name (str): last name of the customer 128 | tx_ref (str): your transaction id 129 | phone_number (str, optional): phone number of the customer. 130 | Defaults to None. 131 | currency (str, optional): currency the transaction. Defaults to 'ETB'. 132 | callback_url (str, optional): function that runs when payment is successful. 133 | Defaults to None. 134 | return_url (str, optional): web address to redirect the user after payment is 135 | successful. Defaults to None. 136 | customization (dict, optional): customization, currently 'title' and 'description' 137 | are available. Defaults to None. 138 | headers(dict, optional): header to attach on the request. Default to None 139 | 140 | Return: 141 | dict: response from the server 142 | response(Response): response object of the response data return from the Chapa server. 143 | """ 144 | 145 | data = { 146 | "first_name": first_name, 147 | "last_name": last_name, 148 | "tx_ref": tx_ref, 149 | "currency": currency, 150 | } 151 | 152 | if kwargs: 153 | data.update(kwargs) 154 | 155 | if not isinstance(amount, int): 156 | if str(amount).replace(".", "", 1).isdigit() and float(amount) > 0: 157 | pass 158 | else: 159 | raise ValueError("invalid amount") 160 | elif isinstance(amount, int): 161 | if amount < 0: 162 | raise ValueError("invalid amount") 163 | 164 | data["amount"] = amount 165 | 166 | if not re.match(r"[^@]+@[^@]+\.[^@]+", email): 167 | raise ValueError("invalid email") 168 | 169 | data["email"] = email 170 | 171 | if phone_number: 172 | data["phone_number"] = phone_number 173 | 174 | if callback_url: 175 | data["callback_url"] = callback_url 176 | 177 | if return_url: 178 | data["return_url"] = return_url 179 | 180 | if customization: 181 | if "title" in customization: 182 | data["customization[title]"] = customization["title"] 183 | if "description" in customization: 184 | data["customization[description]"] = customization["description"] 185 | if "logo" in customization: 186 | data["customization[logo]"] = customization["logo"] 187 | 188 | response = self._construct_request( 189 | url=f"{self.base_url}/{self.api_version}/transaction/initialize", 190 | method="post", 191 | data=data, 192 | headers=headers, 193 | ) 194 | return response 195 | 196 | def verify(self, transaction: str, headers=None) -> dict | Response: 197 | """Verify the transaction 198 | 199 | Args: 200 | transaction (str): transaction id 201 | 202 | Response: 203 | dict: response from the server 204 | response(Response): response object of the response data return from the Chapa server. 205 | """ 206 | response = self._construct_request( 207 | url=f"{self.base_url}/{self.api_version}/transaction/verify/{transaction}", 208 | method="get", 209 | headers=headers, 210 | ) 211 | return response 212 | 213 | def create_subaccount( 214 | self, 215 | business_name: str, 216 | account_name: str, 217 | bank_code: str, 218 | account_number: str, 219 | split_value: str, 220 | split_type: str, 221 | headers=None, 222 | **kwargs, 223 | ) -> dict | Response: 224 | """ 225 | Create a subaccount for split payment 226 | 227 | Args: 228 | business_name (str): business name 229 | account_name (str): account name 230 | bank_code (str): bank code 231 | account_number (str): account number 232 | split_value (str): split value 233 | split_type (str): split type 234 | headers(dict, optional): header to attach on the request. Default to None 235 | **kwargs: additional data to be sent to the server 236 | 237 | Return: 238 | dict: response from the server 239 | response(Response): response object of the response data return from the Chapa server. 240 | """ 241 | 242 | data = { 243 | "business_name": business_name, 244 | "account_name": account_name, 245 | "bank_code": bank_code, 246 | "account_number": account_number, 247 | "split_value": split_value, 248 | "split_type": split_type, 249 | } 250 | 251 | if kwargs: 252 | data.update(kwargs) 253 | 254 | response = self._construct_request( 255 | url=f"{self.base_url}/{self.api_version}/subaccount", 256 | method="post", 257 | data=data, 258 | headers=headers, 259 | ) 260 | return response 261 | 262 | def initialize_split_payment( 263 | self, 264 | amount: int, 265 | currency: str, 266 | email: str, 267 | first_name: str, 268 | last_name: str, 269 | tx_ref: str, 270 | callback_url: str, 271 | return_url: str, 272 | subaccount_id: str, 273 | headers=None, 274 | **kwargs, 275 | ) -> dict | Response: 276 | """ 277 | Initialize split payment transaction 278 | 279 | Args: 280 | email (str): customer email 281 | amount (int): amount to be paid 282 | first_name (str): first name of the customer 283 | last_name (str): last name of the customer 284 | tx_ref (str): your transaction id 285 | currency (str, optional): currency the transaction. Defaults to 'ETB'. 286 | callback_url (str, optional): url for the customer to redirect after payment. 287 | Defaults to None. 288 | return_url (str, optional): url for the customer to redirect after payment. 289 | Defaults to None. 290 | subaccount_id (str, optional): subaccount id to split payment. 291 | Defaults to None. 292 | headers(dict, optional): header to attach on the request. Default to None 293 | **kwargs: additional data to be sent to the server 294 | 295 | Return: 296 | dict: response from the server 297 | response(Response): response object of the response data return from the Chapa server. 298 | """ 299 | 300 | data = { 301 | "first_name": first_name, 302 | "last_name": last_name, 303 | "tx_ref": tx_ref, 304 | "currency": currency, 305 | "callback_url": callback_url, 306 | "return_url": return_url, 307 | "subaccount_id": subaccount_id, 308 | } 309 | 310 | if kwargs: 311 | data.update(kwargs) 312 | 313 | if not isinstance(amount, int): 314 | if str(amount).replace(".", "", 1).isdigit() and float(amount) > 0: 315 | pass 316 | else: 317 | raise ValueError("invalid amount") 318 | elif isinstance(amount, int): 319 | if amount < 0: 320 | raise ValueError("invalid amount") 321 | 322 | data["amount"] = amount 323 | 324 | if not re.match(r"[^@]+@[^@]+\.[^@]+", email): 325 | raise ValueError("invalid email") 326 | 327 | data["email"] = email 328 | 329 | response = self._construct_request( 330 | url=f"{self.base_url}/{self.api_version}/transaction/initialize", 331 | method="post", 332 | data=data, 333 | headers=headers, 334 | ) 335 | return response 336 | 337 | def get_banks(self, headers=None) -> dict | Response: 338 | """Get the list of all banks 339 | 340 | Response: 341 | dict: response from the server 342 | response(Response): response object of the response data return from the Chapa server. 343 | """ 344 | response = self._construct_request( 345 | url=f"{self.base_url}/{self.api_version}/banks", 346 | method="get", 347 | headers=headers, 348 | ) 349 | return response 350 | 351 | def transfer_to_bank( 352 | self, 353 | *, 354 | account_name: str, 355 | account_number: str, 356 | amount: str, 357 | reference: str, 358 | beneficiary_name: Optional[str], 359 | bank_code: str, 360 | currency: str = "ETB", 361 | ) -> dict | Response: 362 | """Initiate a Bank Transfer 363 | 364 | This section describes how to Initiate a transfer with Chapa 365 | 366 | Args: 367 | account_name (str): This is the recipient Account Name matches on their bank account 368 | account_number (str): This is the recipient Account Number. 369 | amount (str): This the amount to be transferred to the recipient. 370 | beneficiary_name (Optional[str]): This is the full name of the Transfer beneficiary (You may use it to match on your required). 371 | currency (float): This is the currency for the Transfer. Expected value is ETB. Default value is ETB. 372 | reference (str): This a merchant’s uniques reference for the transfer, it can be used to query for the status of the transfer 373 | bank_code (str): This is the recipient bank code. You can see a list of all the available banks and their codes from the get banks endpoint. 374 | 375 | Returns: 376 | dict: response from the server 377 | response(Response): response object of the response data return from the Chapa server. 378 | """ 379 | data = { 380 | "account_name": account_name, 381 | "account_number": account_number, 382 | "amount": amount, 383 | "reference": reference, 384 | "bank_code": bank_code, 385 | "currency": currency, 386 | } 387 | if beneficiary_name: 388 | data["beneficiary_name"] = beneficiary_name 389 | 390 | response = self._construct_request( 391 | url=f"{self.base_url}/{self.api_version}/transfer", 392 | method="post", 393 | data=data, 394 | ) 395 | return response 396 | 397 | def verify_transfer(self, reference: str) -> dict | Response: 398 | """Verify the status of a transfer 399 | 400 | This section describes how to verify the status of a transfer with Chapa 401 | 402 | Args: 403 | reference (str): This a merchant’s uniques reference for the transfer, it can be used to query for the status of the transfer 404 | 405 | Returns: 406 | dict: response from the server 407 | - message: str 408 | - status: str 409 | - data: str | None 410 | response(Response): response object of the response data return from the Chapa server. 411 | """ 412 | response = self._construct_request( 413 | url=f"{self.base_url}/{self.api_version}/transfer/verify/{reference}", 414 | method="get", 415 | ) 416 | return response 417 | 418 | 419 | class AsyncChapa: 420 | def __init__( 421 | self, 422 | secret: str, 423 | base_ur: str = "https://api.chapa.co", 424 | api_version: str = "v1", 425 | response_format: str = "json", 426 | ) -> None: 427 | self._key = secret 428 | self.base_url = base_ur 429 | self.api_version = api_version 430 | if response_format and response_format in ["json", "obj"]: 431 | self.response_format = response_format 432 | else: 433 | raise ValueError("response_format must be 'json' or 'obj'") 434 | 435 | self.headers = {"Authorization": f"Bearer {self._key}"} 436 | self.client = httpx.AsyncClient() 437 | 438 | async def send_request( 439 | self, 440 | url: str, 441 | method: str, 442 | data: Optional[Dict] = None, 443 | params: Optional[Dict] = None, 444 | headers: Optional[Dict] = None, 445 | ): 446 | """ 447 | Request sender to the api 448 | 449 | Args: 450 | url (str): url for the request to be sent. 451 | method (str): the method for the request. 452 | data (dict, optional): request body. Defaults to None. 453 | 454 | Returns: 455 | response: response of the server. 456 | """ 457 | if params and not isinstance(params, dict): 458 | raise ValueError("params must be a dict") 459 | 460 | if data and not isinstance(data, dict): 461 | raise ValueError("data must be a dict") 462 | 463 | if headers and isinstance(data, dict): 464 | headers.update(self.headers) 465 | elif headers and not isinstance(data, dict): 466 | raise ValueError("headers must be a dict") 467 | else: 468 | headers = self.headers 469 | 470 | async with self.client as client: 471 | func = getattr(client, method) 472 | response = await func(url, data=data, headers=headers) 473 | return getattr(response, "json", lambda: response.text)() 474 | 475 | async def _construct_request(self, *args, **kwargs): 476 | """Construct the request to send to the API""" 477 | 478 | res = await self.send_request(*args, **kwargs) 479 | if self.response_format == "obj" and isinstance(res, dict): 480 | return convert_response(res) 481 | 482 | return res 483 | 484 | async def initialize( 485 | self, 486 | *, 487 | email: Optional[str] = None, 488 | amount: float, 489 | first_name: Optional[str] = None, 490 | last_name: Optional[str] = None, 491 | phone_number: Optional[str] = None, 492 | tx_ref: str, 493 | currency: str, 494 | callback_url: Optional[str] = None, 495 | return_url: Optional[str] = None, 496 | customization: Optional[Dict] = None, 497 | subaccount_id: Optional[str] = None, 498 | **kwargs, 499 | ): 500 | """Initialize the Transaction and Get a payment link 501 | 502 | Once all the information needed to proceed with the transaction is retrieved, the action taken further would be to associate the following information into the javascript function(chosen language) which will innately display the checkout. 503 | 504 | 505 | Args: 506 | amount (float): A customer’s email. address 507 | tx_ref (str): A unique reference given to each transaction. 508 | currency (str): The currency in which all the charges are made. Currency allowed is ETB and USD. 509 | email (Optional[str], optional): A customer’s email. address. Defaults to None. 510 | first_name (Optional[str], optional): A customer’s first name. Defaults to None. 511 | last_name (Optional[str], optional): A customer’s last name. Defaults to None. 512 | phone_number (Optional[str], optional): The customer’s phone number. Defaults to None. 513 | callback_url (Optional[str], optional): Function that runs when payment is successful. This should ideally be a script that uses the verify endpoint on the Chapa API to check the status of the transaction. Defaults to None. 514 | return_url (Optional[str], optional): Web address to redirect the user after payment is successful. Defaults to None. 515 | customization (Optional[Dict], optional): The customizations field (optional) allows you to customize the look and feel of the payment modal. You can set a logo, the store name to be displayed (title), and a description for the payment. Defaults to None. 516 | subaccount_id (Optional[str], optional): The subaccount id to split payment. Defaults to None. 517 | **kwargs: Additional data to be sent to the server. 518 | 519 | Returns: 520 | dict: response from the server 521 | - message: str 522 | - status: str 523 | - data: dict 524 | - checkout_url: str 525 | response(Response): response object of the response data return from the Chapa server. 526 | 527 | Raises: 528 | ValueError: If the parameters are invalid. 529 | """ 530 | data = { 531 | "first_name": first_name, 532 | "last_name": last_name, 533 | "tx_ref": tx_ref, 534 | "currency": currency, 535 | } 536 | 537 | if subaccount_id: 538 | data["subaccount"] = {"id": subaccount_id} 539 | 540 | if kwargs: 541 | data.update(kwargs) 542 | 543 | if not isinstance(amount, int): 544 | if str(amount).replace(".", "", 1).isdigit() and float(amount) > 0: 545 | pass 546 | else: 547 | raise ValueError("invalid amount") 548 | elif isinstance(amount, int): 549 | if amount < 0: 550 | raise ValueError("invalid amount") 551 | 552 | data["amount"] = amount 553 | 554 | regex = re.compile( 555 | r"([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})+" 556 | ) 557 | if not regex.match(email): 558 | raise ValueError("invalid email") 559 | 560 | data["email"] = email 561 | 562 | if phone_number: 563 | data["phone_number"] = phone_number 564 | 565 | if callback_url: 566 | data["callback_url"] = callback_url 567 | 568 | if return_url: 569 | data["return_url"] = return_url 570 | 571 | if customization: 572 | if "title" in customization: 573 | data["customization[title]"] = customization["title"] 574 | if "description" in customization: 575 | data["customization[description]"] = customization["description"] 576 | if "logo" in customization: 577 | data["customization[logo]"] = customization["logo"] 578 | 579 | response = await self._construct_request( 580 | url=f"{self.base_url}/{self.api_version}/transaction/initialize", 581 | method="post", 582 | data=data, 583 | ) 584 | return response 585 | 586 | async def verify(self, tx_ref: str, headers: Optional[Dict] = None): 587 | """Verify the transaction 588 | 589 | Args: 590 | tx_ref (str): transaction id 591 | 592 | Returns: 593 | dict: response from the server 594 | response(Response): response object of the response data return from the Chapa server. 595 | """ 596 | response = await self._construct_request( 597 | url=f"{self.base_url}/{self.api_version}/transaction/verify/{tx_ref}", 598 | method="get", 599 | headers=headers, 600 | ) 601 | return response 602 | 603 | async def create_subaccount( 604 | self, 605 | bank_code: str, 606 | account_number: str, 607 | business_name: str, 608 | account_name: str, 609 | split_type: str, 610 | split_value: str, 611 | headers: Optional[Dict] = None, 612 | **kwargs, 613 | ): 614 | """ 615 | Create a subaccount for split payment. 616 | 617 | **Note:** that sub-accounts are working with ETB currency as a default settlement. This means if we get subaccount in your payload regardless of the currency we will convert it to ETB and do the settlement. 618 | 619 | Args: 620 | bank_code (str): The bank account details for this subaccount. The bank_code is the bank id (you can get this from the get banks endpoint). 621 | account_number (str): The account_number is the bank account number. 622 | business_name (str): The vendor/merchant detail the subaccount for. 623 | account_name (str): The vendor/merchant account’s name matches from the bank account. 624 | split_type (str): The type of split you want to use with this subaccount. 625 | - Use flat if you want to get a flat fee from each transaction, while the subaccount gets the rest. 626 | - Use percentage if you want to get a percentage of each transaction. 627 | split_value (str): The amount you want to get as commission on each transaction. This goes with the split_type. 628 | Example: 629 | - to collect 3% from each transaction, split_type will be percentage and split_value will be 0.03. 630 | - to collect 25 Birr from each transaction, split_type will be flat and split_value will be 25. 631 | headers(dict, optional): header to attach on the request. Default to None 632 | **kwargs: additional data to be sent to the server 633 | 634 | Return: 635 | dict: response from the server 636 | - message: str 637 | - status: str 638 | - data: dict 639 | - subaccounts[id]": str 640 | response(Response): response object of the response data return from the Chapa server. 641 | """ 642 | 643 | data = { 644 | "business_name": business_name, 645 | "account_name": account_name, 646 | "bank_code": bank_code, 647 | "account_number": account_number, 648 | "split_value": split_value, 649 | "split_type": split_type, 650 | } 651 | 652 | if kwargs: 653 | data.update(kwargs) 654 | 655 | response = await self._construct_request( 656 | url=f"{self.base_url}/{self.api_version}/subaccount", 657 | method="post", 658 | data=data, 659 | headers=headers, 660 | ) 661 | return response 662 | 663 | async def get_banks(self, headers: Optional[Dict] = None): 664 | """Get the list of all banks 665 | 666 | Returns: 667 | dict: response from the server 668 | response(Response): response object of the response data return from the Chapa server. 669 | """ 670 | response = await self._construct_request( 671 | url=f"{self.base_url}/{self.api_version}/banks", 672 | method="get", 673 | headers=headers, 674 | ) 675 | return response 676 | 677 | async def transfer_to_bank( 678 | self, 679 | *, 680 | account_name: str, 681 | account_number: str, 682 | amount: str, 683 | reference: str, 684 | beneficiary_name: Optional[str], 685 | bank_code: str, 686 | currency: str = "ETB", 687 | ): 688 | """Initiate a Bank Transfer 689 | 690 | This section describes how to Initiate a transfer with Chapa 691 | 692 | Args: 693 | account_name (str): This is the recipient Account Name matches on their bank account 694 | account_number (str): This is the recipient Account Number. 695 | amount (str): This the amount to be transferred to the recipient. 696 | beneficiary_name (Optional[str]): This is the full name of the Transfer beneficiary (You may use it to match on your required). 697 | currency (float): This is the currency for the Transfer. Expected value is ETB. Default value is ETB. 698 | reference (str): This a merchant’s uniques reference for the transfer, it can be used to query for the status of the transfer 699 | bank_code (str): This is the recipient bank code. You can see a list of all the available banks and their codes from the get banks endpoint. 700 | 701 | Returns: 702 | dict: response from the server 703 | - message: str 704 | - status: str 705 | - data: str | None 706 | response(Response): response object of the response data return from the Chapa server. 707 | """ 708 | data = { 709 | "account_name": account_name, 710 | "account_number": account_number, 711 | "amount": amount, 712 | "reference": reference, 713 | "bank_code": bank_code, 714 | "currency": currency, 715 | } 716 | if beneficiary_name: 717 | data["beneficiary_name"] = beneficiary_name 718 | 719 | response = await self._construct_request( 720 | url=f"{self.base_url}/{self.api_version}/transfer", 721 | method="post", 722 | data=data, 723 | ) 724 | return response 725 | 726 | async def verify_transfer(self, reference: str): 727 | """Verify the status of a transfer 728 | 729 | This section describes how to verify the status of a transfer with Chapa 730 | 731 | Args: 732 | reference (str): This a merchant’s uniques reference for the transfer, it can be used to query for the status of the transfer 733 | 734 | Returns: 735 | dict: response from the server 736 | - message: str 737 | - status: str 738 | - data: str | None 739 | response(Response): response object of the response data return from the Chapa server. 740 | """ 741 | response = await self._construct_request( 742 | url=f"{self.base_url}/{self.api_version}/transfer/verify/{reference}", 743 | method="get", 744 | ) 745 | return response 746 | 747 | 748 | def get_testing_cards(self): 749 | """Get the list of all testing cards 750 | 751 | Returns: 752 | List[dict]: all testing cards 753 | """ 754 | testing_cards = [ 755 | { 756 | "Brand": "Visa", 757 | "Card Number": "4200 0000 0000 0000", 758 | "CVV": "123", 759 | "Expiry": "12/34", 760 | }, 761 | { 762 | "Brand": "Amex", 763 | "Card Number": "3700 0000 0000 0000", 764 | "CVV": "1234", 765 | "Expiry": "12/34", 766 | }, 767 | { 768 | "Brand": "Mastercard", 769 | "Card Number": "5400 0000 0000 0000", 770 | "CVV": "123", 771 | "Expiry": "12/34", 772 | }, 773 | { 774 | "Brand": "Union Pay", 775 | "Card Number": "6200 0000 0000 0000", 776 | "CVV": "123", 777 | "Expiry": "12/34", 778 | }, 779 | { 780 | "Brand": "Diners", 781 | "Card Number": "3800 0000 0000 0000", 782 | "CVV": "123", 783 | "Expiry": "12/34", 784 | }, 785 | ] 786 | 787 | return testing_cards 788 | 789 | 790 | def get_testing_mobile(self): 791 | """ 792 | Get the list of all testing mobile numbers 793 | 794 | Returns: 795 | List[dict]: all testing mobile numbers 796 | """ 797 | testing_mobile = [ 798 | {"Bank": "Awash Bank", "Phone": "0900123456", "OTP": "12345"}, 799 | {"Bank": "Awash Bank", "Phone": "0900112233", "OTP": "12345"}, 800 | {"Bank": "Awash Bank", "Phone": "0900881111", "OTP": "12345"}, 801 | {"Bank": "Amole", "Phone": "0900123456", "OTP": "12345"}, 802 | {"Bank": "Amole", "Phone": "0900112233", "OTP": "12345"}, 803 | {"Bank": "Amole", "Phone": "0900881111", "OTP": "12345"}, 804 | {"Bank": "telebirr", "Phone": "0900123456", "OTP": "12345"}, 805 | {"Bank": "telebirr", "Phone": "0900112233", "OTP": "12345"}, 806 | {"Bank": "telebirr", "Phone": "0900881111", "OTP": "12345"}, 807 | {"Bank": "CBEBirr", "Phone": "0900123456", "OTP": "12345"}, 808 | {"Bank": "CBEBirr", "Phone": "0900112233", "OTP": "12345"}, 809 | {"Bank": "CBEBirr", "Phone": "0900881111", "OTP": "12345"}, 810 | {"Bank": "COOPPay-ebirr", "Phone": "0900123456", "OTP": "12345"}, 811 | {"Bank": "COOPPay-ebirr", "Phone": "0900112233", "OTP": "12345"}, 812 | {"Bank": "COOPPay-ebirr", "Phone": "0900881111", "OTP": "12345"}, 813 | ] 814 | return testing_mobile 815 | -------------------------------------------------------------------------------- /chapa/webhook.py: -------------------------------------------------------------------------------- 1 | """ 2 | Chapa Webhook Utilities Module 3 | """ 4 | import hmac 5 | import hashlib 6 | import json 7 | 8 | 9 | WEBHOOKS_EVENT_DESCRIPTION = { 10 | 'charge.dispute.create': 'Dispute against company created.', 11 | 'charge.dispute.remind': 'Reminder of an unresolved dispute against company.', 12 | 'charge.dispute.resolve': 'Dispute has been resolved.', 13 | 'charge.success': 'Charged successfully.', 14 | 'customeridentification.failed': 'Customer identification failed.', 15 | 'customeridentification.success': 'Customer identified successfully.', 16 | 'invoice.create': 'An invoice has been created for a customer\'s subscription.' \ 17 | 'Usually sent 3 days before the subscription is due.', 18 | 'invoice.payment_failed': 'Payment for invoice has failed.', 19 | 'invoice.update': 'Customer\'s invoice has been updated. This invoice should' \ 20 | 'be examined carfeully, and take necessary action.', 21 | 'paymentrequest.pending': 'Payment request has been sent to customer and payment is pending.', 22 | 'paymentrequest.success': 'Customer\'s payment is successful.', 23 | 'subscription.create': 'Subscription has been created.', 24 | 'subscription.disable': 'Account\'s subscription has been disabled.', 25 | 'subscription.enable': 'Account\'s subscription has been enabled.', 26 | 'transfer.failed': 'Transfer of money has failed.', 27 | 'transfer.success': 'A transfer has been completed.', 28 | 'transfer.reversed': 'A transfer has been reversed.', 29 | 'issuingauthentication.request': 'An authorization has been requested.', 30 | 'issuingauthentication.created': 'An authorization has been created.', 31 | } 32 | 33 | WEBHOOK_EVENTS = WEBHOOKS_EVENT_DESCRIPTION.keys() 34 | 35 | 36 | def verify_webhook(secret_key: str, body: dict, chapa_signature: str) -> bool: 37 | """ 38 | Verify the webhook request 39 | 40 | Args: 41 | secret_key (str): The secret key 42 | body (dict): The request body 43 | chapa_signature (str): The signature from the request headers 44 | 45 | Returns: 46 | bool: True if the request is valid, False otherwise 47 | """ 48 | signature = hmac.new(secret_key.encode(), json.dumps(body).encode(), hashlib.sha256).hexdigest() 49 | return signature == chapa_signature -------------------------------------------------------------------------------- /docs.md: -------------------------------------------------------------------------------- 1 | # API SDK for Chapa Payment Gateway Documentation 2 | 3 | ## Introduction 4 | 5 | This document provides a comprehensive guide to integrating and using the Chapa Payment Gateway SDK in your application. Chapa is a powerful payment gateway that supports various payment methods, facilitating seamless transactions for businesses. This SDK simplifies interaction with Chapa’s API, enabling operations such as initiating payments, verifying transactions, and managing subaccounts. 6 | 7 | ## Installation 8 | 9 | To use the Chapa SDK in your project, you need to install it using pip, as it is a dependency for making HTTP requests it will also install `httpx` as a dependency. 10 | 11 | ```bash 12 | pip install chapa 13 | ``` 14 | 15 | ## Usage 16 | 17 | To begin using the SDK, import the `Chapa` class from the module and instantiate it with your secret key. 18 | 19 | ### Initializing the SDK 20 | 21 | ```python 22 | from chapa import Chapa 23 | 24 | # Replace 'your_secret_key' with your actual Chapa secret key 25 | chapa = Chapa('your_secret_key') 26 | ``` 27 | 28 | ### Making Payments 29 | 30 | To initiate a payment, use the `initialize` method. This method requires a set of parameters like the customer's email, amount, first name, last name, and a transaction reference. 31 | 32 | ```python 33 | response = chapa.initialize( 34 | email="customer@example.com", 35 | amount=1000, 36 | first_name="John", 37 | last_name="Doe", 38 | tx_ref="your_unique_transaction_reference", 39 | callback_url="https://yourcallback.url/here" 40 | ) 41 | print(response) 42 | ``` 43 | 44 | ### Verifying Payments 45 | 46 | After initiating a payment, you can verify the transaction status using the `verify` method. 47 | 48 | ```python 49 | transaction_id = "your_transaction_id" 50 | verification_response = chapa.verify(transaction_id) 51 | print(verification_response) 52 | ``` 53 | 54 | ### Creating Subaccounts 55 | 56 | You can create subaccounts for split payments using the `create_subaccount` method. 57 | 58 | ```python 59 | subaccount_response = chapa.create_subaccount( 60 | business_name="My Business", 61 | account_name="My Business Account", 62 | bank_code="12345", 63 | account_number="0012345678", 64 | split_value="0.2", 65 | split_type="percentage" 66 | ) 67 | print(subaccount_response) 68 | ``` 69 | 70 | ### Bank Transfers 71 | 72 | To initiate a bank transfer, use the `transfer_to_bank` method. 73 | 74 | ```python 75 | transfer_response = chapa.transfer_to_bank( 76 | account_name="Recipient Name", 77 | account_number="0987654321", 78 | amount="500", 79 | reference="your_transfer_reference", 80 | bank_code="67890", 81 | currency="ETB" 82 | ) 83 | print(transfer_response) 84 | ``` 85 | 86 | ### Getting Testing Cards and Mobile Numbers 87 | 88 | For testing purposes, you can retrieve a set of test cards and mobile numbers. 89 | 90 | ```python 91 | # Get a list of testing cards 92 | test_cards = chapa.get_testing_cards() 93 | print(test_cards) 94 | 95 | # Get a list of testing mobile numbers 96 | test_mobiles = chapa.get_testing_mobile() 97 | print(test_mobiles) 98 | ``` 99 | 100 | ## Conclusion 101 | 102 | The Chapa Payment Gateway SDK is a flexible tool that allows developers to integrate various payment functionalities into their applications easily. By following the steps outlined in this documentation, you can implement features like payment initialization, transaction verification, and subaccount management. Feel free to explore the SDK further to discover all the supported features and functionalities. 103 | ``` 104 | -------------------------------------------------------------------------------- /examples/initiate_transaction.py: -------------------------------------------------------------------------------- 1 | from chapa import Chapa 2 | import random 3 | import string 4 | 5 | def generete_tx_ref(length): 6 | #Generate a transaction reference 7 | tx_ref = string.ascii_lowercase 8 | return ''.join(random.choice(tx_ref) for i in range(length)) 9 | 10 | 11 | def checkout(): 12 | data = { 13 | # Required fields 14 | 'email': 'user@example.com', # customer/client email address 15 | 'amount': 1311.00, # total payment 16 | 'first_name': 'Abebe', # customer/client first name 17 | 'last_name': 'Bikila', # customer/client last name 18 | 'tx_ref': generete_tx_ref(12), 19 | 20 | # Optional fields 21 | 'callback_url': 'your_callback_url', # after successful payment chapa will redirect your customer/client to this url 22 | 'customization': { 23 | 'title': 'Example.com', 24 | 'description': 'Payment for your services', 25 | } 26 | } 27 | 28 | chapa = Chapa('[YOUR_CHAPA_SECRET_KEY]') 29 | response = chapa.initialize(**data) 30 | 31 | # After successfull initialization redirect to the `checkout_url` 32 | if response['status'] == 'success': 33 | # do some action 34 | """ Redirect to the checkout page using `response['data']['checkout_url']` """ 35 | else: 36 | # If initialization fails display the error message 37 | print(response['message']) 38 | -------------------------------------------------------------------------------- /examples/verify_transaction.py: -------------------------------------------------------------------------------- 1 | from chapa import Chapa 2 | 3 | 4 | def verify(transaction_id): 5 | """ Accept the transaction id and verify it """ 6 | chapa = Chapa('[YOUR_CHAPA_SECRET_KEY]') 7 | response = chapa.verify(transaction_id) 8 | 9 | if response['status'] == 'success': 10 | # do some actions, probably redirect to success page 11 | pass 12 | else: 13 | # do some actions, probably redirect to failed page 14 | pass 15 | 16 | """ 17 | If verification response has succeed it looks like 18 | {'message': 'Payment details', 'status': 'success', 'data': { 19 | 'first_name': 'Abebe', 'last_name': 'Bikila', 'email': 'user@example.com', 'currency': 'ETB', 20 | 'amount': '1,311.00', 'charge': '45.89', 'mode': 'test', 'method': 'test', 'type': 'API', 21 | 'status': 'success', 'reference': '[reference]', 'tx_ref': '[tx_ref]', 22 | 'customization': {'title': 'Example.com', 'description': 'Payment for your services', 'logo': None}, 23 | 'meta': None, 'created_at': '2022-08-24T12:29:52.000000Z', 'updated_at': '2022-08-24T12:29:52.000000Z'}} 24 | """ 25 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | httpx>=0.27.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | import os 3 | 4 | with open('README.md', 'r', encoding='utf-8') as fh: 5 | long_description = fh.read() 6 | 7 | version_number = os.environ.get('CHAPA_VERSION', '0.1.0') 8 | 9 | setuptools.setup( 10 | name='chapa', 11 | version=version_number, 12 | author='Temkin Mengistu (Chapi)', 13 | author_email='chapimenge3@gmail.com', 14 | description='Python SDK for Chapa API https://developer.chapa.co', 15 | long_description=long_description, 16 | long_description_content_type='text/markdown', 17 | url='https://github.com/chapimenge3/chapa', 18 | packages=['chapa'], 19 | package_dir={'chapa': 'chapa'}, 20 | project_urls={ 21 | 'Source': 'https://github.com/chapimenge3/chapa', 22 | 'Bug Tracker': 'https://github.com/chapimenge3/chapa/issues', 23 | }, 24 | classifiers=[ 25 | 'Programming Language :: Python :: 3', 26 | 'License :: OSI Approved :: MIT License', 27 | 'Operating System :: OS Independent', 28 | 'Development Status :: 3 - Alpha', 29 | 'Intended Audience :: Developers', 30 | 'Topic :: Software Development :: Libraries :: Python Modules', 31 | ], 32 | python_requires='>=3.6', 33 | install_requires=[ 34 | 'httpx>=0.27.0', 35 | ], 36 | ) 37 | --------------------------------------------------------------------------------