├── tests ├── __init__.py ├── helpers.py ├── config.py ├── test_midtransclient.py ├── context.py ├── test_config.py ├── test_http_client.py ├── test_tokenization.py ├── test_subscription.py ├── test_snap.py └── test_core_api.py ├── examples ├── flask_app │ ├── .python-version │ ├── Pipfile │ ├── Dockerfile │ ├── templates │ │ ├── simple_core_api_checkout_permata.html │ │ ├── index.html │ │ ├── simple_checkout.html │ │ ├── simple_core_api_checkout.html │ │ └── core_api_credit_card_frontend_sample.html │ ├── README.md │ ├── web.py │ ├── static │ │ └── checkout.css │ └── Pipfile.lock ├── core_api │ ├── core_api_simple_example.py │ └── core_api_credit_card_example.py ├── snap │ ├── snap_simple_example.py │ └── snap_advanced_example.py ├── transaction_actions │ ├── transaction_actions_example.py │ └── notification_example.py ├── subscription │ ├── credit_card_subscription_example.py │ └── gopay_subscription_example.py └── tokenization │ └── tokenization_example.py ├── Dockerfile ├── docker-entrypoint.sh ├── Pipfile ├── .gitignore ├── midtransclient ├── __init__.py ├── error_midtrans.py ├── snap.py ├── config.py ├── http_client.py ├── transactions.py └── core_api.py ├── docker-compose.yml ├── .travis.yml ├── Pipfile.lock ├── LICENSE ├── setup.py ├── Maintaining.md └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/flask_app/.python-version: -------------------------------------------------------------------------------- 1 | 3.7.0 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-alpine 2 | RUN pip install pytest -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # pip install pytest; 3 | pip install .; 4 | pytest; -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | import sys 2 | def is_str(target_str): 3 | if sys.version_info[0] >= 3: 4 | return isinstance(target_str, str) 5 | return isinstance(target_str, basestring) 6 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | 10 | [requires] 11 | python_version = "3.7" 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pytest_cache 2 | tests/__pycache__ 3 | *.pyc 4 | midtransclient/*.pyc 5 | midtransclient.egg-info 6 | dist/ 7 | build/ 8 | push_to_twine.sh 9 | ./Pipfile 10 | env/ 11 | .DS_STORE 12 | .idea -------------------------------------------------------------------------------- /tests/config.py: -------------------------------------------------------------------------------- 1 | # Note: keys are intentionally hardcoded, to make test consistent and centralized 2 | USED_SERVER_KEY='SB-Mid-server-GwUP_WGbJPXsDzsNEBRs8IYA' 3 | USED_CLIENT_KEY='SB-Mid-client-61XuGAwQ8Bj8LxSS' 4 | -------------------------------------------------------------------------------- /midtransclient/__init__.py: -------------------------------------------------------------------------------- 1 | from .snap import Snap 2 | from .core_api import CoreApi 3 | 4 | from .error_midtrans import MidtransAPIError 5 | from .error_midtrans import JSONDecodeError 6 | 7 | __version__ = '1.4.2' 8 | -------------------------------------------------------------------------------- /examples/flask_app/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | flask = "*" 8 | midtransclient = "*" 9 | 10 | [dev-packages] 11 | 12 | [requires] 13 | python_version = "3.7" 14 | -------------------------------------------------------------------------------- /tests/test_midtransclient.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from .context import midtransclient 3 | from pprint import pprint 4 | 5 | def test_midtransclient_module(): 6 | attributes = dir(midtransclient) 7 | assert 'Snap' in attributes 8 | assert 'CoreApi' in attributes -------------------------------------------------------------------------------- /tests/context.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | import os 5 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 6 | 7 | import midtransclient 8 | from midtransclient.config import ApiConfig 9 | from midtransclient.http_client import HttpClient -------------------------------------------------------------------------------- /examples/flask_app/Dockerfile: -------------------------------------------------------------------------------- 1 | from python:3.7-alpine 2 | LABEL name "midflask" 3 | 4 | # Create app directory 5 | WORKDIR /usr/src/midtrans-payment-example-app 6 | 7 | COPY . . 8 | 9 | RUN pip install pipenv 10 | 11 | RUN pipenv install --system --deploy 12 | EXPOSE 5000 13 | CMD ["python", "web.py"] -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | dev: 4 | build: . 5 | # ports: 6 | # - "5000:5000" # ports not needed for dev 7 | volumes: 8 | - ./.:/usr/src/midtrans-python-client 9 | working_dir: /usr/src/midtrans-python-client 10 | entrypoint: 11 | - ./docker-entrypoint.sh -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # @TODO: this Travis CI no longer work properly due to changes on Travis CI side regarding opensoure repo. 2 | # Need to migrate to other Open Source friendly CI providers. 3 | # Some candidates: Github Action, Circle CI, etc 4 | language: python 5 | python: 6 | - "3.5" 7 | - "3.6" 8 | - "3.7" 9 | - "3.8" 10 | - "3.9" 11 | # command to install dependencies 12 | install: 13 | - pip install pytest 14 | - pip install . 15 | # command to run tests 16 | script: 17 | - pytest -------------------------------------------------------------------------------- /midtransclient/error_midtrans.py: -------------------------------------------------------------------------------- 1 | class JSONDecodeError(Exception): 2 | pass 3 | 4 | class MidtransAPIError(Exception): 5 | def __init__(self, message, api_response_dict=None, http_status_code=None, raw_http_client_data=None): 6 | self.message = message 7 | self.api_response_dict = api_response_dict 8 | self.http_status_code = int(http_status_code) 9 | self.raw_http_client_data = raw_http_client_data 10 | 11 | def __str__(self): 12 | return self.message 13 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "7e7ef69da7248742e869378f8421880cf8f0017f96d94d086813baa518a65489" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": {}, 19 | "develop": {} 20 | } 21 | -------------------------------------------------------------------------------- /examples/flask_app/templates/simple_core_api_checkout_permata.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Simple Core API Permata VA Example 4 | 5 | 6 | 7 |
8 |

Permata VA Transaction Created

9 | VA Number For Customer To Pay:
10 | Please transfer to Permata Bank with account number: {{ permata_va_number }}
11 | Amount:
12 | Rp. {{ gross_amount }}
13 | Order ID:
14 | {{ order_id }}
15 |

16 | Check `web.py` file, section `/simple_core_api_checkout_permata` for the backend implementation 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/flask_app/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Index of example pages:

7 | 10 |

Advanced usage:

