The adapter is the central point of the configuration of an external API call. As mentioned earlier, the Adapter defines how the request for the API call to the counterparty will be constructed.

There is a convention that adapters are named as follows:
ADAPTER.{entity}-{target}
Thus, to send the COMPANY entity to Acountancy system, the adapter name will look like this: ADAPTER.COMPANY-ACCOUNTANCY

Storing the secrets

It is important that you do not store passwords, api keys and tokens in plain text in the adapter, but that you use a secure use of secret for this case. The instructions are described in detail in the chapter Saving the secrets.

For example, the Adapter configuration would look like this:

{
    "constants": {
        "name": "Our accounting",
        "url": "https://xxx.yyyy.com",
        "apiKey": "{{secret:secret_name}}",
        "apiType": "rest",
        "method": "POST"
    },
    "requestMapping": {
        "method": "${currentObject:constants.method}",
        "url": "${currentObject:constants.url}/company",
        "headers": {
            "Content-Type": "application/json",
            "X-Api-Key": "${currentObject:constants.apiKey}"
        },
        "body": {
                    "id": "${currentObject:input.id}",
                    "name": "${currentObject:input.name}",
                    "address": {
                        "zip": "${currentObject:input.address.zip}",
                        "city": "${currentObject:input.address.city}",
                        "state": "${currentObject:input.address.state}",
                        "street": "${currentObject:input.address.street}",
                        "country": "${currentObject:input.address.country}"
                    }
                }
    },
   "preResponseMapping": {
   			// some pre mapping actions can be performed here
				// result is available as ${currentObject:preResponse...}
   },
   "responseMapping": {
        "properties": {
            "extName": {
                "value": "Accounting",
                "type": "string"
            },
            "extId": {
                "value": "${currentObject:response.id}",
                "type": "string"
            },
            "extStatus": {
                "_comment": "expected final value: 'EXT.READY' or 'EXT.OK' or 'EXT.ERROR'",
                "value": "${evaluate:String(${currentObject:meta.error}) === 'true' ? 'EXT.ERROR' : 'EXT.OK'}",
                "type": "string"
            },
            "extMessage": {
                "value": "${currentObject:meta.message}",
                "type": "string"
            },
            "extTimestamp": {
                "value": "${timestampToDateTime:${computed:timestamp}}",
                "type": "timestamp"
            }
        }
    }
}

Now we will go through the Adapter configuration parameters in detail:

IP addresses and ports
the following methods can be used for outward communication

"url": "http://[ip.address]:[port]/[subfolder]/" but beware, at your own risk, http communication is strongly discouraged!!!! For testing purposes only, where no raw data is sent.
"url": "https://[domain]/"
"url": "https://[domain]:[port]/"
"url": "https://[domain]/[subfolder]/"
"url": "https://[domain]:[port]/[subfolder]/"

If no port is specified, the standard port 443 will be used.
The ports that can be used for communication out of Teamogy are 44300-44399

QUERY PARAMETERS in URL

if the URL needs to contain query parameters that contain characters that will need to be encoded in the final url, then they need to be written to the adapter in UNencoded form.

Examples:

This will NOT work (entered in URL ENCODED form)
"url": "https://[domain]/[subfolder]?sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0

This is how it WILL work (entered in ENCODED form)
"url": "https://[domain]/[subfolder]?sp=/triggers/manual/run&sv=1.0

Help - you can check encode and decode here: https://www.urldecoder.org/

Security

Teamogy supports successive forms of Authentication:

  • Authentication via API key
  • Simple authentication
  • ID Token authentication
  • Oauth authentication

For security reasons, we also recommend that you also limit the IP addresses from which communication is allowed on the counterparty where inward communication is allowed. The IP addresses will be provided by Teamogy support upon request.

Oauth Authentication
If the counterparty requires Oauth authentication, an intermediary microservice must be used to obtain the necessary authentication token with the first request and send the data to the target api with the authentication token with the second request.
The configuration of the adapter then looks like this: (in the example it is required that the obtained SessionId is sent as a cookie on the final request).
Headers contains instructions for login requests: x-url-login, x-method-login, x-content-type-login,
and for data: x-url-data, x-method-data, x-content-type-data

The body is then composed of 3 objects - loginBody, dataHeader, dataBody

"requestMapping": {
  	"method": "POST",
    "url": "https://xxx.yyyy.com/oauthproxy",
    "headers": {
        "x-url-login": "https://xxx.yyyy.com/login",
        "x-method-login": "POST",
        "x-content-type-login": "application/json",
        "x-url-data": "https://xxx.yyyy.com/data",
        "x-method-data": "POST"
        "x-content-type-data": "application/json",
      },
        "body": "{
            \"loginBody\": {\"UserName\": \"best_user\", \"Password\": \"123456789\", \"CompanyDB\": \"ABCDEFGH_EUR\"},
            \"dataHeader\": [{\"name\":\"Cookie\", \"value\":\"{{SessionId}}; ROUTEID=.node1\"}], 
            \"dataBody\": ${configRegister:INTEGRATION.TEMPLATE-COMPANY.JSON.10}
        }"
},

