# Infor M3 ION API Gateway OAuth 2.0 Python Client Generator

> Source: https://netray.co/tools/infor-m3-ion-api-oauth2-python-client-generator
> Publisher: Netray (netray.co), AI and integration engineering for ERP and manufacturing systems.
> Section: Free Tools

## Short answer

Generate a Python script that loads your .ionapi credentials file, gets and reuses an OAuth 2.0 token, and calls an M3 MI transaction through the on-prem ION API Gateway.

## Category

developer

## Erp

Infor M3

## Audience

- M3 developer
- Integration developer
- ION administrator

## Tool

**Kind:** template

**Output:** code

**Language:** python

**Fields:**

#### .ionapi file path

**Key:** ionapi_path

**Type:** text

**Default:** C:/ionapi/M3_Integration.ionapi

#### MI program

**Key:** program

**Type:** text

**Default:** CRS610MI

#### Transaction

**Key:** transaction

**Type:** text

**Default:** GetBasicData

#### Input parameters (one KEY=VALUE per line)

**Key:** inputs

**Type:** textarea

**Default:** CUNO=10001

#### Company (CONO)

**Key:** cono

**Type:** text

**Default:** 100

#### Division (DIVI, blank for none)

**Key:** divi

**Type:** text

**Default:** AAA

#### Max records (Lst transactions)

**Key:** maxrecs

**Type:** number

**Default:** 100

#### Return columns (comma separated, blank for all)

**Key:** returncols

**Type:** text

**Default:** CUNO,CUNM,STAT

#### CA bundle for your internal TLS (blank uses system store)

**Key:** ca_bundle

**Type:** text

**Blocks:**

#### m3_ion_client.py

**Body:** import json
import time
import requests

IONAPI_PATH = '{{ionapi_path}}'
PROGRAM = '{{program}}'
TRANSACTION = '{{transaction}}'
CONO = '{{cono}}'
DIVI = '{{divi}}'
MAXRECS = '{{maxrecs}}'
RETURNCOLS = '{{returncols}}'
CA_BUNDLE = '{{ca_bundle}}' or True
INPUTS_TEXT = """{{inputs}}"""

class IonClient:
    def __init__(self, path):
        with open(path, encoding='utf-8') as f:
            self.cfg = json.load(f)
        self.token = None
        self.refresh = None
        self.expires = 0

    def _token_request(self, data):
        url = self.cfg['pu'] + self.cfg['ot']
        data['client_id'] = self.cfg['ci']
        data['client_secret'] = self.cfg['cs']
        r = requests.post(url, data=data, timeout=60, verify=CA_BUNDLE)
        r.raise_for_status()
        tok = r.json()
        self.token = tok['access_token']
        self.refresh = tok.get('refresh_token')
        self.expires = time.time() + int(tok.get('expires_in', 3600)) - 60

    def get_token(self):
        if self.token and time.time() < self.expires:
            return self.token
        if self.refresh:
            try:
                self._token_request({'grant_type': 'refresh_token', 'refresh_token': self.refresh})
                return self.token
            except requests.HTTPError:
                self.refresh = None
        self._token_request({
            'grant_type': 'password',
            'username': self.cfg['saak'],
            'password': self.cfg['sask'],
        })
        return self.token

    def execute(self, program, transaction, params):
        url = self.cfg['iu'].rstrip('/') + '/' + self.cfg['ti'] + '/M3/m3api-rest/v2/execute/' + program + '/' + transaction
        url += ';cono=' + CONO
        if DIVI:
            url += ';divi=' + DIVI
        query = dict(params)
        query['maxrecs'] = MAXRECS
        if RETURNCOLS:
            query['returncols'] = RETURNCOLS
        headers = {'Authorization': 'Bearer ' + self.get_token(), 'Accept': 'application/json'}
        r = requests.get(url, params=query, headers=headers, timeout=120, verify=CA_BUNDLE)
        r.raise_for_status()
        return r.json()

def parse_inputs(text):
    params = {}
    for line in text.splitlines():
        if '=' in line:
            k, v = line.split('=', 1)
            params[k.strip()] = v.strip()
    return params

if __name__ == '__main__':
    client = IonClient(IONAPI_PATH)
    data = client.execute(PROGRAM, TRANSACTION, parse_inputs(INPUTS_TEXT))
    if data.get('nrOfFailedTransactions'):
        for res in data.get('results', []):
            print('ERROR', res.get('errorMessage'), res.get('errorField'), res.get('errorCode'))
    else:
        for res in data.get('results', []):
            for rec in res.get('records', []):
                print(rec)

#### .ionapi keys the script reads

**Table:**

**Columns:**

- Key
- Used for

**Rows:**

**0:** ti