11 | 16 | 17 | -------------------------------------------------------------------------------- /examples/flask_app/README.md: -------------------------------------------------------------------------------- 1 | # Sample Flask App 2 | 3 | Example of checkout page using Midtrans Snap. A quick and secure way to accept 4 | payment with a beautiful user interface done for you. 5 | 6 | This is a very simple, very minimalist example to demonstrate integrating 7 | Midtrans Snap with Flask (Python). To start: 8 | 9 | ## Run Natively / Without Docker 10 | 11 | 1. Install Python (v3.7.0 for example, as used by this example) 12 | 2. Clone the repository 13 | 3. Install flask: `pip install Flask` 14 | 4. Install midtrans: `pip install midtransclient` 15 | 5. Run the web server using: 16 | - `python web.py`, or 17 | - `FLASK_APP=web.py flask run`, 18 | 19 | > Or replace step 3 & 4 by `pipenv install`, if you are using pipenv. 20 | 21 | The app will run at port 5000. Visit this url on your browser: `http://localhost:5000` 22 | 23 | ## Run With Docker 24 | > required: Docker installed. 25 | 26 | - First time to build & run: `docker build -t midflask . && docker run -p 5000:5000 --rm -it midflask`. 27 | - Next time just run `docker run -p 5000:5000 --rm -it midflask` 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Midtrans 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 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from .context import ApiConfig 3 | from pprint import pprint 4 | 5 | def test_api_config_class(): 6 | apiconfig = ApiConfig(is_production=False, 7 | server_key='sk-abc', 8 | client_key='ck-123') 9 | assert apiconfig.is_production == False 10 | assert apiconfig.server_key == 'sk-abc' 11 | assert apiconfig.client_key == 'ck-123' 12 | # assert repr(apiconfig) == '' 13 | 14 | def test_api_config_class_with_custom_headers(): 15 | apiconfig = ApiConfig(is_production=False, 16 | server_key='sk-abc', 17 | client_key='ck-123', 18 | custom_headers={'X-Override-Notification':'https://example.org'}) 19 | assert apiconfig.is_production == False 20 | assert apiconfig.server_key == 'sk-abc' 21 | assert apiconfig.client_key == 'ck-123' 22 | assert 'sk-abc' in repr(apiconfig) 23 | assert 'ck-123' in repr(apiconfig) 24 | assert 'X-Override-Notification' in repr(apiconfig) 25 | 26 | def test_api_config_class_with_proxies(): 27 | apiconfig = ApiConfig(is_production=False, 28 | server_key='sk-abc', 29 | client_key='ck-123', 30 | custom_headers=dict(), 31 | proxies={ 32 | 'http': 'http://example.org:3128', 33 | 'https': 'http://example.org:1080', 34 | }) 35 | assert apiconfig.is_production == False 36 | assert apiconfig.server_key == 'sk-abc' 37 | assert apiconfig.client_key == 'ck-123' 38 | assert 'sk-abc' in repr(apiconfig) 39 | assert 'ck-123' in repr(apiconfig) 40 | assert 'example.org:1080' in repr(apiconfig) -------------------------------------------------------------------------------- /examples/core_api/core_api_simple_example.py: -------------------------------------------------------------------------------- 1 | import midtransclient 2 | # This is just for very basic implementation reference, in production, you should validate the incoming requests and implement your backend more securely. 3 | 4 | # Initialize core api client object 5 | # You can find it in Merchant Portal -> Settings -> Access keys 6 | core = midtransclient.CoreApi( 7 | is_production=False, 8 | server_key='YOUR_SERVER_KEY', 9 | client_key='YOUR_CLIENT_KEY' 10 | ) 11 | 12 | # prepare CORE API parameter ( refer to: https://docs.midtrans.com/en/core-api/bank-transfer?id=sample-request-and-request-body ) charge bank_transfer parameter example 13 | param = { 14 | "payment_type": "bank_transfer", 15 | "transaction_details": { 16 | "gross_amount": 24145, 17 | "order_id": "test-transaction-321", 18 | }, 19 | "bank_transfer":{ 20 | "bank": "bni" 21 | } 22 | } 23 | 24 | # charge transaction 25 | charge_response = core.charge(param) 26 | print('charge_response:') 27 | print(charge_response) 28 | 29 | # charge_response is dictionary representation of API JSON response 30 | # sample: 31 | # { 32 | # 'currency': 'IDR', 33 | # 'fraud_status': 'accept', 34 | # 'gross_amount': '24145.00', 35 | # 'order_id': 'test-transaction-321', 36 | # 'payment_type': 'bank_transfer', 37 | # 'status_code': '201', 38 | # 'status_message': 'Success, Bank Transfer transaction is created', 39 | # 'transaction_id': '6ee793df-9b1d-4343-8eda-cc9663b4222f', 40 | # 'transaction_status': 'pending', 41 | # 'transaction_time': '2018-10-24 15:34:33', 42 | # 'va_numbers': [{'bank': 'bca', 'va_number': '490526303019299'}] 43 | # } 44 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """A setuptools based setup module. 2 | Based on: https://github.com/pypa/sampleproject/blob/90d44abe361688aba5a189e661423863b34f5208/setup.py 3 | """ 4 | 5 | # Always prefer setuptools over distutils 6 | from setuptools import setup, find_packages 7 | import pathlib 8 | 9 | here = pathlib.Path(__file__).parent.resolve() 10 | 11 | # Get the long description from the README file 12 | long_description = (here / 'README.md').read_text(encoding='utf-8') 13 | 14 | pkg_req = [ 15 | 'requests>=2.25.0' 16 | ] 17 | test_req = pkg_req + [ 18 | 'pytest>=3.0.6' 19 | ] 20 | 21 | setup( 22 | name="midtransclient", 23 | version="1.4.2", 24 | author="Midtrans - Integration Support Team", 25 | author_email="support@midtrans.com", 26 | license='MIT', 27 | description="Official Midtrans Payment API Client", 28 | long_description=long_description, 29 | long_description_content_type="text/markdown", 30 | url="https://github.com/midtrans/midtrans-python-client/", 31 | packages=find_packages(), 32 | classifiers=[ 33 | 'Programming Language :: Python', 34 | 'Programming Language :: Python :: 3', 35 | 'Programming Language :: Python :: 3 :: Only', 36 | 'Programming Language :: Python :: 3.5', 37 | 'Programming Language :: Python :: 3.6', 38 | 'Programming Language :: Python :: 3.7', 39 | 'Programming Language :: Python :: 3.8', 40 | 'Programming Language :: Python :: 3.9', 41 | 'Programming Language :: Python :: 3.10', 42 | 'Topic :: Software Development :: Libraries :: Python Modules' 43 | ], 44 | python_requires='>=3.5', 45 | install_requires=pkg_req, 46 | tests_requires=test_req 47 | ) 48 | -------------------------------------------------------------------------------- /examples/snap/snap_simple_example.py: -------------------------------------------------------------------------------- 1 | import midtransclient 2 | # This is just for very basic implementation reference, in production, you should validate the incoming requests and implement your backend more securely. 3 | # Please refer to this docs for snap popup: 4 | # https://docs.midtrans.com/en/snap/integration-guide?id=integration-steps-overview 5 | 6 | # Please refer to this docs for snap-redirect: 7 | # https://docs.midtrans.com/en/snap/integration-guide?id=alternative-way-to-display-snap-payment-page-via-redirect 8 | 9 | # Initialize snap client object 10 | # You can find it in Merchant Portal -> Settings -> Access keys 11 | snap = midtransclient.Snap( 12 | is_production=False, 13 | server_key='YOUR_SERVER_KEY', 14 | client_key='YOUR_CLIENT_KEY' 15 | ) 16 | 17 | # prepare SNAP API parameter ( refer to: https://snap-docs.midtrans.com ) minimum parameter example 18 | param = { 19 | "transaction_details": { 20 | "order_id": "test-transaction-123", 21 | "gross_amount": 200000 22 | }, "credit_card":{ 23 | "secure" : True 24 | } 25 | } 26 | 27 | # create transaction 28 | transaction = snap.create_transaction(param) 29 | 30 | # transaction token 31 | transaction_token = transaction['token'] 32 | print('transaction_token:') 33 | print(transaction_token) 34 | 35 | # transaction redirect url 36 | transaction_redirect_url = transaction['redirect_url'] 37 | print('transaction_redirect_url:') 38 | print(transaction_redirect_url) 39 | 40 | # transaction is dictionary representation of API JSON response 41 | # sample: 42 | # { 43 | # 'redirect_url': 'https://app.sandbox.midtrans.com/snap/v2/vtweb/f0a2cbe7-dfb7-4114-88b9-1ecd89e90121', 44 | # 'token': 'f0a2cbe7-dfb7-4114-88b9-1ecd89e90121' 45 | # } -------------------------------------------------------------------------------- /examples/transaction_actions/transaction_actions_example.py: -------------------------------------------------------------------------------- 1 | import midtransclient 2 | # This is just for very basic implementation reference, in production, you should validate the incoming requests and implement your backend more securely. 3 | 4 | # Initialize api client object 5 | # You can find it in Merchant Portal -> Settings -> Access keys 6 | api_client = midtransclient.CoreApi( 7 | is_production=False, 8 | server_key='YOUR_SERVER_KEY', 9 | client_key='YOUR_CLIENT_KEY' 10 | ) 11 | 12 | # These are wrapper/implementation of API methods described on: https://api-docs.midtrans.com/#midtrans-api 13 | 14 | # get status of transaction that already recorded on midtrans (already `charge`-ed) 15 | status_response = api_client.transactions.status('YOUR_ORDER_ID OR TRANSACTION_ID') 16 | 17 | # get transaction status of VA b2b transaction 18 | statusb2b_response = api_client.transactions.statusb2b('YOUR_ORDER_ID OR TRANSACTION_ID') 19 | 20 | # approve a credit card transaction with `challenge` fraud status 21 | approve_response = api_client.transactions.approve('YOUR_ORDER_ID OR TRANSACTION_ID') 22 | 23 | # deny a credit card transaction with `challenge` fraud status 24 | deny_response = api_client.transactions.deny('YOUR_ORDER_ID OR TRANSACTION_ID') 25 | 26 | # cancel a credit card transaction or pending transaction 27 | cancel_response = api_client.transactions.cancel('YOUR_ORDER_ID OR TRANSACTION_ID') 28 | 29 | # expire a pending transaction 30 | expire_response = api_client.transactions.expire('YOUR_ORDER_ID OR TRANSACTION_ID') 31 | 32 | # refund a transaction (not all payment channel allow refund via API) 33 | param = { 34 | "amount": 5000, 35 | "reason": "Item out of stock" 36 | } 37 | refund_response = api_client.transactions.refund('YOUR_ORDER_ID OR TRANSACTION_ID',param) -------------------------------------------------------------------------------- /examples/flask_app/templates/simple_checkout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Simple Snap Example 4 | 5 | 6 | 9 | 10 | 11 |
12 | 13 | 28 | 29 | 30 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /midtransclient/snap.py: -------------------------------------------------------------------------------- 1 | from .config import ApiConfig 2 | from .http_client import HttpClient 3 | from .transactions import Transactions 4 | 5 | class Snap: 6 | """ 7 | Snap object used to do request to Midtrans Snap API 8 | """ 9 | 10 | def __init__(self, 11 | is_production=False, 12 | server_key='', 13 | client_key='', 14 | custom_headers=dict(), 15 | proxies=dict()): 16 | 17 | self.api_config = ApiConfig(is_production,server_key,client_key,custom_headers,proxies) 18 | self.http_client = HttpClient() 19 | self.transactions = Transactions(self) 20 | 21 | @property 22 | def api_config(self): 23 | return self.__api_config 24 | 25 | @api_config.setter 26 | def api_config(self, new_value): 27 | self.__api_config = new_value 28 | 29 | def create_transaction(self,parameters=dict()): 30 | """ 31 | Trigger API call to Snap API 32 | :param parameters: dictionary of SNAP API JSON body as parameter, will be converted to JSON 33 | (more params detail refer to: https://snap-docs.midtrans.com) 34 | 35 | :return: Dictionary from JSON decoded response, that contains `token` and `redirect_url` 36 | """ 37 | api_url = self.api_config.get_snap_base_url()+'/transactions' 38 | 39 | response_dict, response_object = self.http_client.request( 40 | 'post', 41 | self.api_config.server_key, 42 | api_url, 43 | parameters, 44 | self.api_config.custom_headers, 45 | self.api_config.proxies) 46 | return response_dict 47 | 48 | def create_transaction_token(self,parameters=dict()): 49 | """ 50 | Wrapper method that call `create_transaction` and directly :return: `token` 51 | """ 52 | return self.create_transaction(parameters)['token'] 53 | 54 | def create_transaction_redirect_url(self,parameters=dict()): 55 | """ 56 | Wrapper method that call `create_transaction` and directly :return: `redirect_url` 57 | """ 58 | return self.create_transaction(parameters)['redirect_url'] 59 | -------------------------------------------------------------------------------- /Maintaining.md: -------------------------------------------------------------------------------- 1 | > Warning: This note is for developer/maintainer of this package only 2 | 3 | ## Updating Package 4 | 5 | - If from scratch, using `pipenv` 6 | - Install pipenv `pip install pipenv` or `pip3 install pipenv` 7 | - If fail, you may need to prefix the command with `sudo ` 8 | - CD to project directory 9 | - Install using pipenv `pipenv install` 10 | - If fail, you may need to specify which python bin file by `pipenv install --python /usr/bin/python3`. (Run `which python` or `which python3` to know the file path) 11 | - Activate and enter python env for current folder `pipenv shell`, now you are inside python env 12 | - Install project as local package `pip install -e .` 13 | - Make your code changes 14 | - Increase `version` value on: 15 | - `./setup.py` file 16 | - `./midtransclient/__init__.py` file 17 | - `./midtransclient/http_client.py` file on User-Agent value 18 | - To install the package locally with a symlink `pip install -e .` 19 | - To run test, run `pytest` 20 | - To run specific test, e.g: `pytest -k "test_core_api_charge_fail_401"` 21 | - If fail, you may need to install pytest first `pip install pytest` 22 | - To update https://pypi.org repo, run these on terminal: 23 | ```bash 24 | # install setuptools & wheel 25 | python -m pip install --upgrade setuptools wheel 26 | 27 | # Generate `dist/` folder for upload 28 | python setup.py sdist bdist_wheel 29 | 30 | # Update / install Twine 31 | python -m pip install --upgrade twine 32 | 33 | # upload to pypi / pip repository 34 | # you will be asked for username and password for https://pypi.org account 35 | twine upload dist/* --skip-existing 36 | 37 | # To upload to test pypi use this instead 38 | # twine upload --repository-url https://test.pypi.org/legacy/ dist/* --skip-existing; 39 | ``` 40 | - if fail to upload, clean up all files within your `./dist` folder, then re-try the above commands 41 | 42 | ## Dev & Test via Docker Compose 43 | 44 | - To use docker-compose to test and run project, `cd` to repo dir 45 | - Run `docker-compose up`, which basically run pytest on container 46 | - Run `docker-compose down`, to clean up when done 47 | 48 | ## TODO 49 | - need a better test cases / coverage 50 | - check if any test cases break due to post-1OMS API behavior changes -------------------------------------------------------------------------------- /examples/transaction_actions/notification_example.py: -------------------------------------------------------------------------------- 1 | import midtransclient 2 | # This is just for very basic implementation reference, in production, you should validate the incoming requests and implement your backend more securely. 3 | # Please refer to this docs for sample HTTP POST notifications: 4 | # https://docs.midtrans.com/en/after-payment/http-notification?id=sample-of-different-payment-channels 5 | 6 | # Initialize api client object 7 | # You can find it in Merchant Portal -> Settings -> Access keys 8 | api_client = midtransclient.CoreApi( 9 | is_production=False, 10 | server_key='YOUR_SERVER_KEY', 11 | client_key='YOUR_CLIENT_KEY' 12 | ) 13 | 14 | mock_notification = { 15 | 'currency': 'IDR', 16 | 'fraud_status': 'accept', 17 | 'gross_amount': '24145.00', 18 | 'order_id': 'test-transaction-321', 19 | 'payment_type': 'bank_transfer', 20 | 'status_code': '201', 21 | 'status_message': 'Success, Bank Transfer transaction is created', 22 | 'transaction_id': '6ee793df-9b1d-4343-8eda-cc9663b4222f', 23 | 'transaction_status': 'pending', 24 | 'transaction_time': '2018-10-24 15:34:33', 25 | 'va_numbers': [{'bank': 'bca', 'va_number': '490526303019299'}] 26 | } 27 | # handle notification JSON sent by Midtrans, it auto verify it by doing get status 28 | # parameter can be Dictionary or String of JSON 29 | status_response = api_client.transactions.notification(mock_notification) 30 | 31 | order_id = status_response['order_id'] 32 | transaction_status = status_response['transaction_status'] 33 | fraud_status = status_response['fraud_status'] 34 | 35 | print('Transaction notification received. Order ID: {0}. Transaction status: {1}. Fraud status: {2}'.format(order_id, 36 | transaction_status, 37 | fraud_status)) 38 | 39 | # Sample transaction_status handling logic 40 | 41 | if transaction_status == 'capture': 42 | if fraud_status == 'challenge': 43 | # TODO set transaction status on your databaase to 'challenge' 44 | None 45 | elif fraud_status == 'accept': 46 | # TODO set transaction status on your databaase to 'success' 47 | None 48 | elif transaction_status == 'cancel' or transaction_status == 'deny' or transaction_status == 'expire': 49 | # TODO set transaction status on your databaase to 'failure' 50 | None 51 | elif transaction_status == 'pending': 52 | # TODO set transaction status on your databaase to 'pending' / waiting payment 53 | None 54 | -------------------------------------------------------------------------------- /midtransclient/config.py: -------------------------------------------------------------------------------- 1 | class ApiConfig: 2 | """ 3 | Config Object that used to store is_production, server_key, client_key. 4 | And also API base urls. 5 | note: client_key is not necessarily required for API call. 6 | """ 7 | CORE_SANDBOX_BASE_URL = 'https://api.sandbox.midtrans.com'; 8 | CORE_PRODUCTION_BASE_URL = 'https://api.midtrans.com'; 9 | SNAP_SANDBOX_BASE_URL = 'https://app.sandbox.midtrans.com/snap/v1'; 10 | SNAP_PRODUCTION_BASE_URL = 'https://app.midtrans.com/snap/v1'; 11 | 12 | def __init__(self, 13 | is_production=False, 14 | server_key='', 15 | client_key='', 16 | custom_headers=dict(), 17 | proxies=dict()): 18 | self.is_production = is_production 19 | self.server_key = server_key 20 | self.client_key = client_key 21 | self.custom_headers = custom_headers 22 | self.proxies = proxies 23 | 24 | def get_core_api_base_url(self): 25 | if self.is_production: 26 | return self.CORE_PRODUCTION_BASE_URL 27 | return self.CORE_SANDBOX_BASE_URL 28 | 29 | def get_snap_base_url(self): 30 | if self.is_production: 31 | return self.SNAP_PRODUCTION_BASE_URL 32 | return self.SNAP_SANDBOX_BASE_URL 33 | 34 | # properties setter 35 | def set(self, 36 | is_production=None, 37 | server_key=None, 38 | client_key=None): 39 | if is_production is not None: 40 | self.is_production = is_production 41 | if server_key is not None: 42 | self.server_key = server_key 43 | if client_key is not None: 44 | self.client_key = client_key 45 | 46 | @property 47 | def server_key(self): 48 | return self.__server_key 49 | 50 | @server_key.setter 51 | def server_key(self, new_value): 52 | self.__server_key = new_value 53 | 54 | @property 55 | def client_key(self): 56 | return self.__client_key 57 | 58 | @client_key.setter 59 | def client_key(self, new_value): 60 | self.__client_key = new_value 61 | 62 | @property 63 | def custom_headers(self): 64 | return self.__custom_headers 65 | 66 | @custom_headers.setter 67 | def custom_headers(self, new_value): 68 | self.__custom_headers = new_value 69 | 70 | @property 71 | def proxies(self): 72 | return self.__proxies 73 | 74 | @proxies.setter 75 | def proxies(self, new_value): 76 | self.__proxies = new_value 77 | 78 | def __repr__(self): 79 | return ("".format(self.is_production, 80 | self.server_key, 81 | self.client_key, 82 | self.custom_headers, 83 | self.proxies)) -------------------------------------------------------------------------------- /examples/core_api/core_api_credit_card_example.py: -------------------------------------------------------------------------------- 1 | import midtransclient 2 | # This is just for very basic implementation reference, in production, you should validate the incoming requests and implement your backend more securely. 3 | 4 | # Initialize core api client object 5 | # You can find it in Merchant Portal -> Settings -> Access keys 6 | core = midtransclient.CoreApi( 7 | is_production=False, 8 | server_key='YOUR_SERVER_KEY', 9 | client_key='YOUR_CLIENT_KEY' 10 | ) 11 | 12 | # Alternative way to initialize CoreApi client object: 13 | # core = midtransclient.CoreApi() 14 | # core.api_config.set( 15 | # is_production=False, 16 | # server_key='YOUR_SERVER_KEY', 17 | # client_key='YOUR_CLIENT_KEY' 18 | # ) 19 | 20 | # Another alternative way to initialize CoreApi client object: 21 | # core = midtransclient.CoreApi() 22 | # core.api_config.is_production=False 23 | # core.api_config.server_key='YOUR_SERVER_KEY' 24 | # core.api_config.client_key='YOUR_CLIENT_KEY' 25 | 26 | # IMPORTANT NOTE: You should do credit card get token via frontend using `midtrans-new-3ds.min.js`, to avoid card data breach risks on your backend 27 | # ( refer to: https://docs.midtrans.com/en/core-api/credit-card?id=_1-getting-the-card-token ) 28 | # For full example on Credit Card 3DS transaction refer to: 29 | # (/examples/flask_app) that implement Snap & Core Api 30 | 31 | # prepare CORE API parameter to get credit card token 32 | # another sample of card number can refer to https://docs.midtrans.com/en/technical-reference/sandbox-test?id=card-payments 33 | params = { 34 | 'card_number': '5264 2210 3887 4659', 35 | 'card_exp_month': '12', 36 | 'card_exp_year': '2025', 37 | 'card_cvv': '123', 38 | 'client_key': core.api_config.client_key, 39 | } 40 | card_token_response = core.card_token(params) 41 | cc_token = card_token_response['token_id'] 42 | 43 | # prepare CORE API parameter to charge credit card ( refer to: https://docs.midtrans.com/en/core-api/credit-card?id=_2-sending-transaction-data-to-charge-api ) 44 | param = { 45 | "payment_type": "credit_card", 46 | "transaction_details": { 47 | "gross_amount": 12145, 48 | "order_id": "test-transaction-54321", 49 | }, 50 | "credit_card":{ 51 | "token_id": cc_token 52 | } 53 | } 54 | 55 | # charge transaction 56 | charge_response = core.charge(param) 57 | print('charge_response:') 58 | print(charge_response) 59 | 60 | # charge_response is dictionary representation of API JSON response 61 | # sample: 62 | # { 63 | # 'approval_code': '1540370521462', 64 | # 'bank': 'bni', 65 | # 'card_type': 'debit', 66 | # 'channel_response_code': '00', 67 | # 'channel_response_message': 'Approved', 68 | # 'currency': 'IDR', 69 | # 'fraud_status': 'accept', 70 | # 'gross_amount': '12145.00', 71 | # 'masked_card': '526422-4659', 72 | # 'order_id': 'test-transaction-54321', 73 | # 'payment_type': 'credit_card', 74 | # 'status_code': '200', 75 | # 'status_message': 'Success, Credit Card transaction is successful', 76 | # 'transaction_id': '2bc57149-b52b-46ff-b901-86418ad1abcc', 77 | # 'transaction_status': 'capture', 78 | # 'transaction_time': '2018-10-24 15:42:01' 79 | # } 80 | -------------------------------------------------------------------------------- /tests/test_http_client.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from unittest.mock import patch 3 | from .helpers import is_str 4 | from .context import HttpClient 5 | import datetime 6 | from pprint import pprint 7 | 8 | def test_http_client_class(): 9 | http_client = HttpClient() 10 | assert type(http_client.http_client).__name__ == 'module' 11 | 12 | def test_has_request_method(): 13 | http_client = HttpClient() 14 | methods = dir(http_client) 15 | assert 'request' in methods 16 | 17 | def test_can_raw_request_to_snap(): 18 | http_client = HttpClient() 19 | response_dict, response_object = http_client.request(method='post', 20 | server_key='SB-Mid-server-GwUP_WGbJPXsDzsNEBRs8IYA', 21 | request_url='https://app.sandbox.midtrans.com/snap/v1/transactions', 22 | parameters=generate_param_min()) 23 | assert isinstance(response_dict, dict) 24 | assert is_str(response_dict['token']) 25 | 26 | def test_fail_request_401_to_snap(): 27 | http_client = HttpClient() 28 | err = '' 29 | try: 30 | response_dict, response_object = http_client.request(method='post', 31 | server_key='wrong-server-key', 32 | request_url='https://app.sandbox.midtrans.com/snap/v1/transactions', 33 | parameters=generate_param_min()) 34 | except Exception as e: 35 | err = e 36 | assert 'MidtransAPIError' in err.__class__.__name__ 37 | assert is_str(err.message) 38 | assert isinstance(err.api_response_dict, dict) 39 | assert 401 == err.http_status_code 40 | 41 | def test_response_not_json_exception(): 42 | http_client = HttpClient() 43 | try: 44 | response = http_client.request(method='post', 45 | server_key='', 46 | request_url='https://midtrans.com/', 47 | parameters='') 48 | except Exception as e: 49 | assert 'JSONDecodeError' in repr(e) 50 | 51 | def test_is_custom_headers_applied(): 52 | http_client = HttpClient() 53 | 54 | custom_headers = { 55 | 'X-Override-Notification':'https://example.org' 56 | } 57 | 58 | # Mock requests 59 | with patch('requests.request') as mock_request: 60 | # Set status code to 200 to prevent MidtransAPIError 61 | mock_request.return_value.status_code = 200 62 | 63 | # Trigger request 64 | http_client.request(method='post', 65 | server_key='SB-Mid-server-GwUP_WGbJPXsDzsNEBRs8IYA', 66 | request_url='https://app.sandbox.midtrans.com/snap/v1/transactions', 67 | parameters=generate_param_min(), 68 | custom_headers=custom_headers) 69 | 70 | # Fetch the headers from requests.request arguments 71 | headers = mock_request.call_args[1]['headers'] 72 | 73 | # Make sure default header still exist 74 | assert headers.get('content-type') == 'application/json' 75 | 76 | # Assert custom headers 77 | assert 'X-Override-Notification' in headers 78 | assert headers.get('X-Override-Notification') == 'https://example.org' 79 | 80 | # TODO test GET request 81 | 82 | # ======== HELPER FUNCTIONS BELOW ======== # 83 | def generate_param_min(): 84 | return { 85 | "transaction_details": { 86 | "order_id": "py-midtransclient-test-"+datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), 87 | "gross_amount": 200000 88 | }, "credit_card":{ 89 | "secure" : True 90 | } 91 | } -------------------------------------------------------------------------------- /tests/test_tokenization.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from .config import USED_SERVER_KEY, USED_CLIENT_KEY 3 | from .helpers import is_str 4 | from .context import midtransclient 5 | import datetime 6 | import json 7 | from pprint import pprint 8 | 9 | PHONEUNREGISTERED = "123450001" 10 | PHONEBLOCKED = "123450002" 11 | ACCOUNT_ID = '' 12 | 13 | def test_tokenization_class(): 14 | tokenization = generate_tokenization_instance() 15 | methods = dir(tokenization) 16 | assert "link_payment_account" in methods 17 | assert is_str(tokenization.api_config.server_key) 18 | assert is_str(tokenization.api_config.client_key) 19 | 20 | def test_tokenization_link_account(): 21 | tokenization = generate_tokenization_instance() 22 | parameters = generate_param('81234567891') 23 | response = tokenization.link_payment_account(parameters) 24 | global ACCOUNT_ID 25 | ACCOUNT_ID = response['account_id'] 26 | assert isinstance(response, dict) 27 | assert 'account_id' in response.keys() 28 | assert response['status_code'] == '201' 29 | assert response['account_status'] == 'PENDING' 30 | 31 | def test_tokenization_link_account_user_not_found(): 32 | tokenization = generate_tokenization_instance() 33 | parameters = generate_param(PHONEUNREGISTERED) 34 | response = tokenization.link_payment_account(parameters) 35 | assert isinstance(response, dict) 36 | assert response['status_code'] == '202' 37 | assert response['channel_response_message'] == 'User Not Found' 38 | 39 | def test_tokenization_link_account_user_blocked(): 40 | tokenization = generate_tokenization_instance() 41 | parameters = generate_param(PHONEBLOCKED) 42 | response = tokenization.link_payment_account(parameters) 43 | assert isinstance(response, dict) 44 | assert response['status_code'] == '202' 45 | assert response['channel_response_message'] == 'Wallet is Blocked' 46 | 47 | def test_tokenization_link_account_phone_start_with_0(): 48 | tokenization = generate_tokenization_instance() 49 | parameters = generate_param('081234567891') 50 | err = '' 51 | try: 52 | response = tokenization.link_payment_account(parameters) 53 | except Exception as e: 54 | err = e 55 | assert 'MidtransAPIError' in err.__class__.__name__ 56 | assert '400' in err.message 57 | assert 'gopay_partner.phone_number must be numeric digits and must not begin with 0' in err.message 58 | 59 | def test_tokenization_get_account(): 60 | tokenization = generate_tokenization_instance() 61 | response = tokenization.get_payment_account(ACCOUNT_ID) 62 | assert isinstance(response, dict) 63 | assert response['status_code'] == '201' 64 | assert response['account_id'] == ACCOUNT_ID 65 | 66 | def test_tokenization_unlink_account(): 67 | tokenization = generate_tokenization_instance() 68 | err = '' 69 | try: 70 | response = tokenization.unlink_payment_account(ACCOUNT_ID) 71 | except Exception as e: 72 | err = e 73 | assert 'MidtransAPIError' in err.__class__.__name__ 74 | assert '412' in err.message 75 | assert 'Account status cannot be updated.' in err.message 76 | 77 | 78 | # ======== HELPER FUNCTIONS BELOW ======== # 79 | def generate_tokenization_instance(): 80 | tokenization = midtransclient.CoreApi(is_production=False, 81 | server_key=USED_SERVER_KEY, 82 | client_key=USED_CLIENT_KEY) 83 | return tokenization 84 | 85 | def generate_param(phone_number): 86 | return { 87 | "payment_type": "gopay", 88 | "gopay_partner": { 89 | "phone_number": phone_number, 90 | "country_code": "62", 91 | "redirect_url": "https://mywebstore.com/gopay-linking-finish" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /midtransclient/http_client.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import sys 4 | from .error_midtrans import MidtransAPIError 5 | from .error_midtrans import JSONDecodeError 6 | 7 | class HttpClient(object): 8 | """ 9 | Http Client Class that is wrapper to Python's `requests` module 10 | Used to do API call to Midtrans API urls. 11 | Capable of doing http :request: 12 | """ 13 | def __init__(self): 14 | self.http_client = requests 15 | 16 | def request(self, method, server_key, request_url, parameters=dict(), 17 | custom_headers=dict(), proxies=dict()): 18 | """ 19 | Perform http request to an url (supposedly Midtrans API url) 20 | :param method: http method 21 | :param server_key: Midtrans API server_key that will be used as basic auth header 22 | :param request_url: target http url 23 | :param parameters: dictionary of Midtrans API JSON body as parameter, will be converted to JSON 24 | 25 | :return: tuple of: 26 | response_dict: Dictionary from JSON decoded response 27 | response_object: Response object from `requests` 28 | """ 29 | 30 | # allow string of JSON to be used as parameters 31 | is_parameters_string = isinstance(parameters, str) 32 | if is_parameters_string: 33 | try: 34 | parameters = json.loads(parameters) 35 | except Exception as e: 36 | raise JSONDecodeError('fail to parse `parameters` string as JSON. Use JSON string or Dict as `parameters`. with message: `{0}`'.format(repr(e))) 37 | 38 | payload = json.dumps(parameters) if method != 'get' else parameters 39 | default_headers = { 40 | 'content-type': 'application/json', 41 | 'accept': 'application/json', 42 | 'user-agent': 'midtransclient-python/1.4.2' 43 | } 44 | headers = default_headers 45 | 46 | # only merge if custom headers exist 47 | if custom_headers: 48 | headers = {**default_headers, **custom_headers} 49 | 50 | response_object = self.http_client.request( 51 | method, 52 | request_url, 53 | auth=requests.auth.HTTPBasicAuth(server_key, ''), 54 | data=payload if method != 'get' else None, 55 | params=payload if method == 'get' else None, 56 | headers=headers, 57 | proxies=proxies, 58 | allow_redirects=True 59 | ) 60 | # catch response JSON decode error 61 | try: 62 | response_dict = response_object.json() 63 | except json.decoder.JSONDecodeError as e: 64 | raise JSONDecodeError('Fail to decode API response as JSON, API response is not JSON: `{0}`. with message: `{1}`'.format(response_object.text,repr(e))) 65 | 66 | # raise API error HTTP status code 67 | if response_object.status_code >= 400: 68 | raise MidtransAPIError( 69 | message='Midtrans API is returning API error. HTTP status code: `{0}`. ' 70 | 'API response: `{1}`'.format(response_object.status_code,response_object.text), 71 | api_response_dict=response_dict, 72 | http_status_code=response_object.status_code, 73 | raw_http_client_data=response_object 74 | ) 75 | # raise core API error status code 76 | if 'status_code' in response_dict.keys() and int(response_dict['status_code']) >= 400 and int(response_dict['status_code']) != 407: 77 | raise MidtransAPIError( 78 | 'Midtrans API is returning API error. API status code: `{0}`. ' 79 | 'API response: `{1}`'.format(response_dict['status_code'],response_object.text), 80 | api_response_dict=response_dict, 81 | http_status_code=response_object.status_code, 82 | raw_http_client_data=response_object 83 | ) 84 | 85 | return response_dict, response_object 86 | -------------------------------------------------------------------------------- /examples/subscription/credit_card_subscription_example.py: -------------------------------------------------------------------------------- 1 | # This is just for very basic implementation reference, in production, you should validate the incoming requests and implement your backend more securely. 2 | import midtransclient 3 | import datetime 4 | 5 | # Initialize core api client object 6 | # You can find it in Merchant Portal -> Settings -> Access keys 7 | core_api = midtransclient.CoreApi( 8 | is_production=False, 9 | server_key='YOUR_SERVER_KEY', 10 | client_key='YOUR_CLIENT_KEY' 11 | ) 12 | 13 | # To use API subscription for credit card, you should first obtain the 1 click token 14 | # Refer to this docs: https://docs.midtrans.com/en/core-api/advanced-features?id=recurring-transaction-with-subscriptions-api 15 | 16 | # You will receive saved_token_id as part of the response when the initial card payment is accepted (will also available in the HTTP notification's JSON) 17 | # Refer to this docs: https://docs.midtrans.com/en/core-api/advanced-features?id=sample-3ds-authenticate-json-response-for-the-first-transaction 18 | # { 19 | # ... 20 | # "card_type": "credit", 21 | # "saved_token_id":"481111xDUgxnnredRMAXuklkvAON1114", 22 | # "saved_token_id_expired_at": "2022-12-31 07:00:00", 23 | # ... 24 | # } 25 | # Sample saved token id for testing purpose 26 | SAVED_TOKEN_ID = '436502qFfqfAQKScMtPRPdZDOaeg7199' 27 | 28 | # prepare subscription parameter ( refer to: https://api-docs.midtrans.com/#create-subscription ) 29 | param = { 30 | "name": "SUBS-PY-"+datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), 31 | "amount": "100000", 32 | "currency": "IDR", 33 | "payment_type": "credit_card", 34 | "token": SAVED_TOKEN_ID, 35 | "schedule": { 36 | "interval": 1, 37 | "interval_unit": "day", 38 | "max_interval": 7 39 | }, 40 | "metadata": { 41 | "description": "Recurring payment for A" 42 | }, 43 | "customer_details": { 44 | "first_name": "John A", 45 | "last_name": "Doe A", 46 | "email": "johndoe@email.com", 47 | "phone": "+62812345678" 48 | } 49 | } 50 | 51 | # create subscription 52 | create_subscription_response = core_api.create_subscription(param) 53 | print('create_subscription_response:') 54 | print(create_subscription_response) 55 | 56 | # subscription_response is dictionary representation of API JSON response 57 | # sample: 58 | # { 59 | # 'id': 'b6eb6a04-33e6-46a2-a298-cd78e55b3a3f', 60 | # 'name': 'SUBS-PY-1', 61 | # 'amount': '100000', 62 | # 'currency': 'IDR', 63 | # 'created_at': '2021-10-27 13:29:51', 64 | # 'schedule': { 65 | # 'interval': 1, 66 | # 'current_interval': 0, 67 | # 'max_interval': 7, 68 | # 'interval_unit': 'day', 69 | # 'start_time': '2021-10-27 13:30:01', 70 | # 'next_execution_at': '2021-10-27 13:30:01' 71 | # }, 72 | # 'status': 'active', 73 | # 'token': '436502qFfqfAQKScMtPRPdZDOaeg7199', 74 | # 'payment_type': 'credit_card', 75 | # 'transaction_ids': [ 76 | 77 | # ], 78 | # 'metadata': { 79 | # 'description': 'Recurring payment for A' 80 | # }, 81 | # 'customer_details': { 82 | # 'email': 'johndoe@email.com', 83 | # 'first_name': 'John', 84 | # 'last_name': 'Doe', 85 | # 'phone': '+62812345678' 86 | # } 87 | # } 88 | 89 | subscription_id_response = create_subscription_response['id'] 90 | 91 | # get subscription by subscription_id 92 | get_subscription_response = core_api.get_subscription(subscription_id_response) 93 | print('get_subscription_response:') 94 | print(get_subscription_response) 95 | 96 | # enable subscription by subscription_id 97 | enable_subscription_response = core_api.enable_subscription(subscription_id_response) 98 | print('enable_subscription_response:') 99 | print(enable_subscription_response) 100 | 101 | # update subscription by subscription_id 102 | update_param = { 103 | "name": "SUBS-PY-UPDATE", 104 | "amount": "100000", 105 | "currency": "IDR", 106 | "token": SAVED_TOKEN_ID, 107 | "schedule": { 108 | "interval": 1 109 | } 110 | } 111 | update_subscription_response = core_api.update_subscription(subscription_id_response, update_param) 112 | print('update_subscription_response:') 113 | print(update_subscription_response) 114 | 115 | # disable subscription by subscription_id 116 | disable_subscription_response = core_api.disable_subscription(subscription_id_response) 117 | print('disable_subscription_response:') 118 | print(disable_subscription_response) 119 | -------------------------------------------------------------------------------- /examples/tokenization/tokenization_example.py: -------------------------------------------------------------------------------- 1 | # This is just for very basic implementation reference, in production, you should validate the incoming requests and implement your backend more securely. 2 | import midtransclient 3 | import datetime 4 | 5 | # Initialize core api client object 6 | # You can find it in Merchant Portal -> Settings -> Access keys 7 | core_api = midtransclient.CoreApi( 8 | is_production=False, 9 | server_key='YOUR_SERVER_KEY', 10 | client_key='YOUR_CLIENT_KEY' 11 | ) 12 | 13 | # prepare parameter ( refer to: https://api-docs.midtrans.com/#create-pay-account ) 14 | param = { 15 | "payment_type": "gopay", 16 | "gopay_partner": { 17 | "phone_number": "81234567891", 18 | "country_code": "62", 19 | "redirect_url": "https://mywebstore.com/gopay-linking-finish" #please update with your redirect URL 20 | } 21 | } 22 | 23 | # link payment account 24 | link_payment_account_response = core_api.link_payment_account(param) 25 | print('link_payment_account_response:') 26 | print(link_payment_account_response) 27 | 28 | # link_payment_account_response is dictionary representation of API JSON response 29 | # sample: 30 | # { 31 | # "status_code": "201", 32 | # "payment_type": "gopay", 33 | # "account_id": "6e902848-c4c3-4f40-abe6-109e8749df21", 34 | # "account_status": "PENDING", 35 | # "actions": [ 36 | # { 37 | # "name": "activation-deeplink", 38 | # "method": "GET", 39 | # "url": "https://api-v2.sandbox.midtrans.com/v2/pay/account/gpar_2b78cfea-2afa-49b3-86d5-3866626ce015/link" 40 | # }, 41 | # { 42 | # "name": "activation-link-url", 43 | # "method": "GET", 44 | # "url": "https://api-v2.sandbox.midtrans.com/v2/pay/account/gpar_2b78cfea-2afa-49b3-86d5-3866626ce015/link" 45 | # }, 46 | # { 47 | # "name": "activation-link-app", 48 | # "method": "GET", 49 | # "url": "https://simulator-v2.sandbox.midtrans.com/gopay/partner/web/otp?id=e4514b08-cc16-486e-9e4a-d8d8dd0bfe49" 50 | # } 51 | # ], 52 | # "metadata": { 53 | # "reference_id": "48b854c9-cc65-4af2-a3f0-38ae81798512" 54 | # } 55 | # } 56 | # for the first link, the account status is PENDING, you must activate it by accessing one of the URLs on the actions object 57 | 58 | # Sample active account id for testing purpose 59 | active_account_id = "6975fc98-8d44-490d-b50a-28d2810d6856" 60 | # get payment account by account_id 61 | get_payment_account_response = core_api.get_payment_account(active_account_id) 62 | print('get_payment_account_response:') 63 | print(get_payment_account_response) 64 | # sample 65 | # { 66 | # "status_code": "200", 67 | # "payment_type": "gopay", 68 | # "account_id": "6975fc98-8d44-490d-b50a-28d2810d6856", 69 | # "account_status": "ENABLED", 70 | # "metadata": { 71 | # "payment_options": [ 72 | # { 73 | # "name": "PAY_LATER", 74 | # "active": true, 75 | # "balance": { 76 | # "value": "4649999.00", 77 | # "currency": "IDR" 78 | # }, 79 | # "metadata": {}, 80 | # "token": "04ed77b7-5ad5-4ba5-b631-d72aa369c2f7" 81 | # }, 82 | # { 83 | # "name": "GOPAY_WALLET", 84 | # "active": true, 85 | # "balance": { 86 | # "value": "6100000.00", 87 | # "currency": "IDR" 88 | # }, 89 | # "metadata": {}, 90 | # "token": "8035816f-462e-4fa1-a5ef-c30bf71e2ee6" 91 | # } 92 | # ] 93 | # } 94 | # } 95 | 96 | # request charge 97 | params = { 98 | "payment_type": "gopay", 99 | "gopay": { 100 | "account_id": get_payment_account['account_id'], 101 | "payment_option_token": get_payment_account['metadata']['payment_options'][0]['token'], 102 | "callback_url": "https://mywebstore.com/gopay-linking-finish" #please update with your redirect URL 103 | }, 104 | "transaction_details": { 105 | "gross_amount": 100000, 106 | "order_id": "GOPAY-LINK-"+datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") 107 | } 108 | } 109 | charge_response = core_api.charge(params) 110 | print('charge_response:') 111 | print(charge_response) 112 | 113 | 114 | # unlink payment account by account_id 115 | # when account status still PENDING, you will get status code 412 116 | # sample response 117 | # { 118 | # "status_code": "412", 119 | # "status_message": "Account status cannot be updated.", 120 | # "id": "19eda9e4-37c9-4bfd-abb2-c60bb3a91084" 121 | # } 122 | try: 123 | unlink_payment_account_response = core_api.unlink_payment_account(link_payment_account['account_id']) 124 | print('unlink_payment_account_response:') 125 | print(unlink_payment_account_response) 126 | except Exception as e: 127 | print('unlink_failure_response:') 128 | print(e) 129 | -------------------------------------------------------------------------------- /midtransclient/transactions.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json 3 | 4 | class Transactions: 5 | """ 6 | These are wrapper/implementation of API methods described on: 7 | https://api-docs.midtrans.com/#midtrans-api 8 | """ 9 | 10 | def __init__(self,parent): 11 | self.parent = parent 12 | 13 | def status(self, transaction_id): 14 | api_url = self.parent.api_config.get_core_api_base_url()+'/v2/'+transaction_id+'/status' 15 | response_dict, response_object = self.parent.http_client.request( 16 | 'get', 17 | self.parent.api_config.server_key, 18 | api_url, 19 | dict(), 20 | self.parent.api_config.custom_headers, 21 | self.parent.api_config.proxies) 22 | return response_dict 23 | 24 | def statusb2b(self, transaction_id): 25 | api_url = self.parent.api_config.get_core_api_base_url()+'/v2/'+transaction_id+'/status/b2b' 26 | response_dict, response_object = self.parent.http_client.request( 27 | 'get', 28 | self.parent.api_config.server_key, 29 | api_url, 30 | dict(), 31 | self.parent.api_config.custom_headers, 32 | self.parent.api_config.proxies) 33 | return response_dict 34 | 35 | def approve(self, transaction_id): 36 | api_url = self.parent.api_config.get_core_api_base_url()+'/v2/'+transaction_id+'/approve' 37 | response_dict, response_object = self.parent.http_client.request( 38 | 'post', 39 | self.parent.api_config.server_key, 40 | api_url, 41 | dict(), 42 | self.parent.api_config.custom_headers, 43 | self.parent.api_config.proxies) 44 | return response_dict 45 | 46 | def deny(self, transaction_id): 47 | api_url = self.parent.api_config.get_core_api_base_url()+'/v2/'+transaction_id+'/deny' 48 | response_dict, response_object = self.parent.http_client.request( 49 | 'post', 50 | self.parent.api_config.server_key, 51 | api_url, 52 | dict(), 53 | self.parent.api_config.custom_headers, 54 | self.parent.api_config.proxies) 55 | return response_dict 56 | 57 | def cancel(self, transaction_id): 58 | api_url = self.parent.api_config.get_core_api_base_url()+'/v2/'+transaction_id+'/cancel' 59 | response_dict, response_object = self.parent.http_client.request( 60 | 'post', 61 | self.parent.api_config.server_key, 62 | api_url, 63 | dict(), 64 | self.parent.api_config.custom_headers, 65 | self.parent.api_config.proxies) 66 | return response_dict 67 | 68 | def expire(self, transaction_id): 69 | api_url = self.parent.api_config.get_core_api_base_url()+'/v2/'+transaction_id+'/expire' 70 | response_dict, response_object = self.parent.http_client.request( 71 | 'post', 72 | self.parent.api_config.server_key, 73 | api_url, 74 | dict(), 75 | self.parent.api_config.custom_headers, 76 | self.parent.api_config.proxies) 77 | return response_dict 78 | 79 | def refund(self, transaction_id,parameters=dict()): 80 | api_url = self.parent.api_config.get_core_api_base_url()+'/v2/'+transaction_id+'/refund' 81 | response_dict, response_object = self.parent.http_client.request( 82 | 'post', 83 | self.parent.api_config.server_key, 84 | api_url, 85 | parameters, 86 | self.parent.api_config.custom_headers, 87 | self.parent.api_config.proxies) 88 | return response_dict 89 | 90 | def refundDirect(self, transaction_id,parameters=dict()): 91 | api_url = self.parent.api_config.get_core_api_base_url()+'/v2/'+transaction_id+'/refund/online/direct' 92 | response_dict, response_object = self.parent.http_client.request( 93 | 'post', 94 | self.parent.api_config.server_key, 95 | api_url, 96 | parameters, 97 | self.parent.api_config.custom_headers, 98 | self.parent.api_config.proxies) 99 | return response_dict 100 | 101 | def notification(self, notification=dict()): 102 | is_notification_string = isinstance(notification, str) 103 | if is_notification_string: 104 | try: 105 | notification = json.loads(notification) 106 | except Exception as e: 107 | raise JSONDecodeError('fail to parse `notification` string as JSON. Use JSON string or Dict as `notification`. with message: `{0}`'.format(repr(e))) 108 | 109 | transaction_id = notification['transaction_id'] 110 | api_url = self.parent.api_config.get_core_api_base_url()+'/v2/'+transaction_id+'/status' 111 | response_dict, response_object = self.parent.http_client.request( 112 | 'get', 113 | self.parent.api_config.server_key, 114 | api_url, 115 | self.parent.api_config.custom_headers, 116 | self.parent.api_config.proxies) 117 | return response_dict 118 | 119 | class JSONDecodeError(Exception): 120 | pass -------------------------------------------------------------------------------- /examples/snap/snap_advanced_example.py: -------------------------------------------------------------------------------- 1 | import midtransclient 2 | # This is just for very basic implementation reference, in production, you should validate the incoming requests and implement your backend more securely. 3 | # Please refer to this docs for snap popup: 4 | # https://docs.midtrans.com/en/snap/integration-guide?id=integration-steps-overview 5 | 6 | # Please refer to this docs for snap-redirect: 7 | # https://docs.midtrans.com/en/snap/integration-guide?id=alternative-way-to-display-snap-payment-page-via-redirect 8 | 9 | # Initialize snap client object 10 | # You can find it in Merchant Portal -> Settings -> Access keys 11 | snap = midtransclient.Snap( 12 | is_production=False, 13 | server_key='YOUR_SERVER_KEY', 14 | client_key='YOUR_CLIENT_KEY' 15 | ) 16 | 17 | # Alternative way to initialize snap client object: 18 | # snap = midtransclient.Snap() 19 | # snap.api_config.set( 20 | # is_production=False, 21 | # server_key='YOUR_SERVER_KEY', 22 | # client_key='YOUR_CLIENT_KEY' 23 | # ) 24 | 25 | # Another alternative way to initialize snap client object: 26 | # snap = midtransclient.Snap() 27 | # snap.api_config.is_production=False 28 | # snap.api_config.server_key='YOUR_SERVER_KEY' 29 | # snap.api_config.client_key='YOUR_CLIENT_KEY' 30 | 31 | # prepare SNAP API parameter ( refer to: https://snap-docs.midtrans.com ) this is full parameter including optionals parameter. 32 | param = { 33 | "transaction_details": { 34 | "order_id": "test-transaction-1234", 35 | "gross_amount": 10000 36 | }, 37 | "item_details": [{ 38 | "id": "ITEM1", 39 | "price": 10000, 40 | "quantity": 1, 41 | "name": "Midtrans Bear", 42 | "brand": "Midtrans", 43 | "category": "Toys", 44 | "merchant_name": "Midtrans" 45 | }], 46 | "customer_details": { 47 | "first_name": "John", 48 | "last_name": "Watson", 49 | "email": "test@example.com", 50 | "phone": "+628123456", 51 | "billing_address": { 52 | "first_name": "John", 53 | "last_name": "Watson", 54 | "email": "test@example.com", 55 | "phone": "081 2233 44-55", 56 | "address": "Sudirman", 57 | "city": "Jakarta", 58 | "postal_code": "12190", 59 | "country_code": "IDN" 60 | }, 61 | "shipping_address": { 62 | "first_name": "John", 63 | "last_name": "Watson", 64 | "email": "test@example.com", 65 | "phone": "0 8128-75 7-9338", 66 | "address": "Sudirman", 67 | "city": "Jakarta", 68 | "postal_code": "12190", 69 | "country_code": "IDN" 70 | } 71 | }, 72 | "enabled_payments": ["credit_card", "mandiri_clickpay", "cimb_clicks","bca_klikbca", "bca_klikpay", "bri_epay", "echannel", "indosat_dompetku","mandiri_ecash", "permata_va", "bca_va", "bni_va", "other_va", "gopay","kioson", "indomaret", "gci", "danamon_online"], 73 | "credit_card": { 74 | "secure": True, 75 | "bank": "bca", 76 | "installment": { 77 | "required": False, 78 | "terms": { 79 | "bni": [3, 6, 12], 80 | "mandiri": [3, 6, 12], 81 | "cimb": [3], 82 | "bca": [3, 6, 12], 83 | "offline": [6, 12] 84 | } 85 | }, 86 | "whitelist_bins": [ 87 | "48111111", 88 | "41111111" 89 | ] 90 | }, 91 | "bca_va": { 92 | "va_number": "12345678911", 93 | "free_text": { 94 | "inquiry": [ 95 | { 96 | "en": "text in English", 97 | "id": "text in Bahasa Indonesia" 98 | } 99 | ], 100 | "payment": [ 101 | { 102 | "en": "text in English", 103 | "id": "text in Bahasa Indonesia" 104 | } 105 | ] 106 | } 107 | }, 108 | "bni_va": { 109 | "va_number": "12345678" 110 | }, 111 | "permata_va": { 112 | "va_number": "1234567890", 113 | "recipient_name": "SUDARSONO" 114 | }, 115 | "callbacks": { 116 | "finish": "https://demo.midtrans.com" 117 | }, 118 | "expiry": { 119 | "start_time": "2025-12-20 18:11:08 +0700", 120 | "unit": "minute", 121 | "duration": 9000 122 | }, 123 | "custom_field1": "custom field 1 content", 124 | "custom_field2": "custom field 2 content", 125 | "custom_field3": "custom field 3 content" 126 | } 127 | 128 | # create transaction 129 | transaction = snap.create_transaction(param) 130 | 131 | # transaction token 132 | transaction_token = transaction['token'] 133 | print('transaction_token:') 134 | print(transaction_token) 135 | 136 | # transaction redirect url 137 | transaction_url = transaction['redirect_url'] 138 | print('transaction_url:') 139 | print(transaction_url) 140 | 141 | # alternative way to create transaction_token: 142 | transaction_token = snap.create_transaction_token(param) 143 | print('transaction_token:') 144 | print(transaction_token) 145 | 146 | # alternative way to create transaction_url: 147 | transaction_url = snap.create_transaction_redirect_url(param) 148 | print('transaction_url:') 149 | print(transaction_url) 150 | -------------------------------------------------------------------------------- /examples/subscription/gopay_subscription_example.py: -------------------------------------------------------------------------------- 1 | # This is just for very basic implementation reference, in production, you should validate the incoming requests and implement your backend more securely. 2 | import midtransclient 3 | import datetime 4 | 5 | # Initialize core api client object 6 | # You can find it in Merchant Portal -> Settings -> Access keys 7 | core_api = midtransclient.CoreApi( 8 | is_production=False, 9 | server_key='YOUR_SERVER_KEY', 10 | client_key='YOUR_CLIENT_KEY' 11 | ) 12 | 13 | # To use API subscription for gopay, you should first link your customer gopay account with gopay tokenization 14 | # Refer to this docs: https://api-docs.midtrans.com/#gopay-tokenization 15 | 16 | # You will receive gopay payment token using `get_payment_account` API call. 17 | # You can see some Tokenization API examples here (examples/tokenization) 18 | # { 19 | # "status_code": "200", 20 | # "payment_type": "gopay", 21 | # "account_id": "6975fc98-8d44-490d-b50a-28d2810d6856", 22 | # "account_status": "ENABLED", 23 | # "metadata": { 24 | # "payment_options": [ 25 | # { 26 | # "name": "PAY_LATER", 27 | # "active": true, 28 | # "balance": { 29 | # "value": "4649999.00", 30 | # "currency": "IDR" 31 | # }, 32 | # "metadata": {}, 33 | # "token": "04ed77b7-5ad5-4ba5-b631-d72aa369c2f7" 34 | # }, 35 | # { 36 | # "name": "GOPAY_WALLET", 37 | # "active": true, 38 | # "balance": { 39 | # "value": "6100000.00", 40 | # "currency": "IDR" 41 | # }, 42 | # "metadata": {}, 43 | # "token": "8035816f-462e-4fa1-a5ef-c30bf71e2ee6" 44 | # } 45 | # ] 46 | # } 47 | # } 48 | # Sample gopay payment option token and gopay account id for testing purpose that has been already activated before 49 | GOPAY_PAYMENT_OPTION_TOKEN = '04ed77b7-5ad5-4ba5-b631-d72aa369c2f7' 50 | ACTIVE_ACCOUNT_ID = '6975fc98-8d44-490d-b50a-28d2810d6856' 51 | 52 | # prepare subscription parameter ( refer to: https://api-docs.midtrans.com/#create-subscription ) 53 | param = { 54 | "name": "SUBS-PY-"+datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), 55 | "amount": "100000", 56 | "currency": "IDR", 57 | "payment_type": "gopay", 58 | "token": GOPAY_PAYMENT_OPTION_TOKEN, 59 | "schedule": { 60 | "interval": 1, 61 | "interval_unit": "day", 62 | "max_interval": 7 63 | }, 64 | "metadata": { 65 | "description": "Recurring payment for A" 66 | }, 67 | "customer_details": { 68 | "first_name": "John A", 69 | "last_name": "Doe A", 70 | "email": "johndoe@email.com", 71 | "phone": "+62812345678" 72 | }, 73 | "gopay": { 74 | "account_id": ACTIVE_ACCOUNT_ID 75 | } 76 | } 77 | 78 | # create subscription 79 | create_subscription_response = core_api.create_subscription(param) 80 | print('create_subscription_response:') 81 | print(create_subscription_response) 82 | 83 | # subscription_response is dictionary representation of API JSON response 84 | # sample: 85 | # { 86 | # 'id': 'b6eb6a04-33e6-46a2-a298-cd78e55b3a3f', 87 | # 'name': 'SUBS-PY-1', 88 | # 'amount': '100000', 89 | # 'currency': 'IDR', 90 | # 'created_at': '2021-10-27 13:29:51', 91 | # 'schedule': { 92 | # 'interval': 1, 93 | # 'current_interval': 0, 94 | # 'max_interval': 7, 95 | # 'interval_unit': 'day', 96 | # 'start_time': '2021-10-27 13:30:01', 97 | # 'next_execution_at': '2021-10-27 13:30:01' 98 | # }, 99 | # 'status': 'active', 100 | # 'token': '436502qFfqfAQKScMtPRPdZDOaeg7199', 101 | # 'payment_type': 'gopay', 102 | # 'transaction_ids': [ 103 | 104 | # ], 105 | # 'metadata': { 106 | # 'description': 'Recurring payment for A' 107 | # }, 108 | # 'customer_details': { 109 | # 'email': 'johndoe@email.com', 110 | # 'first_name': 'John', 111 | # 'last_name': 'Doe', 112 | # 'phone': '+62812345678' 113 | # } 114 | # } 115 | 116 | subscription_id_response = create_subscription_response['id'] 117 | 118 | # get subscription by subscription_id 119 | get_subscription_response = core_api.get_subscription(subscription_id_response) 120 | print('get_subscription_response:') 121 | print(get_subscription_response) 122 | 123 | # enable subscription by subscription_id 124 | enable_subscription_response = core_api.enable_subscription(subscription_id_response) 125 | print('enable_subscription_response:') 126 | print(enable_subscription_response) 127 | 128 | # update subscription by subscription_id 129 | update_param = { 130 | "name": "SUBS-PY-UPDATE", 131 | "amount": "100000", 132 | "currency": "IDR", 133 | "token": GOPAY_PAYMENT_OPTION_TOKEN, 134 | "schedule": { 135 | "interval": 1 136 | } 137 | } 138 | update_subscription_response = core_api.update_subscription(subscription_id_response, update_param) 139 | print('update_subscription_response:') 140 | print(update_subscription_response) 141 | 142 | # disable subscription by subscription_id 143 | disable_subscription_response = core_api.disable_subscription(subscription_id_response) 144 | print('disable_subscription_response:') 145 | print(disable_subscription_response) 146 | -------------------------------------------------------------------------------- /tests/test_subscription.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from .config import USED_SERVER_KEY, USED_CLIENT_KEY 3 | from .helpers import is_str 4 | from .context import midtransclient 5 | import datetime 6 | import json 7 | from pprint import pprint 8 | 9 | SUBSCRIPTION_ID = '' 10 | 11 | def test_subscription_class(): 12 | subscription = generate_subscription_instance() 13 | methods = dir(subscription) 14 | assert "create_subscription" in methods 15 | assert is_str(subscription.api_config.server_key) 16 | assert is_str(subscription.api_config.client_key) 17 | 18 | def test_subscription_create_subscription(): 19 | subscription = generate_subscription_instance() 20 | parameters = generate_param() 21 | response = subscription.create_subscription(parameters) 22 | global SUBSCRIPTION_ID 23 | SUBSCRIPTION_ID = response['id'] 24 | assert isinstance(response, dict) 25 | assert 'id' in response.keys() 26 | assert response['status'] == 'active' 27 | 28 | def test_subscription_fail_empty_param(): 29 | subscription = generate_subscription_instance() 30 | parameters = None 31 | err = '' 32 | try: 33 | response = subscription.create_subscription(parameters) 34 | except Exception as e: 35 | err = e 36 | assert 'MidtransAPIError' in err.__class__.__name__ 37 | assert '400' in err.message 38 | assert 'Bad request' in err.message 39 | 40 | def test_subscription_fail_zero_amount(): 41 | subscription = generate_subscription_instance() 42 | parameters = generate_param() 43 | parameters['amount'] = 0 44 | err = '' 45 | try: 46 | response = subscription.create_subscription(parameters) 47 | except Exception as e: 48 | err = e 49 | assert 'MidtransAPIError' in err.__class__.__name__ 50 | assert '400' in err.message 51 | assert 'subscription.amount must be between 0.01 - 99999999999.00' in err.message 52 | 53 | def test_subscription_get_subscription(): 54 | subscription = generate_subscription_instance() 55 | parameters = generate_param() 56 | response = subscription.get_subscription(SUBSCRIPTION_ID) 57 | assert isinstance(response, dict) 58 | assert response['id'] == SUBSCRIPTION_ID 59 | assert response['status'] == 'active' 60 | 61 | def test_subscription_get_subscription_not_found(): 62 | subscription = generate_subscription_instance() 63 | err = '' 64 | try: 65 | response = subscription.get_subscription('123') 66 | except Exception as e: 67 | err = e 68 | assert 'MidtransAPIError' in err.__class__.__name__ 69 | assert '404' in err.message 70 | assert 'Subscription doesn\'t exist.' in err.message 71 | 72 | def test_subscription_disable_subscription(): 73 | subscription = generate_subscription_instance() 74 | response = subscription.disable_subscription(SUBSCRIPTION_ID) 75 | assert isinstance(response, dict) 76 | assert response['status_message'] == 'Subscription is updated.' 77 | get_subscription = subscription.get_subscription(SUBSCRIPTION_ID) 78 | assert get_subscription['id'] == SUBSCRIPTION_ID 79 | assert get_subscription['status'] == 'inactive' 80 | 81 | def test_subscription_enable_subscription(): 82 | subscription = generate_subscription_instance() 83 | response = subscription.enable_subscription(SUBSCRIPTION_ID) 84 | assert isinstance(response, dict) 85 | assert response['status_message'] == 'Subscription is updated.' 86 | get_subscription = subscription.get_subscription(SUBSCRIPTION_ID) 87 | assert get_subscription['id'] == SUBSCRIPTION_ID 88 | assert get_subscription['status'] == 'active' 89 | # disable subscription to prevent Core API continue to execute subscription 90 | response = subscription.disable_subscription(SUBSCRIPTION_ID) 91 | 92 | def test_subscription_update_subscription(): 93 | subscription = generate_subscription_instance() 94 | parameters = generate_param() 95 | parameters['metadata']['description'] = 'update recurring payment to ABC' 96 | response = subscription.update_subscription(SUBSCRIPTION_ID,parameters) 97 | assert isinstance(response, dict) 98 | assert response['status_message'] == 'Subscription is updated.' 99 | get_subscription = subscription.get_subscription(SUBSCRIPTION_ID) 100 | assert get_subscription['id'] == SUBSCRIPTION_ID 101 | assert get_subscription['metadata']['description'] == 'update recurring payment to ABC' 102 | 103 | # ======== HELPER FUNCTIONS BELOW ======== # 104 | def generate_subscription_instance(): 105 | subscription = midtransclient.CoreApi(is_production=False, 106 | server_key=USED_SERVER_KEY, 107 | client_key=USED_CLIENT_KEY) 108 | return subscription 109 | 110 | def generate_param(): 111 | return { 112 | "name": "SUBS-PY-"+datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"), 113 | "amount": "100000", 114 | "currency": "IDR", 115 | "payment_type": "credit_card", 116 | "token": "436502qFfqfAQKScMtPRPdZDOaeg7199", 117 | "schedule": { 118 | "interval": 1, 119 | "interval_unit": "day", 120 | "max_interval": 7 121 | }, 122 | "metadata": { 123 | "description": "Recurring payment for A" 124 | }, 125 | "customer_details": { 126 | "first_name": "John A", 127 | "last_name": "Doe A", 128 | "email": "johndoe@email.com", 129 | "phone": "+62812345678" 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /examples/flask_app/templates/simple_core_api_checkout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Checkout 5 | 6 | 7 | 8 | 9 | 10 | 11 |