Request Body Mapping

Here the request body is mapped/built. To build it, you can use the input object, as shown in the example, or use a reference to another Configuration Register

Example of mapping a simple Request Body directly in the Adapter:

...
"body": {
    "id": "${currentObject:input.id}",
    "name": "${currentObject:input.name}",
    "address": {
        "zip": "${currentObject:input.address.zip}",
        "city": "${currentObject:input.address.city}",
        "state": "${currentObject:input.address.state}",
        "street": "${currentObject:input.address.street}",
        "country": "${currentObject:input.address.country}"
    }
}
...

More complicated Request Body are ideally mapped in the extra Configuration Register:

...
"body": "${configRegister:INTEGRATION.TEMPLATE-COMPANY.JSON.10}"
...

The mapping method using the extra Configuration Register is described in the following chapter.

Response Mapping

That is, how the response should be built from the API response back to Teamogy

Response for post-processing purposes always has the following structure:

{
  "meta": {
    // ... information from response header of external api
    "error": true,                            // ... true / false
    "message": "Internal Server Error",       // ... empty string if not error
    "status": 500                             // ... conterparty response status
  },
  "response": {
    //... response body from counterpary api always converted to JSON (even if the api responds via XML) ...  
  }
}

Example of mapped response

Any property can be written into the exported entity. However, the convention is that properties written by integration and belonging to an external system must be called "ext" followed by any string starting with an uppercase letter. Such properties are excluded from the save conflict (if the user were to edit the given entity and write the integration module to the same entity in the background)

"responseMapping": {
    "properties": {
        "extName": {
            // name of external system
            "value": "${currentObject:constants.name}",
            "type": "string"
        },
        "extId": {
            // id of the record in integrates external system
            "value": "",
            "type": "string"
        },
        "extStatus": {
            // status if the record in external system
          	// expected final value: 'EXT.READY' or 'EXT.OK' or 'EXT.ERROR'
            "value": "${if:${isTrue:${currentObject:meta.error},EXT.ERROR,EXT.OK}",
            "type": "string"
        },
        "extMessage": {
          	// message - if integration error happens
            "value": "${currentObject:meta.message}",
            "type": "string"
        },
        "extTimestamp": {
          	// timestamp of the synchronization
            "value": "${timestampToDateTime:${computed:timestamp}}",
            "type": "string"
        },
        "editDisabled": {
            // if the intention is that the successfully exported document can no longer be edited
            "value": "${if:${isTrue:${currentObject:meta.error},false,true}",
            "type": "string"
        }
    }
}

Subsequent actions mapping

Calling the next adapter

If appropriate, it is possible to call another adapter after getting data from the API configured in the adapter. It is also possible to call the same adapter, with a modified configuration, which is useful, for example, when the counterparty provides paginated results (the response is not returned in a single query, but a flag comes with the response that another call needs to be made to continue the response).

Here is an example for calling another api (monitoring mockup) where it is configured to pass both the original input and the response and its metadata

Important note: Connectors can have internally two formats - single adapter call (configured as string MAIN_KEY-SUB_KEY) or array of adapters (configures as json array of strings MAIN_KEY-SUB_KEY). In this implementation only the single connector call is supported.

"onResponseConnector": {
    "connector": "MOCKUP.TEST",
    "data": {
        "firstInput": "${currentObject:input}",
        "firstResponse": "${currentObject:response}",
        "firstMeta": "${currentObject:meta}"
    }
}`

Here is an example for recursive calling of the same api - the called api uses pagination of results.

"onResponseConnector": {
  "connector": "BANK.TRANSACTIONS.LIST",
  "data": {
    "variables": {
      "nextPageId": "${currentObject:response.meta.next_id}",
      "repeatedCall": true,
      "buffer":"${currentObject:responseMapping.transactions}"
    }
  }
}

Saving registers

If necessary, data from response of the external service can be saved (patched) in the config registry. If a private registry already exists, it is used; if no private registry exists, a copy of the shared registry is created for the current business unit

"onResponseActions": [
    {
        "action": "patchRegister",
        "parameter": "MAINKEY1-SUBKEY1",
        "data": {
            "value": {}
        }
    },
    {
        "action": "patchRegister",
        "parameter": "MAINKEY2-SUBKEY2",
        "data": {
            "value": ${currentObject:response}
        }
    }
]

Saving the secret

It is possible, if the api returns a secret (e.g. refreshToken) which will be used for further calls, to store this secret in secrets. The secret is stored in the secret manager and is only used to call the api of the domain for which it was created. Neither the administrator nor the system configurator has permission to read the secret value after storing.

"onResponseActions": [{
    "action": "saveSecret",
    "data": {
        "key": "microsoftRefreshToken",
        "value": "${currentObject:response.refresh_token}",
        "url": "https://login.microsoftonline.com"
    }
}]