**1:** Tenant segment in the API URL

**0:** ci / cs

**1:** OAuth 2.0 client id and secret

**0:** pu + ot

**1:** Token endpoint (SSO base URL plus token path)

**0:** iu

**1:** ION API Gateway base URL

**0:** saak / sask

**1:** Service account access key and secret key, sent as username and password

**Notes:** Verify against the M3 API documentation in your ION API Gateway (the Swagger for the M3 suite): the v2 execute path, whether cono and divi are accepted as matrix parameters and maxrecs and returncols as query parameters on your version, and the exact error keys in the v2 response. The script prints the whole error entry so you see what your server returns. Keep the .ionapi file out of source control and readable only by the service account.

## Content

**Intro:** This generator writes a Python client for the on-prem ION API Gateway that reads a downloaded .ionapi file, gets an OAuth 2.0 token with service account keys, reuses it until shortly before expiry and calls one M3 MI transaction through the v2 execute endpoint. Use it for scheduled jobs and integrations that should not depend on a person's password.

**Sections:**

#### How to use it

**Body:** In ION API, create a backend service client authorized app, add a service account mapped to an M3 user, and download the credentials as a .ionapi file. Enter its path, the MI program and transaction (CRS610MI GetBasicData for a customer), the input fields one per line, the company and division, and for Lst transactions the maximum records and the columns to return. Install requests, run the script and check the printed records. Point CA_BUNDLE at your internal certificate chain if the gateway uses a private CA.

**Bullets:**

- One authorized app per integration, so you can revoke it alone
- Map the service account to an M3 user with only the MI security it needs
- Use returncols to keep responses small

#### Gateway versus calling the M3 BE grid directly

**Body:** The classic endpoint on the M3 server, /m3api-rest/execute/PROGRAM/TRANSACTION, uses grid authentication, usually basic auth with an M3 user, and returns MIRecord lists with name and value pairs. Through ION API you call {iu}/{ti}/M3/m3api-rest/v2/execute with a bearer token instead; the gateway handles the grid authentication, logs and throttles calls, and the v2 response returns results with plain record objects. Service accounts exist so a job authenticates with revocable keys tied to one app rather than a person's password that expires or leaves with the person.

#### Worked example and reading the response

**Body:** With CUNO=10001, company 100 and division AAA, the script calls CRS610MI GetBasicData and prints a record such as CUNO, CUNM and STAT. In the v2 response the counters nrOfSuccessfullTransactions and nrOfFailedTransactions sit at the top level, and each entry in results holds the transaction name and its records. When a call fails the entry carries an error message, and often the field that caused it; the script prints them as returned. A wrong customer number, for example, comes back as a business error rather than an HTTP failure.

#### Token handling and common mistakes

**Body:** The token request posts grant_type password with saak and sask as username and password plus the client id and secret, to pu joined with ot. The client stores expires_in and renews one minute early, preferring the refresh token when the server issued one. Common errors are building the token URL from iu instead of pu, a service account not mapped to an M3 user, a clock skew that makes tokens look expired, and requesting a new token for every row in a loop, which slows the job and loads the authorization server.

**Cta:** If you want an assistant that answers M3 questions through these same ION API calls, on your own servers and with read-only defaults, we can set that up.

**Faq:**

#### What is in an .ionapi file?

**Answer:** It is a JSON credentials file downloaded from ION API for an authorized app. The keys used here are ti (tenant), ci and cs (client id and secret), pu and ot (the token endpoint), iu (the gateway URL) and, for backend service apps, saak and sask (service account keys). Treat it like a password.

#### Where is the M3 REST API documentation or Swagger?

**Answer:** In ION API the M3 suite publishes API documentation with Swagger for the m3api-rest endpoints, and on the M3 server the API browser lists MI programs, transactions and fields. Use them to confirm parameter names and mandatory input fields for your version.

#### Why use a service account instead of a user login?

**Answer:** A service account ties the integration to one authorized app with keys you can rotate or revoke without touching a person's account. Its calls are attributed to the mapped M3 user, so you can give it exactly the MI security it needs and audit it separately.

#### How do I limit the rows and columns returned by an M3 Lst transaction?

**Answer:** Pass maxrecs for the row limit and returncols with a comma-separated list of output fields. Small, explicit column lists reduce response size and processing time. For more than one page of data, page by key or use a dedicated extract approach rather than a very large maxrecs.

## Search terms

infor m3 api gateway, infor m3 rest api documentation, infor m3 api swagger, ionapi file python, ion api oauth2 python, m3api-rest v2 execute, ion api service account m3, call m3 api python

---
Talk to an engineer: https://netray.co/contact?intent=erp_ai | Machine catalog: https://netray.co/agent/catalog.json