Checkout

12 |
13 |
14 | Checkout 15 | Field that may be presented to customer: 16 |

17 | 18 | 19 |

20 |

21 | 22 | 23 | / 24 | 25 |

26 |

27 | 28 | 29 |

30 | 31 | Fields that shouldn't be presented to the customer: 32 |

33 | 34 | 35 |

36 | 37 | 38 | 39 |
40 |
41 | 42 | 43 | Transaction Result: 44 |
 Awaiting transactions... 
45 | Transaction verified status result: 46 |
 Awaiting transactions... 
47 |
 48 |   Testing cards:
 49 | 
 50 |     For 3D Secure:
 51 |     Visa success              4811 1111 1111 1114
 52 |     Visa deny by bank         4711 1111 1111 1115
 53 |     Visa deny by FDS          4611 1111 1111 1116
 54 | 
 55 |     MasterCard success        5211 1111 1111 1117
 56 |     MasterCard deny by bank   5111 1111 1111 1118
 57 |     MasterCard deny by FDS    5411 1111 1111 1115
 58 | 
 59 |     Challenge by FDS          4511 1111 1111 1117 
 60 |     
61 |
62 | 63 |
64 | Check `web.py` file, section `Using Core API - Credit Card` for the backend implementation 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 189 | 190 | -------------------------------------------------------------------------------- /tests/test_snap.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from .config import USED_SERVER_KEY, USED_CLIENT_KEY 3 | from .helpers import is_str 4 | from .context import midtransclient 5 | import datetime 6 | from pprint import pprint 7 | 8 | reused_order_id = "py-midtransclient-test-"+str(datetime.datetime.now()).replace(" ", "").replace(":", "") 9 | 10 | def test_snap_class(): 11 | snap = generate_snap_instance() 12 | methods = dir(snap) 13 | assert "create_transaction" in methods 14 | assert "create_transaction_token" in methods 15 | assert "create_transaction_redirect_url" in methods 16 | assert is_str(snap.api_config.server_key) 17 | assert is_str(snap.api_config.client_key) 18 | 19 | def test_snap_create_transaction_min(): 20 | snap = generate_snap_instance() 21 | param = generate_param_min() 22 | param['transaction_details']['order_id'] = reused_order_id 23 | transaction = snap.create_transaction(param) 24 | assert isinstance(transaction, dict) 25 | assert is_str(transaction['token']) 26 | assert is_str(transaction['redirect_url']) 27 | 28 | def test_snap_create_transaction_max(): 29 | snap = generate_snap_instance() 30 | param = generate_param_max() 31 | transaction = snap.create_transaction(param) 32 | assert isinstance(transaction, dict) 33 | assert is_str(transaction['token']) 34 | assert is_str(transaction['redirect_url']) 35 | 36 | def test_snap_create_transaction_token(): 37 | snap = generate_snap_instance() 38 | param = generate_param_min() 39 | token = snap.create_transaction_token(param) 40 | assert is_str(token) 41 | 42 | def test_snap_create_transaction_redirect_url(): 43 | snap = generate_snap_instance() 44 | param = generate_param_min() 45 | redirect_url = snap.create_transaction_redirect_url(param) 46 | assert is_str(redirect_url) 47 | 48 | def test_snap_status_fail_404(): 49 | snap = generate_snap_instance() 50 | err = '' 51 | try: 52 | response = snap.transactions.status('non-exist-order-id') 53 | except Exception as e: 54 | err = e 55 | assert 'MidtransAPIError' in err.__class__.__name__ 56 | assert '404' in err.message 57 | assert 'exist' in err.message 58 | 59 | def test_snap_request_fail_401(): 60 | snap = generate_snap_instance() 61 | snap.api_config.server_key='dummy' 62 | param = generate_param_min() 63 | err = '' 64 | try: 65 | transaction = snap.create_transaction(param) 66 | except Exception as e: 67 | err = e 68 | assert 'MidtransAPIError' in err.__class__.__name__ 69 | assert '401' in err.message 70 | assert 'unauthorized' in err.message 71 | 72 | def test_snap_request_fail_empty_param(): 73 | snap = generate_snap_instance() 74 | param = None 75 | err = '' 76 | try: 77 | transaction = snap.create_transaction(param) 78 | except Exception as e: 79 | err = e 80 | assert 'MidtransAPIError' in err.__class__.__name__ 81 | assert '400' in err.message 82 | assert 'is required' in err.message 83 | 84 | def test_snap_request_fail_zero_gross_amount(): 85 | snap = generate_snap_instance() 86 | param = generate_param_min() 87 | param['transaction_details']['gross_amount'] = 0 88 | err = '' 89 | try: 90 | transaction = snap.create_transaction(param) 91 | except Exception as e: 92 | err = e 93 | assert 'MidtransAPIError' in err.__class__.__name__ 94 | 95 | def test_snap_exception_MidtransAPIError(): 96 | snap = generate_snap_instance() 97 | snap.api_config.server_key='dummy' 98 | param = generate_param_min() 99 | err = '' 100 | try: 101 | transaction = snap.create_transaction(param) 102 | except Exception as e: 103 | err = e 104 | assert 'MidtransAPIError' in err.__class__.__name__ 105 | assert is_str(err.message) 106 | assert isinstance(err.api_response_dict, dict) 107 | assert isinstance(err.http_status_code,int) 108 | 109 | def test_snap_create_transaction_min_with_custom_headers_via_setter(): 110 | snap = generate_snap_instance() 111 | snap.api_config.custom_headers = { 112 | 'X-Override-Notification':'https://example.org' 113 | } 114 | param = generate_param_min() 115 | param['transaction_details']['order_id'] = reused_order_id 116 | transaction = snap.create_transaction(param) 117 | assert isinstance(transaction, dict) 118 | assert is_str(transaction['token']) 119 | assert is_str(transaction['redirect_url']) 120 | 121 | # ======== HELPER FUNCTIONS BELOW ======== # 122 | def generate_snap_instance(): 123 | snap = midtransclient.Snap(is_production=False, 124 | server_key=USED_SERVER_KEY, 125 | client_key=USED_CLIENT_KEY) 126 | return snap 127 | 128 | def generate_param_min(): 129 | return { 130 | "transaction_details": { 131 | "order_id": "py-midtransclient-test-"+datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), 132 | "gross_amount": 200000 133 | }, "credit_card":{ 134 | "secure" : True 135 | } 136 | } 137 | 138 | def generate_param_max(): 139 | return { 140 | "transaction_details": { 141 | "order_id": "py-midtransclient-test-"+datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), 142 | "gross_amount": 10000 143 | }, 144 | "item_details": [{ 145 | "id": "ITEM1", 146 | "price": 10000, 147 | "quantity": 1, 148 | "name": "Midtrans Bear", 149 | "brand": "Midtrans", 150 | "category": "Toys", 151 | "merchant_name": "Midtrans" 152 | }], 153 | "customer_details": { 154 | "first_name": "John", 155 | "last_name": "Watson", 156 | "email": "test@example.com", 157 | "phone": "+628123456", 158 | "billing_address": { 159 | "first_name": "John", 160 | "last_name": "Watson", 161 | "email": "test@example.com", 162 | "phone": "081 2233 44-55", 163 | "address": "Sudirman", 164 | "city": "Jakarta", 165 | "postal_code": "12190", 166 | "country_code": "IDN" 167 | }, 168 | "shipping_address": { 169 | "first_name": "John", 170 | "last_name": "Watson", 171 | "email": "test@example.com", 172 | "phone": "0 8128-75 7-9338", 173 | "address": "Sudirman", 174 | "city": "Jakarta", 175 | "postal_code": "12190", 176 | "country_code": "IDN" 177 | } 178 | }, 179 | "enabled_payments": ["credit_card", "mandiri_clickpay", "cimb_clicks","bca_klikbca", "bca_klikpay", "bri_epay", "echannel", "indosat_dompetku","mandiri_ecash", "permata_va", "bca_va", "bni_va", "other_va", "gopay","kioson", "indomaret", "gci", "danamon_online"], 180 | "credit_card": { 181 | "secure": True, 182 | "channel": "migs", 183 | "bank": "bca", 184 | "installment": { 185 | "required": False, 186 | "terms": { 187 | "bni": [3, 6, 12], 188 | "mandiri": [3, 6, 12], 189 | "cimb": [3], 190 | "bca": [3, 6, 12], 191 | "offline": [6, 12] 192 | } 193 | }, 194 | "whitelist_bins": [ 195 | "48111111", 196 | "41111111" 197 | ] 198 | }, 199 | "bca_va": { 200 | "va_number": "12345678911", 201 | "free_text": { 202 | "inquiry": [ 203 | { 204 | "en": "text in English", 205 | "id": "text in Bahasa Indonesia" 206 | } 207 | ], 208 | "payment": [ 209 | { 210 | "en": "text in English", 211 | "id": "text in Bahasa Indonesia" 212 | } 213 | ] 214 | } 215 | }, 216 | "bni_va": { 217 | "va_number": "12345678" 218 | }, 219 | "permata_va": { 220 | "va_number": "1234567890", 221 | "recipient_name": "SUDARSONO" 222 | }, 223 | "callbacks": { 224 | "finish": "https://demo.midtrans.com" 225 | }, 226 | "expiry": { 227 | "start_time": "2030-12-20 18:11:08 +0700", 228 | "unit": "minutes", 229 | "duration": 1 230 | }, 231 | "custom_field1": "custom field 1 content", 232 | "custom_field2": "custom field 2 content", 233 | "custom_field3": "custom field 3 content" 234 | } -------------------------------------------------------------------------------- /examples/flask_app/web.py: -------------------------------------------------------------------------------- 1 | # This is just for very basic implementation reference, in production, you should validate the incoming requests and implement your backend more securely. 2 | 3 | import datetime 4 | import json 5 | import os 6 | from flask import Flask, render_template, request, jsonify 7 | from midtransclient import Snap, CoreApi 8 | 9 | # @TODO: Change/fill the following API Keys variable with Your own server & client keys 10 | # You can find it in Merchant Portal -> Settings -> Access keys 11 | SERVER_KEY = 'SB-Mid-server-GwUP_WGbJPXsDzsNEBRs8IYA' 12 | CLIENT_KEY = 'SB-Mid-client-61XuGAwQ8Bj8LxSS' 13 | # Note: by default it uses hardcoded sandbox demo API keys for demonstration purpose 14 | 15 | app = Flask(__name__) 16 | 17 | #==============# 18 | # Using SNAP 19 | #==============# 20 | 21 | # Very simple Snap checkout 22 | @app.route('/simple_checkout') 23 | def simple_checkout(): 24 | snap = Snap( 25 | is_production=False, 26 | server_key=SERVER_KEY, 27 | client_key=CLIENT_KEY 28 | ) 29 | timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 30 | transaction_token = snap.create_transaction_token({ 31 | "transaction_details": { 32 | "order_id": "order-id-python-"+timestamp, 33 | "gross_amount": 200000 34 | }, "credit_card":{ 35 | "secure" : True 36 | } 37 | }) 38 | 39 | return render_template('simple_checkout.html', 40 | token = transaction_token, 41 | client_key = snap.api_config.client_key) 42 | 43 | #==============# 44 | # Using Core API - Credit Card 45 | #==============# 46 | 47 | # [0] Setup API client and config 48 | core = CoreApi( 49 | is_production=False, 50 | server_key=SERVER_KEY, 51 | client_key=CLIENT_KEY 52 | ) 53 | # [1] Render HTML+JS web page to get card token_id and [3] 3DS authentication 54 | @app.route('/simple_core_api_checkout') 55 | def simple_core_api_checkout(): 56 | return render_template('simple_core_api_checkout.html', 57 | client_key = core.api_config.client_key) 58 | 59 | # [2] Handle Core API credit card token_id charge 60 | @app.route('/charge_core_api_ajax', methods=['POST']) 61 | def charge_core_api_ajax(): 62 | request_json = request.get_json() 63 | timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 64 | try: 65 | charge_api_response = core.charge({ 66 | "payment_type": "credit_card", 67 | "transaction_details": { 68 | "gross_amount": 200000, 69 | "order_id": "order-id-python-"+timestamp, 70 | }, 71 | "credit_card":{ 72 | "token_id": request_json['token_id'], 73 | "authentication": request_json['authenticate_3ds'], 74 | } 75 | }) 76 | except Exception as e: 77 | charge_api_response = e.api_response_dict 78 | return charge_api_response 79 | 80 | # [4] Handle Core API check transaction status 81 | @app.route('/check_transaction_status', methods=['POST']) 82 | def check_transaction_status(): 83 | request_json = request.get_json() 84 | transaction_status = core.transactions.status(request_json['transaction_id']) 85 | 86 | # [5.A] Handle transaction status on your backend 87 | # Sample transaction_status handling logic 88 | if transaction_status == 'capture': 89 | if fraud_status == 'challenge': 90 | # TODO set transaction status on your databaase to 'challenge' 91 | None 92 | elif fraud_status == 'accept': 93 | # TODO set transaction status on your databaase to 'success' 94 | None 95 | elif transaction_status == 'settlement': 96 | # TODO set transaction status on your databaase to 'success' 97 | # Note: Non-card transaction will become 'settlement' on payment success 98 | # Card transaction will also become 'settlement' D+1, which you can ignore 99 | # because most of the time 'capture' is enough to be considered as success 100 | None 101 | elif transaction_status == 'cancel' or transaction_status == 'deny' or transaction_status == 'expire': 102 | # TODO set transaction status on your databaase to 'failure' 103 | None 104 | elif transaction_status == 'pending': 105 | # TODO set transaction status on your databaase to 'pending' / waiting payment 106 | None 107 | elif transaction_status == 'refund': 108 | # TODO set transaction status on your databaase to 'refund' 109 | None 110 | return jsonify(transaction_status) 111 | 112 | #==============# 113 | # Handling HTTP Post Notification 114 | #==============# 115 | 116 | # [4] Handle Core API check transaction status 117 | @app.route('/notification_handler', methods=['POST']) 118 | def notification_handler(): 119 | request_json = request.get_json() 120 | transaction_status_dict = core.transactions.notification(request_json) 121 | 122 | order_id = request_json['order_id'] 123 | transaction_status = request_json['transaction_status'] 124 | fraud_status = request_json['fraud_status'] 125 | transaction_json = json.dumps(transaction_status_dict) 126 | 127 | summary = 'Transaction notification received. Order ID: {order_id}. Transaction status: {transaction_status}. Fraud status: {fraud_status}.
Raw notification object:
{transaction_json}
'.format(order_id=order_id,transaction_status=transaction_status,fraud_status=fraud_status,transaction_json=transaction_json) 128 | 129 | # [5.B] Handle transaction status on your backend 130 | # Sample transaction_status handling logic 131 | if transaction_status == 'capture': 132 | if fraud_status == 'challenge': 133 | # TODO set transaction status on your databaase to 'challenge' 134 | None 135 | elif fraud_status == 'accept': 136 | # TODO set transaction status on your databaase to 'success' 137 | None 138 | elif transaction_status == 'settlement': 139 | # TODO set transaction status on your databaase to 'success' 140 | # Note: Non card transaction will become 'settlement' on payment success 141 | # Credit card will also become 'settlement' D+1, which you can ignore 142 | # because most of the time 'capture' is enough to be considered as success 143 | None 144 | elif transaction_status == 'cancel' or transaction_status == 'deny' or transaction_status == 'expire': 145 | # TODO set transaction status on your databaase to 'failure' 146 | None 147 | elif transaction_status == 'pending': 148 | # TODO set transaction status on your databaase to 'pending' / waiting payment 149 | None 150 | elif transaction_status == 'refund': 151 | # TODO set transaction status on your databaase to 'refund' 152 | None 153 | app.logger.info(summary) 154 | return jsonify(summary) 155 | 156 | #==============# 157 | # Using Core API - other payment method, example: Permata VA 158 | #==============# 159 | @app.route('/simple_core_api_checkout_permata', methods=['GET']) 160 | def simple_core_api_checkout_permata(): 161 | timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 162 | charge_api_response = core.charge({ 163 | "payment_type": "bank_transfer", 164 | "transaction_details": { 165 | "gross_amount": 200000, 166 | "order_id": "order-id-python-"+timestamp, 167 | } 168 | }) 169 | 170 | return render_template('simple_core_api_checkout_permata.html', 171 | permata_va_number = charge_api_response['permata_va_number'], 172 | gross_amount = charge_api_response['gross_amount'], 173 | order_id = charge_api_response['order_id']) 174 | 175 | #==============# 176 | # Run Flask app 177 | #==============# 178 | 179 | # Homepage of this web app 180 | @app.route('/') 181 | def index(): 182 | if not SERVER_KEY or not CLIENT_KEY: 183 | # non-relevant function only used for demo/example purpose 184 | return printExampleWarningMessage() 185 | 186 | return render_template('index.html') 187 | 188 | # credit card frontend demo 189 | @app.route('/core_api_credit_card_frontend_sample') 190 | def core_api_credit_card_frontend_sample(): 191 | return render_template('core_api_credit_card_frontend_sample.html', 192 | client_key = core.api_config.client_key) 193 | 194 | 195 | def printExampleWarningMessage(): 196 | pathfile = os.path.abspath("web.py") 197 | message = "

Please set your server key and client key from sandbox

In file: " + pathfile 198 | message += "

# Set Your server key" 199 | message += "
# You can find it in Merchant Portal -> Settings -> Access keys" 200 | message += "
SERVER_KEY = ''" 201 | message += "
CLIENT_KEY = ''
" 202 | return message 203 | 204 | if __name__ == '__main__': 205 | app.run(debug=True,port=5000,host='0.0.0.0') 206 | -------------------------------------------------------------------------------- /examples/flask_app/static/checkout.css: -------------------------------------------------------------------------------- 1 | /* CSS credits to `usf` at https://codepen.io/usf/pen/GgbEa */ 2 | * { 3 | -moz-transition: all 0.3s; 4 | -o-transition: all 0.3s; 5 | -webkit-transition: all 0.3s; 6 | transition: all 0.3s; 7 | -moz-box-sizing: border-box; 8 | -webkit-box-sizing: border-box; 9 | box-sizing: border-box; 10 | } 11 | 12 | body { 13 | background: #ecece6; 14 | } 15 | 16 | @-webkit-keyframes $animation_name { 17 | from { 18 | -moz-transform: rotate(45deg) translate(-80px, 40px); 19 | -ms-transform: rotate(45deg) translate(-80px, 40px); 20 | -webkit-transform: rotate(45deg) translate(-80px, 40px); 21 | transform: rotate(45deg) translate(-80px, 40px); 22 | } 23 | to { 24 | -moz-transform: rotate(0deg) translate(0px, 0); 25 | -ms-transform: rotate(0deg) translate(0px, 0); 26 | -webkit-transform: rotate(0deg) translate(0px, 0); 27 | transform: rotate(0deg) translate(0px, 0); 28 | } 29 | } 30 | @-moz-keyframes $animation_name { 31 | from { 32 | -moz-transform: rotate(45deg) translate(-80px, 40px); 33 | -ms-transform: rotate(45deg) translate(-80px, 40px); 34 | -webkit-transform: rotate(45deg) translate(-80px, 40px); 35 | transform: rotate(45deg) translate(-80px, 40px); 36 | } 37 | to { 38 | -moz-transform: rotate(0deg) translate(0px, 0); 39 | -ms-transform: rotate(0deg) translate(0px, 0); 40 | -webkit-transform: rotate(0deg) translate(0px, 0); 41 | transform: rotate(0deg) translate(0px, 0); 42 | } 43 | } 44 | @-o-keyframes $animation_name { 45 | from { 46 | -moz-transform: rotate(45deg) translate(-80px, 40px); 47 | -ms-transform: rotate(45deg) translate(-80px, 40px); 48 | -webkit-transform: rotate(45deg) translate(-80px, 40px); 49 | transform: rotate(45deg) translate(-80px, 40px); 50 | } 51 | to { 52 | -moz-transform: rotate(0deg) translate(0px, 0); 53 | -ms-transform: rotate(0deg) translate(0px, 0); 54 | -webkit-transform: rotate(0deg) translate(0px, 0); 55 | transform: rotate(0deg) translate(0px, 0); 56 | } 57 | } 58 | @keyframes $animation_name { 59 | from { 60 | -moz-transform: rotate(45deg) translate(-80px, 40px); 61 | -ms-transform: rotate(45deg) translate(-80px, 40px); 62 | -webkit-transform: rotate(45deg) translate(-80px, 40px); 63 | transform: rotate(45deg) translate(-80px, 40px); 64 | } 65 | to { 66 | -moz-transform: rotate(0deg) translate(0px, 0); 67 | -ms-transform: rotate(0deg) translate(0px, 0); 68 | -webkit-transform: rotate(0deg) translate(0px, 0); 69 | transform: rotate(0deg) translate(0px, 0); 70 | } 71 | } 72 | /* button style */ 73 | .cart { 74 | width: 40px; 75 | height: 40px; 76 | padding: 0; 77 | margin: 0; 78 | margin-top: 100px; 79 | position: absolute; 80 | left: 50%; 81 | margin-left: -20px; 82 | -moz-border-radius: 9999em; 83 | -webkit-border-radius: 9999em; 84 | border-radius: 9999em; 85 | border: none; 86 | background: #e54040; 87 | cursor: pointer; 88 | } 89 | .cart:hover { 90 | -moz-box-shadow: inset 0 0 7px 0 rgba(0, 0, 0, 0.5); 91 | -webkit-box-shadow: inset 0 0 7px 0 rgba(0, 0, 0, 0.5); 92 | box-shadow: inset 0 0 7px 0 rgba(0, 0, 0, 0.5); 93 | } 94 | .cart/*:hover*/ .popup { 95 | visibility: visible; 96 | opacity: 1; 97 | pointer-events: auto; 98 | -webkit-animation-duration: 200ms; 99 | -webkit-animation-name: show-popup; 100 | -webkit-animation-direction: normal; 101 | -webkit-animation-timing-function: cubic-bezier(1, 0.18, 1, 0.93); 102 | -moz-animation-duration: 200ms; 103 | -moz-animation-name: show-popup; 104 | -moz-animation-direction: normal; 105 | -moz-animation-timing-function: cubic-bezier(1, 0.18, 1, 0.93); 106 | -o-animation-duration: 200ms; 107 | -o-animation-name: show-popup; 108 | -o-animation-direction: normal; 109 | -o-animation-timing-function: cubic-bezier(1, 0.18, 1, 0.93); 110 | animation-duration: 200ms; 111 | animation-name: show-popup; 112 | animation-direction: normal; 113 | animation-timing-function: cubic-bezier(1, 0.18, 1, 0.93); 114 | } 115 | 116 | /* popup window style */ 117 | .popup { 118 | visibility: hidden; 119 | opacity: 0; 120 | pointer-events: none; 121 | position: absolute; 122 | top: 100%; 123 | width: 250px; 124 | margin-left: -105px; 125 | margin-top: 20px; 126 | background: #ffffff; 127 | border: 1px solid #cbcbcb; 128 | -moz-border-radius: 5px; 129 | -webkit-border-radius: 5px; 130 | border-radius: 5px; 131 | -moz-box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.3); 132 | -webkit-box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.3); 133 | box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.3); 134 | } 135 | .popup:after { 136 | position: absolute; 137 | content: ' '; 138 | top: -30px; 139 | height: 30px; 140 | width: 100%; 141 | } 142 | .popup:before { 143 | position: absolute; 144 | content: ' '; 145 | left: 117px; 146 | top: -9px; 147 | width: 16px; 148 | height: 16px; 149 | border-top: 1px solid #cbcbcb; 150 | border-right: 1px solid #cbcbcb; 151 | background: #ffffff; 152 | -moz-box-shadow: 1px -1px 1px 0 rgba(0, 0, 0, 0.2); 153 | -webkit-box-shadow: 1px -1px 1px 0 rgba(0, 0, 0, 0.2); 154 | box-shadow: 1px -1px 1px 0 rgba(0, 0, 0, 0.2); 155 | -moz-transform: rotate(-45deg); 156 | -ms-transform: rotate(-45deg); 157 | -webkit-transform: rotate(-45deg); 158 | transform: rotate(-45deg); 159 | } 160 | 161 | /* data rows */ 162 | .row { 163 | padding: 15px 20px; 164 | overflow: hidden; 165 | } 166 | .row.header { 167 | background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2U3ZTdlNyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); 168 | background-size: 100%; 169 | background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #e7e7e7)); 170 | background-image: -moz-linear-gradient(top, #ffffff 0%, #e7e7e7 100%); 171 | background-image: -webkit-linear-gradient(top, #ffffff 0%, #e7e7e7 100%); 172 | background-image: linear-gradient(to bottom, #ffffff 0%, #e7e7e7 100%); 173 | background-image: -ms-linear-gradient(top, #ffffff 0%, #e7e7e7 100%); 174 | -moz-box-shadow: 0 1px 0 0 rgba(203, 203, 203, 0.75); 175 | -webkit-box-shadow: 0 1px 0 0 rgba(203, 203, 203, 0.75); 176 | box-shadow: 0 1px 0 0 rgba(203, 203, 203, 0.75); 177 | -moz-border-radius: 5px 5px 0 0; 178 | -webkit-border-radius: 5px; 179 | border-radius: 5px 5px 0 0; 180 | color: #747474; 181 | text-shadow: 1px 1px 1px rgba(255, 255, 255, 0.75); 182 | font: bold 11px Arial; 183 | } 184 | .row.items { 185 | color: #e54040; 186 | font: bold 18px Arial; 187 | position: relative; 188 | } 189 | .row.items span:first-child { 190 | color: #000000; 191 | } 192 | .row.items:after { 193 | content: ''; 194 | position: absolute; 195 | height: 1px; 196 | width: 100%; 197 | background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuMCIgeTE9IjAuNSIgeDI9IjEuMCIgeTI9IjAuNSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iNTAlIiBzdG9wLWNvbG9yPSIjZGRkZGRkIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjZmZmZmZmIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0idXJsKCNncmFkKSIgLz48L3N2Zz4g'); 198 | background-size: 100%; 199 | background-image: -webkit-gradient(linear, 0% 50%, 100% 50%, color-stop(0%, #ffffff), color-stop(50%, #dddddd), color-stop(100%, #ffffff)); 200 | background-image: -moz-linear-gradient(left, #ffffff 0%, #dddddd 50%, #ffffff 100%); 201 | background-image: -webkit-linear-gradient(left, #ffffff 0%, #dddddd 50%, #ffffff 100%); 202 | background-image: linear-gradient(to right, #ffffff 0%, #dddddd 50%, #ffffff 100%); 203 | left: 0; 204 | top: 97%; 205 | } 206 | .row.checkout { 207 | font: normal 12px Arial; 208 | } 209 | .row.checkout span:first-child { 210 | padding: 3px 0; 211 | } 212 | .row.checkout a { 213 | color: #e54040; 214 | text-decoration: none; 215 | } 216 | .row.checkout a:hover { 217 | text-decoration: underline; 218 | } 219 | .row span:first-child { 220 | float: left; 221 | } 222 | .row span:last-child { 223 | float: right; 224 | } 225 | 226 | .checkout-button { 227 | float: right; 228 | padding: 3px 5px; 229 | background: #e54040; 230 | -moz-box-shadow: inset 0 2px 7px 0 rgba(255, 255, 255, 0.3); 231 | -webkit-box-shadow: inset 0 2px 7px 0 rgba(255, 255, 255, 0.3); 232 | box-shadow: inset 0 2px 7px 0 rgba(255, 255, 255, 0.3); 233 | border: 1px solid #e06b6b; 234 | -moz-border-radius: 3px; 235 | -webkit-border-radius: 3px; 236 | border-radius: 3px; 237 | color: #ffffff; 238 | } 239 | .checkout-button:hover { 240 | background: #e54040; 241 | -moz-box-shadow: none; 242 | -webkit-box-shadow: none; 243 | box-shadow: none; 244 | } 245 | .checkout-button:active { 246 | background: #e54040; 247 | -moz-box-shadow: inset 0 1px 2px 0 rgba(0, 0, 0, 0.2); 248 | -webkit-box-shadow: inset 0 1px 2px 0 rgba(0, 0, 0, 0.2); 249 | box-shadow: inset 0 1px 2px 0 rgba(0, 0, 0, 0.2); 250 | } 251 | 252 | .cart { 253 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIGNIUk0AAHolAACAgwAA+f8AAIDoAABSCAABFVgAADqXAAAXb9daH5AAAAGPSURBVHjajNNNi85RGAbw3zwzI/XMmEHeFYmSvJRSYsOWfAHZ+wAWakp8AAs+A5ImKzujUfKapQnZzIaVxzCYGcMzXRbuydO/Wcxdd+ecq3Ouc67rvk9fkqcY8T8mcA3frSIGcLKBfcMaq4wWpjCNjwi2o71aggGcQT92YBybcLTIZ7ABX7G+xlF8qvWgJMs5kuRFksUkDypPJLmd5FSSW0lOJ7lT+GSSt70E7SQ38i+eJDmSZE+SA0n21bi35heTdJMstnrkzOMlfmELhnEVQxjDOlzBDxwv2c/7kvR6sh+Py8QLeFde9GMJ3arQw/JqrNUwtVMVaWMbFnC51pfwGQfrhR1MNgnm8LpuPVsNdr0236xKncdgyZ0eaBD8xhss4hyOlea1hQ1ha0mZwM8mwRKe4VVJ6K9Daj6PD9V0j7DUNBH6sLNcn8efOtytp7cxW82U3j5Yzs1J7ieZSnI3yXDho0nGC7+XZGMSKxEcSjJbDdVJsrvwww18VxKtFf7HTH2wObwvGfClgS/A3wEAjJAlBBKFdqAAAAAASUVORK5CYII=); 254 | background-repeat: no-repeat; 255 | background-position: center; 256 | } 257 | -------------------------------------------------------------------------------- /examples/flask_app/templates/core_api_credit_card_frontend_sample.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Core API Credit Card Frontend Sample 5 | 6 | 7 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |

This page is to further demonstrate how to implement frontend processes for Credit Card transactions

27 | 28 |
29 |
30 | 1. Get Card Token Step 31 |

32 | 33 | 34 |

35 |

36 | 37 | 38 | / 39 | 40 |

41 |

42 | 43 | 44 |

45 | 46 | 47 |

48 | 49 | 50 |

51 | 52 |

53 | 54 |

55 |
56 |

57 |
58 |
59 |
60 | 61 |
62 |
63 | 2. Charge process is done via Backend 64 |

65 | 66 | 67 |

68 |

69 | 70 | 71 |

72 | 73 | 74 |

75 | 76 | 77 |

78 | 79 | Check `web.py` file, section `/charge_core_api_ajax` for the backend implementation 80 |
// sample charge in CURL
 81 | curl -X POST \
 82 |   https://api.sandbox.midtrans.com/v2/charge \
 83 |   -H 'Accept: application/json' \
 84 |   -H 'Authorization: Basic < From your server_key >' \
 85 |   -H 'Content-Type: application/json' \
 86 |   -d '{
 87 |   "payment_type": "credit_card",
 88 |   "transaction_details": {
 89 |     "order_id": "order102-2-1562650595",
 90 |     "gross_amount": 100000
 91 |   },
 92 |   "credit_card": {
 93 |     "token_id": "< Token ID from Get Token Step >",
 94 |     "authentication": true
 95 |   }
 96 | }'
 97 |     
98 |
99 |
100 | 101 |
102 |
103 |
104 | 3. 3DS Authentication Step 105 |

106 | 107 | 108 |

109 | 110 | 111 |

112 | 113 |

...
114 |

115 | 116 |

117 | 118 |

119 |

120 |
121 |
122 | 123 | 124 | 150 | 151 | 152 | 223 | 224 | 225 | 274 | 275 | 276 | -------------------------------------------------------------------------------- /midtransclient/core_api.py: -------------------------------------------------------------------------------- 1 | from .config import ApiConfig 2 | from .http_client import HttpClient 3 | from .transactions import Transactions 4 | 5 | class CoreApi: 6 | """ 7 | CoreApi object used to do request to Midtrans Core API 8 | """ 9 | 10 | def __init__(self, 11 | is_production=False, 12 | server_key='', 13 | client_key='', 14 | custom_headers=dict(), 15 | proxies=dict()): 16 | 17 | self.api_config = ApiConfig(is_production,server_key,client_key,custom_headers,proxies) 18 | self.http_client = HttpClient() 19 | self.transactions = Transactions(self) 20 | 21 | @property 22 | def api_config(self): 23 | return self.__api_config 24 | 25 | @api_config.setter 26 | def api_config(self, new_value): 27 | self.__api_config = new_value 28 | 29 | def charge(self,parameters=dict()): 30 | """ 31 | Trigger `/charge` API call to Core API 32 | :param parameters: dictionary of Core API JSON body as parameter, will be converted to JSON 33 | (more params detail refer to: https://api-docs.midtrans.com) 34 | 35 | :return: Dictionary from JSON decoded response 36 | """ 37 | api_url = self.api_config.get_core_api_base_url()+'/v2/charge' 38 | 39 | response_dict, response_object = self.http_client.request( 40 | 'post', 41 | self.api_config.server_key, 42 | api_url, 43 | parameters, 44 | self.api_config.custom_headers, 45 | self.api_config.proxies) 46 | 47 | return response_dict 48 | 49 | def capture(self,parameters=dict()): 50 | """ 51 | Trigger `/capture` API call to Core API 52 | Capture is only used for pre-authorize transaction only 53 | :param parameters: dictionary of Core API JSON body as parameter, will be converted to JSON 54 | (more params detail refer to: https://api-docs.midtrans.com) 55 | 56 | :return: Dictionary from JSON decoded response 57 | """ 58 | api_url = self.api_config.get_core_api_base_url()+'/v2/capture' 59 | 60 | response_dict, response_object = self.http_client.request( 61 | 'post', 62 | self.api_config.server_key, 63 | api_url, 64 | parameters, 65 | self.api_config.custom_headers, 66 | self.api_config.proxies) 67 | 68 | return response_dict 69 | 70 | def card_register(self,parameters=dict()): 71 | """ 72 | Trigger `/card/register` API call to Core API 73 | :param parameters: dictionary of Core API JSON body as parameter, will be converted to JSON 74 | (more params detail refer to: https://api-docs.midtrans.com) 75 | 76 | :return: Dictionary from JSON decoded response 77 | """ 78 | api_url = self.api_config.get_core_api_base_url()+'/v2/card/register' 79 | 80 | response_dict, response_object = self.http_client.request( 81 | 'get', 82 | self.api_config.server_key, 83 | api_url, 84 | parameters, 85 | self.api_config.custom_headers, 86 | self.api_config.proxies) 87 | 88 | return response_dict 89 | 90 | def card_token(self,parameters=dict()): 91 | """ 92 | Trigger `/token` API call to Core API 93 | :param parameters: dictionary of Core API JSON body as parameter, will be converted to JSON 94 | (more params detail refer to: https://api-docs.midtrans.com) 95 | 96 | :return: Dictionary from JSON decoded response 97 | """ 98 | api_url = self.api_config.get_core_api_base_url()+'/v2/token' 99 | 100 | response_dict, response_object = self.http_client.request( 101 | 'get', 102 | self.api_config.server_key, 103 | api_url, 104 | parameters, 105 | self.api_config.custom_headers, 106 | self.api_config.proxies) 107 | 108 | return response_dict 109 | 110 | def card_point_inquiry(self,token_id): 111 | """ 112 | Trigger `/point_inquiry/` API call to Core API 113 | :param token_id: token id of credit card 114 | (more params detail refer to: https://api-docs.midtrans.com) 115 | 116 | :return: Dictionary from JSON decoded response 117 | """ 118 | api_url = self.api_config.get_core_api_base_url()+'/v2/point_inquiry/'+token_id 119 | 120 | response_dict, response_object = self.http_client.request( 121 | 'get', 122 | self.api_config.server_key, 123 | api_url, 124 | dict(), 125 | self.api_config.custom_headers, 126 | self.api_config.proxies) 127 | 128 | return response_dict 129 | 130 | def create_subscription(self,parameters=dict()): 131 | """ 132 | Trigger `/v1/subscriptions` API call to Core API 133 | Create a subscription transaction by sending all the details required to create a transaction 134 | :param parameters: dictionary of Core API JSON body as parameter, will be converted to JSON 135 | (more params detail refer to: https://api-docs.midtrans.com/#create-subscription) 136 | 137 | :return: Dictionary from JSON decoded response 138 | """ 139 | api_url = self.api_config.get_core_api_base_url()+'/v1/subscriptions' 140 | 141 | response_dict, response_object = self.http_client.request( 142 | 'post', 143 | self.api_config.server_key, 144 | api_url, 145 | parameters, 146 | self.api_config.custom_headers, 147 | self.api_config.proxies) 148 | 149 | return response_dict 150 | 151 | def get_subscription(self,subscription_id): 152 | """ 153 | Trigger `/v1/subscriptions/` API call to Core API 154 | Retrieve the subscription details of a customer using the subscription_id 155 | (more params detail refer to: https://api-docs.midtrans.com/#get-subscription) 156 | 157 | :return: Dictionary from JSON decoded response 158 | """ 159 | api_url = self.api_config.get_core_api_base_url()+'/v1/subscriptions/'+subscription_id 160 | 161 | response_dict, response_object = self.http_client.request( 162 | 'get', 163 | self.api_config.server_key, 164 | api_url, 165 | dict(), 166 | self.api_config.custom_headers, 167 | self.api_config.proxies) 168 | 169 | return response_dict 170 | 171 | def disable_subscription(self,subscription_id): 172 | """ 173 | Trigger `/v1/subscriptions//disable` API call to Core API 174 | Disable the customer's subscription. The customer will not be charged in the future for this subscription 175 | (more params detail refer to: https://api-docs.midtrans.com/#disable-subscription) 176 | 177 | :return: Dictionary from JSON decoded response 178 | """ 179 | api_url = self.api_config.get_core_api_base_url()+'/v1/subscriptions/'+subscription_id+'/disable' 180 | 181 | response_dict, response_object = self.http_client.request( 182 | 'post', 183 | self.api_config.server_key, 184 | api_url, 185 | dict(), 186 | self.api_config.custom_headers, 187 | self.api_config.proxies) 188 | 189 | return response_dict 190 | 191 | def enable_subscription(self,subscription_id): 192 | """ 193 | Trigger `/v1/subscriptions//enable` API call to Core API 194 | Enable the customer's subscription 195 | (more params detail refer to: https://api-docs.midtrans.com/#enable-subscription) 196 | 197 | :return: Dictionary from JSON decoded response 198 | """ 199 | api_url = self.api_config.get_core_api_base_url()+'/v1/subscriptions/'+subscription_id+'/enable' 200 | 201 | response_dict, response_object = self.http_client.request( 202 | 'post', 203 | self.api_config.server_key, 204 | api_url, 205 | dict(), 206 | self.api_config.custom_headers, 207 | self.api_config.proxies) 208 | 209 | return response_dict 210 | 211 | def update_subscription(self,subscription_id,parameters=dict()): 212 | """ 213 | Trigger `/v1/subscriptions/` API call to Core API 214 | Update existing subscription details 215 | (more params detail refer to: https://api-docs.midtrans.com/#update-subscription) 216 | 217 | :return: Dictionary from JSON decoded response 218 | """ 219 | api_url = self.api_config.get_core_api_base_url()+'/v1/subscriptions/'+subscription_id 220 | 221 | response_dict, response_object = self.http_client.request( 222 | 'patch', 223 | self.api_config.server_key, 224 | api_url, 225 | parameters, 226 | self.api_config.custom_headers, 227 | self.api_config.proxies) 228 | 229 | return response_dict 230 | 231 | def link_payment_account(self,parameters=dict()): 232 | """ 233 | Trigger `/v2/pay/account` API call to Core API 234 | Link the customer account to be used for specific payment channels. 235 | :param parameters: dictionary of Core API JSON body as parameter, will be converted to JSON 236 | (more params detail refer to: https://api-docs.midtrans.com/#create-pay-account) 237 | 238 | :return: Dictionary from JSON decoded response 239 | """ 240 | api_url = self.api_config.get_core_api_base_url()+'/v2/pay/account' 241 | 242 | response_dict, response_object = self.http_client.request( 243 | 'post', 244 | self.api_config.server_key, 245 | api_url, 246 | parameters, 247 | self.api_config.custom_headers, 248 | self.api_config.proxies) 249 | 250 | return response_dict 251 | 252 | def get_payment_account(self,account_id): 253 | """ 254 | Trigger `/v2/pay/account/` API call to Core API 255 | Retrieve the payment account details of a customer using the account_id 256 | (more params detail refer to: https://api-docs.midtrans.com/#get-pay-account) 257 | 258 | :return: Dictionary from JSON decoded response 259 | """ 260 | api_url = self.api_config.get_core_api_base_url()+'/v2/pay/account/'+account_id 261 | 262 | response_dict, response_object = self.http_client.request( 263 | 'get', 264 | self.api_config.server_key, 265 | api_url, 266 | dict(), 267 | self.api_config.custom_headers, 268 | self.api_config.proxies) 269 | 270 | return response_dict 271 | 272 | def unlink_payment_account(self,account_id): 273 | """ 274 | Trigger `/v2/pay/account//unbind` API call to Core API 275 | To remove the linked customer account 276 | (more params detail refer to: https://api-docs.midtrans.com/#unbind-pay-account) 277 | 278 | :return: Dictionary from JSON decoded response 279 | """ 280 | api_url = self.api_config.get_core_api_base_url()+'/v2/pay/account/'+account_id+'/unbind' 281 | 282 | response_dict, response_object = self.http_client.request( 283 | 'post', 284 | self.api_config.server_key, 285 | api_url, 286 | dict(), 287 | self.api_config.custom_headers, 288 | self.api_config.proxies) 289 | 290 | return response_dict 291 | -------------------------------------------------------------------------------- /tests/test_core_api.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from .config import USED_SERVER_KEY, USED_CLIENT_KEY 3 | from .helpers import is_str 4 | from .context import midtransclient 5 | import datetime 6 | import json 7 | from pprint import pprint 8 | 9 | REUSED_ORDER_ID = [ 10 | "py-midtransclient-test1-"+str(datetime.datetime.now()).replace(" ", "").replace(":", ""), 11 | "py-midtransclient-test2-"+str(datetime.datetime.now()).replace(" ", "").replace(":", ""), 12 | "py-midtransclient-test3-"+str(datetime.datetime.now()).replace(" ", "").replace(":", ""), 13 | ] 14 | CC_TOKEN = '' 15 | SAVED_CC_TOKEN = '' 16 | API_RESPONSE = '' 17 | 18 | def test_core_api_class(): 19 | core = generate_core_api_instance() 20 | methods = dir(core) 21 | assert "charge" in methods 22 | assert is_str(core.api_config.server_key) 23 | assert is_str(core.api_config.client_key) 24 | 25 | def test_core_api_card_token(): 26 | core = generate_core_api_instance() 27 | params = { 28 | 'card_number': '5264 2210 3887 4659', 29 | 'card_exp_month': '12', 30 | 'card_exp_year': '2030', 31 | 'card_cvv': '123', 32 | 'client_key': core.api_config.client_key, 33 | } 34 | response = core.card_token(params) 35 | assert isinstance(response, dict) 36 | assert int(response['status_code']) == 200 37 | global CC_TOKEN 38 | CC_TOKEN = response['token_id'] 39 | assert is_str(response['token_id']) 40 | 41 | def test_core_api_card_register(): 42 | core = generate_core_api_instance() 43 | params = { 44 | 'card_number': '4811 1111 1111 1114', 45 | 'card_exp_month': '12', 46 | 'card_exp_year': '2030', 47 | 'card_cvv': '123', 48 | 'client_key': core.api_config.client_key, 49 | } 50 | response = core.card_register(params) 51 | assert isinstance(response, dict) 52 | assert int(response['status_code']) == 200 53 | global SAVED_CC_TOKEN 54 | SAVED_CC_TOKEN = response['saved_token_id'] 55 | assert is_str(response['saved_token_id']) 56 | 57 | def test_core_api_card_point_inquiry_valid_bni_card(): 58 | core = generate_core_api_instance() 59 | try: 60 | response = core.card_point_inquiry(CC_TOKEN) 61 | except Exception as e: 62 | err = e 63 | assert is_str(response['status_message']) 64 | assert 'Success' in response['status_message'] 65 | assert is_str(response['point_balance_amount']) 66 | 67 | 68 | def test_core_api_charge_cc_simple(): 69 | core = generate_core_api_instance() 70 | parameters = generate_param_cc_min(order_id=REUSED_ORDER_ID[1],cc_token=CC_TOKEN) 71 | response = core.charge(parameters) 72 | assert isinstance(response, dict) 73 | assert int(response['status_code']) == 200 74 | assert response['transaction_status'] == 'capture' 75 | assert response['fraud_status'] == 'accept' 76 | 77 | def test_core_api_charge_cc_one_click(): 78 | core = generate_core_api_instance() 79 | parameters = generate_param_cc_min(order_id=REUSED_ORDER_ID[2],cc_token=SAVED_CC_TOKEN) 80 | response = core.charge(parameters) 81 | assert isinstance(response, dict) 82 | assert int(response['status_code']) == 200 83 | assert response['transaction_status'] == 'capture' 84 | assert response['fraud_status'] == 'accept' 85 | 86 | def test_core_api_charge_bank_transfer_bca_simple(): 87 | core = generate_core_api_instance() 88 | parameters = generate_param_min(REUSED_ORDER_ID[0]) 89 | response = core.charge(parameters) 90 | assert isinstance(response, dict) 91 | assert int(response['status_code']) == 201 92 | assert response['transaction_status'] == 'pending' 93 | 94 | def test_core_api_status(): 95 | core = generate_core_api_instance() 96 | response = core.transactions.status(REUSED_ORDER_ID[0]) 97 | global API_RESPONSE 98 | API_RESPONSE = response 99 | assert isinstance(response, dict) 100 | assert int(response['status_code']) == 201 101 | assert response['transaction_status'] == 'pending' 102 | 103 | # TODO test statusb2b 104 | 105 | def test_core_api_notification_from_dict(): 106 | core = generate_core_api_instance() 107 | response = core.transactions.notification(API_RESPONSE) 108 | assert isinstance(response, dict) 109 | assert int(response['status_code']) == 201 110 | assert response['transaction_status'] == 'pending' 111 | 112 | def test_core_api_notification_from_json(): 113 | core = generate_core_api_instance() 114 | response = core.transactions.notification(json.dumps(API_RESPONSE)) 115 | assert isinstance(response, dict) 116 | assert int(response['status_code']) == 201 117 | assert response['transaction_status'] == 'pending' 118 | 119 | def test_core_api_notification_from_json_fail(): 120 | core = generate_core_api_instance() 121 | err = '' 122 | try: 123 | response = core.transactions.notification('') 124 | except Exception as e: 125 | err = e 126 | assert 'JSONDecodeError' in repr(err) 127 | 128 | def test_core_api_expire(): 129 | core = generate_core_api_instance() 130 | response = core.transactions.expire(REUSED_ORDER_ID[0]) 131 | assert isinstance(response, dict) 132 | assert int(response['status_code']) == 407 133 | assert response['transaction_status'] == 'expire' 134 | 135 | def test_core_api_approve_fail_cannot_be_updated(): 136 | core = generate_core_api_instance() 137 | err = '' 138 | try: 139 | response = core.transactions.approve(REUSED_ORDER_ID[1]) 140 | except Exception as e: 141 | err = e 142 | assert 'MidtransAPIError' in err.__class__.__name__ 143 | assert '412' in err.message 144 | 145 | def test_core_api_deny_cannot_be_updated(): 146 | core = generate_core_api_instance() 147 | err = '' 148 | try: 149 | response = core.transactions.deny(REUSED_ORDER_ID[1]) 150 | except Exception as e: 151 | err = e 152 | assert 'MidtransAPIError' in err.__class__.__name__ 153 | assert '412' in err.message 154 | 155 | def test_core_api_cancel(): 156 | core = generate_core_api_instance() 157 | response = core.transactions.cancel(REUSED_ORDER_ID[1]) 158 | assert isinstance(response, dict) 159 | assert int(response['status_code']) == 200 160 | assert response['transaction_status'] == 'cancel' 161 | 162 | def test_core_api_refund_fail_not_yet_settlement(): 163 | core = generate_core_api_instance() 164 | params = { 165 | "refund_key": "order1-ref1", 166 | "amount": 5000, 167 | "reason": "for some reason" 168 | } 169 | err = '' 170 | try: 171 | response = core.transactions.refund(REUSED_ORDER_ID[2],params) 172 | except Exception as e: 173 | err = e 174 | assert 'MidtransAPIError' in err.__class__.__name__ 175 | assert '412' in err.message 176 | 177 | def test_core_api_direct_refund_fail_not_yet_settlement(): 178 | core = generate_core_api_instance() 179 | params = { 180 | "refund_key": "order1-ref1", 181 | "amount": 5000, 182 | "reason": "for some reason" 183 | } 184 | err = '' 185 | try: 186 | response = core.transactions.refundDirect(REUSED_ORDER_ID[2],params) 187 | except Exception as e: 188 | err = e 189 | assert 'MidtransAPIError' in err.__class__.__name__ 190 | assert '412' in err.message 191 | 192 | def test_core_api_status_fail_404(): 193 | core = generate_core_api_instance() 194 | err = '' 195 | try: 196 | response = core.transactions.status('non-exist-order-id') 197 | except Exception as e: 198 | err = e 199 | assert 'MidtransAPIError' in err.__class__.__name__ 200 | assert '404' in err.message 201 | assert 'exist' in err.message 202 | 203 | def test_core_api_status_server_key_change_via_property(): 204 | core = midtransclient.CoreApi(is_production=False,server_key='',client_key='') 205 | core.api_config.server_key = USED_SERVER_KEY 206 | response = core.transactions.status(REUSED_ORDER_ID[1]) 207 | assert isinstance(response, dict) 208 | assert int(response['status_code']) == 200 209 | assert response['transaction_status'] == 'cancel' 210 | 211 | def test_core_api_status_server_key_change_via_setter(): 212 | core = midtransclient.CoreApi(is_production=False, 213 | server_key=USED_SERVER_KEY, 214 | client_key='') 215 | assert core.api_config.is_production == False 216 | assert core.api_config.server_key == USED_SERVER_KEY 217 | try: 218 | response = core.transactions.status('non-exist-order-id') 219 | except Exception as e: 220 | assert '404' in e.message 221 | 222 | core.api_config.set(is_production=True, 223 | server_key='abc') 224 | assert core.api_config.is_production == True 225 | assert core.api_config.server_key == 'abc' 226 | try: 227 | response = core.transactions.status(REUSED_ORDER_ID[0]) 228 | except Exception as e: 229 | assert '401' in e.message 230 | 231 | core.api_config.set(is_production=False, 232 | server_key=USED_SERVER_KEY, 233 | client_key=USED_CLIENT_KEY) 234 | assert core.api_config.is_production == False 235 | assert core.api_config.server_key == USED_SERVER_KEY 236 | assert core.api_config.client_key == USED_CLIENT_KEY 237 | response = core.transactions.status(REUSED_ORDER_ID[1]) 238 | assert isinstance(response, dict) 239 | assert int(response['status_code']) == 200 240 | assert response['transaction_status'] == 'cancel' 241 | 242 | def test_core_api_charge_fail_401(): 243 | core = generate_core_api_instance() 244 | core.api_config.server_key='invalidkey' 245 | parameters = generate_param_min() 246 | err = '' 247 | try: 248 | response = core.charge(parameters) 249 | except Exception as e: 250 | err = e 251 | assert 'MidtransAPIError' in err.__class__.__name__ 252 | assert '401' in err.message 253 | # assert 'authorized' in err.message # disabled due to 1OMS changed the err.message to no longer contains this keyword 254 | 255 | def test_core_api_charge_fail_empty_param(): 256 | core = generate_core_api_instance() 257 | parameters = None 258 | err = '' 259 | try: 260 | response = core.charge(parameters) 261 | except Exception as e: 262 | err = e 263 | assert 'MidtransAPIError' in err.__class__.__name__ 264 | assert '500' in err.message 265 | assert 'unexpected' in err.message 266 | 267 | def test_core_api_charge_fail_zero_gross_amount(): 268 | core = generate_core_api_instance() 269 | parameters = generate_param_min() 270 | parameters['transaction_details']['gross_amount'] = 0 271 | err = '' 272 | try: 273 | response = core.charge(parameters) 274 | except Exception as e: 275 | err = e 276 | assert 'MidtransAPIError' in err.__class__.__name__ 277 | assert '400' in err.message 278 | 279 | def test_core_api_exception_MidtransAPIError(): 280 | core = generate_core_api_instance() 281 | err = '' 282 | try: 283 | response = core.transactions.status('non-exist-order-id') 284 | except Exception as e: 285 | err = e 286 | assert 'MidtransAPIError' in err.__class__.__name__ 287 | assert is_str(err.message) 288 | assert isinstance(err.api_response_dict, dict) 289 | assert isinstance(err.http_status_code,int) 290 | 291 | # ======== HELPER FUNCTIONS BELOW ======== # 292 | def generate_core_api_instance(): 293 | core_api = midtransclient.CoreApi(is_production=False, 294 | server_key=USED_SERVER_KEY, 295 | client_key=USED_CLIENT_KEY) 296 | return core_api 297 | 298 | def generate_param_min(order_id=None): 299 | return { 300 | "payment_type": "bank_transfer", 301 | "transaction_details": { 302 | "gross_amount": 44145, 303 | "order_id": "py-midtransclient-test-"+datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") if order_id == None else order_id, 304 | }, 305 | "bank_transfer":{ 306 | "bank": "bca" 307 | } 308 | } 309 | 310 | def generate_param_cc_min(order_id=None,cc_token=None): 311 | return { 312 | "payment_type": "credit_card", 313 | "transaction_details": { 314 | "gross_amount": 12145, 315 | "order_id": "py-midtransclient-test-"+datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") if order_id == None else order_id, 316 | }, 317 | "credit_card":{ 318 | "token_id": cc_token 319 | } 320 | } 321 | 322 | def generate_param_max(): 323 | return {} -------------------------------------------------------------------------------- /examples/flask_app/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "4ea80d9da2829797976093ea53f673bce7374a60a8bd2777f2f7cfbdccdebd56" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", 22 | "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" 23 | ], 24 | "markers": "python_version >= '3.6'", 25 | "version": "==2022.12.7" 26 | }, 27 | "charset-normalizer": { 28 | "hashes": [ 29 | "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b", 30 | "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42", 31 | "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d", 32 | "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b", 33 | "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a", 34 | "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59", 35 | "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154", 36 | "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1", 37 | "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c", 38 | "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a", 39 | "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d", 40 | "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6", 41 | "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b", 42 | "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b", 43 | "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783", 44 | "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5", 45 | "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918", 46 | "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555", 47 | "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639", 48 | "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786", 49 | "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e", 50 | "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed", 51 | "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820", 52 | "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8", 53 | "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3", 54 | "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541", 55 | "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14", 56 | "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be", 57 | "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e", 58 | "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76", 59 | "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b", 60 | "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c", 61 | "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b", 62 | "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3", 63 | "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc", 64 | "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6", 65 | "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59", 66 | "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4", 67 | "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d", 68 | "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d", 69 | "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3", 70 | "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a", 71 | "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea", 72 | "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6", 73 | "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e", 74 | "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603", 75 | "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24", 76 | "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a", 77 | "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58", 78 | "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678", 79 | "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a", 80 | "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c", 81 | "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6", 82 | "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18", 83 | "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174", 84 | "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317", 85 | "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f", 86 | "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc", 87 | "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837", 88 | "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41", 89 | "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c", 90 | "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579", 91 | "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753", 92 | "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8", 93 | "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291", 94 | "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087", 95 | "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866", 96 | "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3", 97 | "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d", 98 | "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1", 99 | "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca", 100 | "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e", 101 | "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db", 102 | "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72", 103 | "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d", 104 | "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc", 105 | "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539", 106 | "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d", 107 | "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af", 108 | "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b", 109 | "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602", 110 | "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f", 111 | "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478", 112 | "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c", 113 | "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e", 114 | "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479", 115 | "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7", 116 | "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8" 117 | ], 118 | "markers": "python_version >= '3.6'", 119 | "version": "==3.0.1" 120 | }, 121 | "click": { 122 | "hashes": [ 123 | "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", 124 | "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" 125 | ], 126 | "markers": "python_version >= '3.7'", 127 | "version": "==8.1.3" 128 | }, 129 | "flask": { 130 | "hashes": [ 131 | "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", 132 | "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" 133 | ], 134 | "index": "pypi", 135 | "version": "==1.1.1" 136 | }, 137 | "idna": { 138 | "hashes": [ 139 | "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", 140 | "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" 141 | ], 142 | "markers": "python_version >= '3.5'", 143 | "version": "==3.4" 144 | }, 145 | "importlib-metadata": { 146 | "hashes": [ 147 | "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad", 148 | "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d" 149 | ], 150 | "markers": "python_version < '3.8'", 151 | "version": "==6.0.0" 152 | }, 153 | "itsdangerous": { 154 | "hashes": [ 155 | "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", 156 | "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a" 157 | ], 158 | "markers": "python_version >= '3.7'", 159 | "version": "==2.1.2" 160 | }, 161 | "jinja2": { 162 | "hashes": [ 163 | "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", 164 | "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" 165 | ], 166 | "markers": "python_version >= '3.7'", 167 | "version": "==3.1.2" 168 | }, 169 | "markupsafe": { 170 | "hashes": [ 171 | "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", 172 | "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", 173 | "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", 174 | "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", 175 | "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", 176 | "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", 177 | "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", 178 | "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", 179 | "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", 180 | "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", 181 | "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", 182 | "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", 183 | "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", 184 | "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", 185 | "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", 186 | "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", 187 | "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", 188 | "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", 189 | "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", 190 | "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", 191 | "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", 192 | "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", 193 | "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", 194 | "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", 195 | "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", 196 | "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", 197 | "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", 198 | "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", 199 | "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", 200 | "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", 201 | "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", 202 | "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", 203 | "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", 204 | "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", 205 | "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", 206 | "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", 207 | "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", 208 | "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", 209 | "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", 210 | "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", 211 | "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", 212 | "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", 213 | "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", 214 | "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", 215 | "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", 216 | "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", 217 | "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", 218 | "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", 219 | "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", 220 | "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" 221 | ], 222 | "markers": "python_version >= '3.7'", 223 | "version": "==2.1.2" 224 | }, 225 | "midtransclient": { 226 | "hashes": [ 227 | "sha256:02502fa63026f3294357f5234d06d56ac12bd4e793c898795d66ceefd614151a", 228 | "sha256:7107e2447633b8beb57701ccd2f4772a7a77813e6ab9a2a6d70ce0e008aca22d" 229 | ], 230 | "index": "pypi", 231 | "version": "==1.1.0" 232 | }, 233 | "requests": { 234 | "hashes": [ 235 | "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", 236 | "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" 237 | ], 238 | "markers": "python_version >= '3.7' and python_version < '4'", 239 | "version": "==2.28.2" 240 | }, 241 | "typing-extensions": { 242 | "hashes": [ 243 | "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", 244 | "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" 245 | ], 246 | "markers": "python_version < '3.8'", 247 | "version": "==4.5.0" 248 | }, 249 | "urllib3": { 250 | "hashes": [ 251 | "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72", 252 | "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1" 253 | ], 254 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 255 | "version": "==1.26.14" 256 | }, 257 | "werkzeug": { 258 | "hashes": [ 259 | "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe", 260 | "sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612" 261 | ], 262 | "index": "pypi", 263 | "version": "==2.2.3" 264 | }, 265 | "zipp": { 266 | "hashes": [ 267 | "sha256:23f70e964bc11a34cef175bc90ba2914e1e4545ea1e3e2f67c079671883f9cb6", 268 | "sha256:e8b2a36ea17df80ffe9e2c4fda3f693c3dad6df1697d3cd3af232db680950b0b" 269 | ], 270 | "markers": "python_version >= '3.7'", 271 | "version": "==3.13.0" 272 | } 273 | }, 274 | "develop": {} 275 | } 276 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Midtrans Client - Python 2 | =============== 3 | 4 | [![PyPI version](https://badge.fury.io/py/midtransclient.svg)](https://badge.fury.io/py/midtransclient) 5 | [![Downloads](https://pepy.tech/badge/midtransclient/month)](https://pepy.tech/project/midtransclient) 6 | [![Downloads](https://pepy.tech/badge/midtransclient)](https://pepy.tech/project/midtransclient) 7 | 8 | Midtrans ❤️ Python! 🐍 9 | 10 | This is the Official Python API client/library for Midtrans Payment API. Visit [https://midtrans.com](https://midtrans.com). More information about the product and see documentation at [http://docs.midtrans.com](https://docs.midtrans.com) for more technical details. 11 | 12 | ## 1. Installation 13 | 14 | ### 1.a Using Pip 15 | 16 | ``` 17 | pip install midtransclient 18 | ``` 19 | 20 | ### 1.b Manual Installation 21 | 22 | If you are not using Pip, you can clone or [download](https://github.com/midtrans/midtrans-python-client/archive/master.zip) this repository. 23 | Then import from `midtransclient` folder. 24 | 25 | Or run Pip install from the repo folder. 26 | ``` 27 | pip install . 28 | ``` 29 | 30 | ## 2. Usage 31 | 32 | ### 2.1 Choose Product/Method 33 | 34 | We have [2 different products](https://docs.midtrans.com/en/welcome/index.html) of payment that you can use: 35 | - [Snap](#22A-snap) - Customizable payment popup will appear on **your web/app** (no redirection). [doc ref](https://snap-docs.midtrans.com/) 36 | - [Snap Redirect](#22B-snap-redirect) - Customer need to be redirected to payment url **hosted by midtrans**. [doc ref](https://snap-docs.midtrans.com/) 37 | - [Core API (VT-Direct)](#22C-core-api-vt-direct) - Basic backend implementation, you can customize the frontend embedded on **your web/app** as you like (no redirection). [doc ref](https://api-docs.midtrans.com/) 38 | 39 | Choose one that you think best for your unique needs. 40 | 41 | ### 2.2 Client Initialization and Configuration 42 | 43 | Get your client key and server key from [Midtrans Dashboard](https://dashboard.midtrans.com) 44 | 45 | Create API client object 46 | 47 | ```python 48 | # Create Core API instance 49 | core_api = midtransclient.CoreApi( 50 | is_production=False, 51 | server_key='YOUR_SERVER_KEY', 52 | client_key='YOUR_CLIENT_KEY' 53 | ) 54 | ``` 55 | 56 | 57 | ```python 58 | # Create Snap API instance 59 | snap = midtransclient.Snap( 60 | is_production=False, 61 | server_key='YOUR_SERVER_KEY', 62 | client_key='YOUR_CLIENT_KEY' 63 | ) 64 | ``` 65 | 66 | You can also re-set config using `Snap.api_config.set( ... )` 67 | example: 68 | 69 | ```python 70 | 71 | # initialize object, empty config 72 | snap = midtransclient.Snap() 73 | 74 | # re-set full config 75 | snap.api_config.set( 76 | is_production=False, 77 | server_key='YOUR_SERVER_KEY', 78 | client_key='YOUR_CLIENT_KEY' 79 | ) 80 | 81 | # re-set server_key only 82 | snap.api_config.set(server_key='YOUR_SERVER_KEY') 83 | 84 | # re-set is_production only 85 | snap.api_config.set(is_production=True) 86 | ``` 87 | 88 | You can also set config directly from attribute 89 | ```python 90 | # initialize object, empty config 91 | snap = midtransclient.Snap() 92 | 93 | # set config 94 | snap.api_config.is_production=False 95 | snap.api_config.server_key='YOUR_SERVER_KEY' 96 | snap.api_config.client='YOUR_CLIENT_KEY' 97 | ``` 98 | 99 | 100 | ### 2.2.A Snap 101 | You can see Snap example [here](examples/snap). 102 | 103 | Available methods for `Snap` class 104 | ```python 105 | # return Snap API /transaction response as Dictionary 106 | def create_transactions(parameter): 107 | 108 | # return Snap API /transaction token as String 109 | def create_transactions_token(parameter): 110 | 111 | # return Snap API /transaction redirect_url as String 112 | def create_transactions_redirect_url(parameter): 113 | ``` 114 | `parameter` is Dictionary or String of JSON of [SNAP Parameter](https://snap-docs.midtrans.com/#json-objects) 115 | 116 | 117 | #### Get Snap Token 118 | 119 | ```python 120 | # Create Snap API instance 121 | snap = midtransclient.Snap( 122 | is_production=False, 123 | server_key='YOUR_SERVER_KEY', 124 | client_key='YOUR_CLIENT_KEY' 125 | ) 126 | # Prepare parameter 127 | param = { 128 | "transaction_details": { 129 | "order_id": "test-transaction-123", 130 | "gross_amount": 200000 131 | }, "credit_card":{ 132 | "secure" : True 133 | } 134 | } 135 | 136 | transaction = snap.create_transaction(param) 137 | 138 | transaction_token = transaction['token'] 139 | # alternative way to create transaction_token: 140 | # transaction_token = snap.create_transaction_token(param) 141 | ``` 142 | 143 | 144 | #### Initialize Snap JS when customer click pay button 145 | 146 | Replace `PUT_TRANSACTION_TOKEN_HERE` with `transaction_token` acquired above 147 | ```html 148 | 149 | 150 | 151 |
JSON result will appear here after payment:
152 | 153 | 154 | 155 | 174 | 175 | 176 | ``` 177 | 178 | #### Implement Notification Handler 179 | [Refer to this section](#23-handle-http-notification) 180 | 181 | ### 2.2.B Snap Redirect 182 | 183 | Also available as examples [here](examples/snap). 184 | 185 | #### Get Redirection URL of a Payment Page 186 | 187 | ```python 188 | # Create Snap API instance 189 | snap = midtransclient.Snap( 190 | is_production=False, 191 | server_key='YOUR_SERVER_KEY', 192 | client_key='YOUR_CLIENT_KEY' 193 | ) 194 | # Prepare parameter 195 | param = { 196 | "transaction_details": { 197 | "order_id": "test-transaction-123", 198 | "gross_amount": 200000 199 | }, "credit_card":{ 200 | "secure" : True 201 | } 202 | } 203 | 204 | transaction = snap.create_transaction(param) 205 | 206 | transaction_redirect_url = transaction['redirect_url'] 207 | # alternative way to create redirect_url: 208 | # transaction_redirect_url = snap.create_redirect_url(param) 209 | ``` 210 | #### Implement Notification Handler 211 | [Refer to this section](#23-handle-http-notification) 212 | 213 | ### 2.2.C Core API (VT-Direct) 214 | 215 | You can see some Core API examples [here](examples/core_api). 216 | 217 | Available methods for `CoreApi` class 218 | ```python 219 | def charge(self,parameters=dict()): 220 | """ 221 | Trigger `/charge` API call to Core API 222 | :param parameters: dictionary of Core API JSON body as parameter, will be converted to JSON 223 | (more params detail refer to: https://api-docs.midtrans.com) 224 | 225 | :return: Dictionary from JSON decoded response 226 | """ 227 | 228 | def capture(self,parameters=dict()): 229 | """ 230 | Trigger `/capture` API call to Core API 231 | Capture is only used for pre-authorize transaction only 232 | :param parameters: dictionary of Core API JSON body as parameter, will be converted to JSON 233 | (more params detail refer to: https://api-docs.midtrans.com) 234 | 235 | :return: Dictionary from JSON decoded response 236 | """ 237 | 238 | def card_register(self,parameters=dict()): 239 | """ 240 | Trigger `/card/register` API call to Core API 241 | :param parameters: dictionary of Core API JSON body as parameter, will be converted to JSON 242 | (more params detail refer to: https://api-docs.midtrans.com) 243 | 244 | :return: Dictionary from JSON decoded response 245 | """ 246 | 247 | def card_token(self,parameters=dict()): 248 | """ 249 | Trigger `/token` API call to Core API 250 | :param parameters: dictionary of Core API JSON body as parameter, will be converted to JSON 251 | (more params detail refer to: https://api-docs.midtrans.com) 252 | 253 | :return: Dictionary from JSON decoded response 254 | """ 255 | 256 | def card_point_inquiry(self,token_id): 257 | """ 258 | Trigger `/point_inquiry/` API call to Core API 259 | :param parameters: dictionary of Core API JSON body as parameter, will be converted to JSON 260 | (more params detail refer to: https://api-docs.midtrans.com) 261 | 262 | :return: Dictionary from JSON decoded response 263 | """ 264 | ``` 265 | `parameter` is Dictionary or String of JSON of [Core API Parameter](https://api-docs.midtrans.com/#json-objects) 266 | 267 | #### Credit Card Get Token 268 | 269 | Get token should be handled on Frontend please refer to [API docs](https://api-docs.midtrans.com) 270 | 271 | #### Credit Card Charge 272 | 273 | ```python 274 | # Create Core API instance 275 | core_api = midtransclient.Snap( 276 | is_production=False, 277 | server_key='YOUR_SERVER_KEY', 278 | client_key='YOUR_CLIENT_KEY' 279 | ) 280 | # Prepare parameter 281 | param = { 282 | "payment_type": "credit_card", 283 | "transaction_details": { 284 | "gross_amount": 12145, 285 | "order_id": "test-transaction-54321", 286 | }, 287 | "credit_card":{ 288 | "token_id": 'CREDIT_CARD_TOKEN', # change with your card token 289 | "authentication": True 290 | } 291 | } 292 | 293 | # charge transaction 294 | charge_response = core_api.charge(param) 295 | print('charge_response:') 296 | print(charge_response) 297 | ``` 298 | 299 | #### Credit Card 3DS Authentication 300 | 301 | The credit card charge result may contains `redirect_url` for 3DS authentication. 3DS Authentication should be handled on Frontend please refer to [API docs](https://api-docs.midtrans.com/#card-features-3d-secure) 302 | 303 | For full example on Credit Card 3DS transaction refer to: 304 | - [Flask App examples](/examples/flask_app) that implement Snap & Core Api 305 | 306 | ### 2.2.D Subscription API 307 | 308 | You can see some Subscription API examples [here](examples/subscription), [Subscription API Docs](https://api-docs.midtrans.com/#subscription-api) 309 | 310 | #### Subscription API for Credit Card 311 | 312 | To use subscription API for credit card, you should first obtain the 1-click saved token, [refer to this docs.](https://docs.midtrans.com/en/core-api/advanced-features?id=recurring-transaction-with-subscriptions-api) 313 | 314 | You will receive `saved_token_id` as part of the response when the initial card payment is accepted (will also available in the HTTP notification's JSON), [refer to this docs.](https://docs.midtrans.com/en/core-api/advanced-features?id=sample-3ds-authenticate-json-response-for-the-first-transaction) 315 | 316 | ```python 317 | # Create Subscription API instance 318 | core_api = midtransclient.CoreApi( 319 | is_production=False, 320 | server_key='YOUR_SERVER_KEY', 321 | client_key='YOUR_CLIENT_KEY' 322 | ) 323 | # Prepare parameter 324 | param = { 325 | "name": "SUBSCRIPTION-STARTER-1", 326 | "amount": "100000", 327 | "currency": "IDR", 328 | "payment_type": "credit_card", 329 | "token": "436502qFfqfAQKScMtPRPdZDOaeg7199", 330 | "schedule": { 331 | "interval": 1, 332 | "interval_unit": "month", 333 | "max_interval": 3, 334 | "start_time": "2021-10-01 07:25:01 +0700" 335 | }, 336 | "metadata": { 337 | "description": "Recurring payment for STARTER 1" 338 | }, 339 | "customer_details": { 340 | "first_name": "John A", 341 | "last_name": "Doe A", 342 | "email": "johndoe@email.com", 343 | "phone": "+62812345678" 344 | } 345 | } 346 | create_subscription_response = core_api.create_subscription(param) 347 | 348 | subscription_id_response = create_subscription_response['id'] 349 | # get subscription by subscription_id 350 | get_subscription_response = core_api.get_subscription(subscription_id_response) 351 | 352 | # disable subscription by subscription_id 353 | disable_subscription_response = core_api.disable_subscription(subscription_id_response) 354 | 355 | # enable subscription by subscription_id 356 | enable_subscription_response = core_api.enable_subscription(subscription_id_response) 357 | 358 | # update subscription by subscription_id 359 | update_param = { 360 | "name": "SUBSCRIPTION-STARTER-1-UPDATE", 361 | "amount": "100000", 362 | "currency": "IDR", 363 | "token": "436502qFfqfAQKScMtPRPdZDOaeg7199", 364 | "schedule": { 365 | "interval": 1 366 | } 367 | update_subscription_response = core_api.update_subscription(subscription_id_response, update_param) 368 | ``` 369 | 370 | #### Subscription API for Gopay 371 | 372 | To use subscription API for gopay, you should first link your customer gopay account with gopay tokenization API, [refer to this section](#22e-tokenization-api) 373 | 374 | You will receive gopay payment token using `get_payment_account` API call 375 | 376 | You can see some Subscription API examples [here](examples/subscription) 377 | 378 | ### 2.2.E Tokenization API 379 | You can see some Tokenization API examples [here](examples/tokenization), [Tokenization API Docs](https://api-docs.midtrans.com/#gopay-tokenization) 380 | 381 | ```python 382 | # Create Tokenization API instance 383 | core_api = midtransclient.CoreApi( 384 | is_production=False, 385 | server_key='YOUR_SERVER_KEY', 386 | client_key='YOUR_CLIENT_KEY' 387 | ) 388 | # Prepare parameter 389 | param = { 390 | "payment_type": "gopay", 391 | "gopay_partner": { 392 | "phone_number": "81234567891", 393 | "country_code": "62", 394 | "redirect_url": "https://mywebstore.com/gopay-linking-finish" #please update with your redirect URL 395 | } 396 | } 397 | 398 | # link payment account 399 | link_payment_account_response = core_api.link_payment_account(param) 400 | 401 | # get payment account 402 | get_payment_account_response = core_api.get_payment_account(active_account_id) 403 | 404 | # unlink account 405 | unlink_payment_account_response = core_api.unlink_payment_account(active_account_id) 406 | ``` 407 | 408 | ### 2.3 Handle HTTP Notification 409 | 410 | > **IMPORTANT NOTE**: To update transaction status on your backend/database, **DO NOT** solely rely on frontend callbacks! For security reason to make sure the status is authentically coming from Midtrans, only update transaction status based on HTTP Notification or API Get Status. 411 | 412 | Create separated web endpoint (notification url) to receive HTTP POST notification callback/webhook. 413 | HTTP notification will be sent whenever transaction status is changed. 414 | Example also available [here](examples/transaction_actions/notification_example.py) 415 | 416 | ```python 417 | # Create Core API / Snap instance (both have shared `transactions` methods) 418 | api_client = midtransclient.CoreApi( 419 | is_production=False, 420 | server_key='YOUR_SERVER_KEY', 421 | client_key='YOUR_CLIENT_KEY' 422 | ) 423 | status_response = api_client.transactions.notification(mock_notification) 424 | 425 | order_id = status_response['order_id'] 426 | transaction_status = status_response['transaction_status'] 427 | fraud_status = status_response['fraud_status'] 428 | 429 | print('Transaction notification received. Order ID: {0}. Transaction status: {1}. Fraud status: {3}'.format(order_id, 430 | transaction_status, 431 | fraud_status)) 432 | 433 | # Sample transaction_status handling logic 434 | 435 | if transaction_status == 'capture': 436 | if fraud_status == 'challenge': 437 | # TODO set transaction status on your databaase to 'challenge' 438 | else if fraud_status == 'accept': 439 | # TODO set transaction status on your databaase to 'success' 440 | else if transaction_status == 'cancel' or 441 | transaction_status == 'deny' or 442 | transaction_status == 'expire': 443 | # TODO set transaction status on your databaase to 'failure' 444 | else if transaction_status == 'pending': 445 | # TODO set transaction status on your databaase to 'pending' / waiting payment 446 | ``` 447 | 448 | ### 2.4 Transaction Action 449 | Also available as examples [here](examples/transaction_actions) 450 | #### Get Status 451 | ```python 452 | # get status of transaction that already recorded on midtrans (already `charge`-ed) 453 | status_response = api_client.transactions.status('YOUR_ORDER_ID OR TRANSACTION_ID') 454 | ``` 455 | #### Get Status B2B 456 | ```python 457 | # get transaction status of VA b2b transaction 458 | statusb2b_response = api_client.transactions.statusb2b('YOUR_ORDER_ID OR TRANSACTION_ID') 459 | ``` 460 | #### Approve Transaction 461 | ```python 462 | # approve a credit card transaction with `challenge` fraud status 463 | approve_response = api_client.transactions.approve('YOUR_ORDER_ID OR TRANSACTION_ID') 464 | ``` 465 | #### Deny Transaction 466 | ```python 467 | # deny a credit card transaction with `challenge` fraud status 468 | deny_response = api_client.transactions.deny('YOUR_ORDER_ID OR TRANSACTION_ID') 469 | ``` 470 | #### Cancel Transaction 471 | ```python 472 | # cancel a credit card transaction or pending transaction 473 | cancel_response = api_client.transactions.cancel('YOUR_ORDER_ID OR TRANSACTION_ID') 474 | ``` 475 | #### Expire Transaction 476 | ```python 477 | # expire a pending transaction 478 | expire_response = api_client.transactions.expire('YOUR_ORDER_ID OR TRANSACTION_ID') 479 | ``` 480 | #### Refund Transaction 481 | ```python 482 | # refund a transaction (not all payment channel allow refund via API) 483 | param = { 484 | "refund_key": "order1-ref1", 485 | "amount": 5000, 486 | "reason": "Item out of stock" 487 | } 488 | refund_response = api_client.transactions.refund('YOUR_ORDER_ID OR TRANSACTION_ID',param) 489 | ``` 490 | 491 | #### Refund Transaction with Direct Refund 492 | ```python 493 | # refund a transaction (not all payment channel allow refund via API) with Direct Refund 494 | param = { 495 | "refund_key": "order1-ref1", 496 | "amount": 5000, 497 | "reason": "Item out of stock" 498 | } 499 | refund_response = api_client.transactions.refundDirect('YOUR_ORDER_ID OR TRANSACTION_ID',param) 500 | ``` 501 | 502 | ## 3. Handling Error / Exception 503 | When using function that result in Midtrans API call e.g: `core.charge(...)` or `snap.create_transaction(...)` 504 | there's a chance it may throw error (`MidtransAPIError` object), the error object will contains below properties that can be used as information to your error handling logic: 505 | ```python 506 | err = None 507 | try: 508 | transaction = snap.create_transaction(param) 509 | except Exception as e: 510 | err = e 511 | err.message 512 | err.api_response_dict 513 | err.http_status_code 514 | err.raw_http_client_data 515 | ``` 516 | ## 4. Advanced Usage 517 | 518 | ### Custom Http Headers 519 | 520 | You can set custom headers via the value of this `.api_config.custom_headers` dict, e.g: 521 | ```python 522 | # Create Snap API instance 523 | snap = midtransclient.Snap( 524 | is_production=False, 525 | server_key='YOUR_SERVER_KEY', 526 | client_key='YOUR_CLIENT_KEY' 527 | ) 528 | 529 | # set custom HTTP header for every request from this instance 530 | snap.api_config.custom_headers = { 531 | 'my-custom-header':'my value', 532 | 'x-override-notification':'https://example.org', 533 | } 534 | ``` 535 | 536 | ### Override/Append Http Notification Url 537 | As [described in API docs](https://snap-docs.midtrans.com/#override-notification-url), merchant can opt to change or add custom notification urls on every transaction. It can be achieved by adding additional HTTP headers into charge request. 538 | 539 | This can be achived by: 540 | ```python 541 | # create instance of api client 542 | snap = midtransclient.Snap( 543 | is_production=False, 544 | server_key='YOUR_SERVER_KEY', 545 | client_key='YOUR_CLIENT_KEY' 546 | ) 547 | # set custom HTTP header that will be used by Midtrans API to override notification url: 548 | snap.api_config.custom_headers = { 549 | 'x-override-notification':'https://example.org', 550 | } 551 | ``` 552 | 553 | or append notification: 554 | ```python 555 | snap.api_config.custom_headers = { 556 | 'x-append-notification':'https://example.org', 557 | } 558 | ``` 559 | 560 | ### Custom Http Proxy 561 | 562 | You can set custom http(s) proxies via the value of this `.api_config.proxies` dict, e.g: 563 | 564 | ```python 565 | # create instance of api client 566 | snap = midtransclient.Snap( 567 | is_production=False, 568 | server_key='YOUR_SERVER_KEY', 569 | client_key='YOUR_CLIENT_KEY' 570 | ) 571 | 572 | snap.api_config.proxies = { 573 | 'http': 'http://10.10.1.10:3128', 574 | 'https': 'http://10.10.1.10:1080', 575 | } 576 | ``` 577 | 578 | Under the hood this API wrapper is using [Requests](https://github.com/requests/requests) as http client. You can further [learn about proxies on its documentation](https://requests.readthedocs.io/en/master/user/advanced/#proxies) 579 | 580 | ## Examples 581 | Examples are available on [/examples](/examples) folder. 582 | There are: 583 | - [Core Api examples](/examples/core_api) 584 | - [Subscription examples](/examples/subscription) 585 | - [Tokenization examples](/examples/tokenization) 586 | - [Snap examples](/examples/snap) 587 | - [Flask App examples](/examples/flask_app) that implement Snap & Core Api 588 | 589 | ## Important Changes 590 | ### v1.3.0 591 | - **Drop support for Python 2** (because Python 2 has reached its end of life), in favor of better compatibility with Python 3 and to prevent package unable to be properly installed on Windows OS env. 592 | 593 | #### Get help 594 | 595 | * [Midtrans Docs](https://docs.midtrans.com) 596 | * [Midtrans Dashboard ](https://dashboard.midtrans.com/) 597 | * [SNAP documentation](http://snap-docs.midtrans.com) 598 | * [Core API documentation](http://api-docs.midtrans.com) 599 | * Can't find answer you looking for? email to [support@midtrans.com](mailto:support@midtrans.com) 600 | --------------------------------------------------------------------------------