> For the complete documentation index, see [llms.txt](https://glyue.docs.sandboxbanking.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://glyue.docs.sandboxbanking.com/reference/adapters/custom-adapters.md).

# Custom Adapters

The Custom Adapter feature lets you author an adapter in Python and store it outside any single integration, together with a typed configuration schema. You can reuse one custom adapter across integrations, and each adapter can hold several named configurations that each supply their own values.

Custom Adapters share the Shared Code page with [Shared Modules](/reference/shared-modules.md) and appear under the **Custom Adapters** tab.

## Permissions

Users must have the `can_use_custom_adapters` permission to access the Custom Adapters tab. This permission is independent of `can_use_shared_modules`; a user might have either, both, or neither.

## Anatomy

Users with the permission see the **Custom Adapters** tab on the Shared Code page.

A custom adapter comprises two kinds of object:

* A **Custom Adapter Code** — the Python source and its configuration schema. This is the reusable definition.
* One or more **Configs** — concrete sets of values that satisfy a Custom Adapter Code's schema. Each config belongs to exactly one Custom Adapter Code.

The tab contains these elements:

* **Adapter Selection Pane**: Select an existing custom adapter, or start a new one.
* **Adapter Name**: Name the adapter. The read-only **System ID** hint beneath this field shows the identifier that the platform derives from the name.
* **Description**: Enter an optional free-text description.
* **Adapter Code**: Enter the Python source for the adapter.
* **Config Schema**: Define the configuration fields and their types.
* **Save** and **Delete**: Save or remove the Custom Adapter Code.
* **Configs** table: Lists the configs defined for the adapter, their active state, and their validation status. This table appears after you save the adapter.

### Custom Adapter Code

| Field         | Description                                                                                                      |
| ------------- | ---------------------------------------------------------------------------------------------------------------- |
| Name          | **REQUIRED** \| Unique display name for the adapter.                                                             |
| System ID     | **DERIVED, READ-ONLY** \| Identifier used to reference the adapter. See [System Identifier](#system-identifier). |
| Description   | **OPTIONAL** \| Free-text description.                                                                           |
| Adapter Code  | **REQUIRED** \| Python source for the adapter. See [Adapter Code](#adapter-code).                                |
| Config Schema | **REQUIRED** \| Field names and their types. See [Config Schema](#config-schema).                                |

### System Identifier

The platform derives the System ID automatically from the name; you cannot edit it directly. To derive it, the platform replaces each run of non-alphanumeric characters with a single underscore, removes leading and trailing underscores, and uppercases the result.

| Name               | System ID        |
| ------------------ | ---------------- |
| `My REST API`      | `MY_REST_API`    |
| `acct-lookup (v2)` | `ACCT_LOOKUP_V2` |

The System ID must be unique across all custom adapters. If you save a name that derives to an identifier already in use, the platform rejects it with a validation error.

{% hint style="warning" %}
The platform recalculates the System ID from the name every time you save the adapter. Renaming an adapter changes its System ID, which can break existing references to the adapter.
{% endhint %}

### Config Schema

The config schema is a JSON object mapping each field name to a type. It defines which values a config for this adapter must supply.

| Type               | Config value     | Notes                                                                                                                      |
| ------------------ | ---------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `string`           | text             |                                                                                                                            |
| `integer`          | whole number     |                                                                                                                            |
| `float`            | number           |                                                                                                                            |
| `boolean`          | `true` / `false` |                                                                                                                            |
| `array`            | JSON array       | Enter as JSON in the config dialog.                                                                                        |
| `object`           | JSON object      | Enter as JSON in the config dialog.                                                                                        |
| `encrypted_string` | text             | The platform encrypts this value at rest. See [Encrypted Fields](#encrypted-fields).                                       |
| `file`             | file upload      | The platform encrypts this value at rest as a base64 string. Maximum size 1 MB. See [Encrypted Fields](#encrypted-fields). |

Field names must be strings. The platform rejects any type outside this list when you save the adapter.

{% hint style="warning" %}
Changing the schema of an adapter that already has configs can invalidate those configs. Adding a field leaves existing configs without a value for it; changing a field's type might cause existing values to no longer match. The interface shows a warning describing the impact before you save such a change. Removing a field drops that field's value from existing configs.
{% endhint %}

## Configs

A config is a named set of values for one Custom Adapter Code. You enter a config through the config dialog. Each config has:

| Field         | Description                                                |
| ------------- | ---------------------------------------------------------- |
| Name          | **REQUIRED** \| Name of the config.                        |
| Active        | Whether the config is available for use.                   |
| Config Values | One input per schema field, typed according to the schema. |

A config's values must match the adapter's schema exactly: every schema field must be present, no config can supply an unlisted field, and each value must match its field's type. You cannot save a config that does not satisfy these rules.

### Encrypted Fields

The platform stores `encrypted_string` and `file` fields encrypted at rest, separately from the other config values.

In the config dialog and in API responses, the platform masks a stored encrypted value with the sentinel `__ENCRYPTED__` instead of showing it. When you edit an existing config:

* Leave the masked value in place to preserve the stored encrypted value.
* Enter a new value to replace it.

The config dialog shows `encrypted_string` fields as password inputs. A `file` field shows an "uploaded" state with a **Replace** action. The platform reads each uploaded file as base64, and the file must not exceed 1 MB.

## Adapter Code

The platform compiles and executes adapter code server-side. The Python editor performs syntax checking as you write the code.

### Available Names

The platform injects these names into the adapter code's namespace; you do not need to import them:

| Name                    | Description                                                      |
| ----------------------- | ---------------------------------------------------------------- |
| `BaseAdapter`           | Base class the adapter class extends.                            |
| `AdapterRequest`        | The request object passed to `execute()`.                        |
| `AdapterResponse`       | The response object `execute()` returns.                         |
| `AdapterFlowController` | Flow-control helper for sequencing steps.                        |
| `MessageTypes`          | Message-type constants used when reporting errors on a response. |
| `base64`                | Python's `base64` module, for decoding `file` fields.            |

The standard `import` statement is available for other modules (for example `import requests`).

### Required Structure

Adapter code must define a class named exactly `CustomAdapter` that extends `BaseAdapter`:

```python
class CustomAdapter(BaseAdapter):
    def execute(self, request):
        ...
```

{% hint style="info" %}
If your code does not define a class named `CustomAdapter`, execution fails with the error "Custom adapter code must define a class named 'CustomAdapter'."
{% endhint %}

* `execute(self, request)` is **required**. It receives an `AdapterRequest` and must return an `AdapterResponse`.
* `validate_config(self)` is **optional**. The platform calls it during config validation; see [Config Validation](#config-validation).

### The Request Object

| Attribute              | Description                                               |
| ---------------------- | --------------------------------------------------------- |
| `request.payload`      | The field mappings the integration passes to the adapter. |
| `request.service_name` | The name of the service request invoking the adapter.     |

### Access the Configuration

Inside the adapter, `self.config` is a plain dictionary of the resolved config values, with any encrypted fields already decrypted. Access values by key:

```python
host = self.config["base_url"]
timeout = self.config.get("timeout", 30)
```

`file` fields arrive as base64-encoded strings. Decode them with the injected `base64` module:

```python
content_bytes = base64.b64decode(self.config["my_file"])
content_text = content_bytes.decode("utf-8")  # for text files
```

### Return a Response

Build and return an `AdapterResponse`. Set `success` and `payload`, and report failures with `add_message_and_stack_trace(message_type, message)`:

```python
response = AdapterResponse()
response.payload = r.json()
response.success = True
return response
```

If the adapter does not handle an exception raised in `execute()`, the platform catches it and returns a failed `AdapterResponse` with the message type `MessageTypes.UNCATEGORIZED_ERROR`.

### Example: Default Template

New adapters open with the following template, which authenticates with a bearer token, posts the request payload, and maps common request failures onto message types:

```python
import requests


class CustomAdapter(BaseAdapter):
    """
    Custom REST adapter template.

    Config fields available via self.config (defined in Config Schema):
        - base_url: Base URL for the API
        - api_key: API key for authentication (encrypted)
        - timeout: Request timeout in seconds

    File fields are stored as base64 strings.
        content_bytes = base64.b64decode(self.config["my_file"])
        content_text = content_bytes.decode("utf-8")  # for text files

    Available imports (no need to import these):
        BaseAdapter, AdapterRequest, AdapterResponse,
        AdapterFlowController, MessageTypes, base64
    """

    def execute(self, request):
        response = AdapterResponse()
        response.success = False

        try:
            url = f"{self.config['base_url']}/{request.service_name}"
            headers = {
                "Authorization": f"Bearer {self.config['api_key']}",
                "Content-Type": "application/json",
            }

            r = requests.request(
                method="POST",
                url=url,
                headers=headers,
                json=request.payload,
                timeout=self.config.get("timeout", 30),
            )
            r.raise_for_status()

            response.payload = r.json()
            response.success = True

        except requests.exceptions.HTTPError as e:
            response.add_message_and_stack_trace(
                MessageTypes.from_status(e.response.status_code),
                f"{e.response.status_code}: {e.response.text}"
            )
        except requests.exceptions.ConnectionError as e:
            response.add_message_and_stack_trace(
                MessageTypes.CONNECTION_ERROR,
                str(e)
            )
        except requests.exceptions.Timeout as e:
            response.add_message_and_stack_trace(
                MessageTypes.CONNECTION_ERROR,
                f"Request timed out: {e}"
            )
        except Exception as e:
            response.add_message_and_stack_trace(
                MessageTypes.UNCATEGORIZED_ERROR,
                str(e)
            )

        return response

    def validate_config(self):
        if not self.config.get("base_url"):
            raise Exception("base_url is required")
        if not self.config.get("api_key"):
            raise Exception("api_key is required")
```

The template ships with a matching schema of `base_url` (string), `api_key` (encrypted\_string), and `timeout` (integer).

## Config Validation

The **Validate** action on a single config, and **Validate All** on the adapter, instantiate the adapter with that config and call its `validate_config()` method:

* A `validate_config()` that returns without raising marks the config as passed.
* An exception from `validate_config()` marks the config as failed. View the raised message through the config's **View Log** action.
* A config whose code fails to compile fails validation with "Failed to compile custom adapter code: …".

Validation runs asynchronously; each config shows one of the following states:

| State          | Meaning                                                         |
| -------------- | --------------------------------------------------------------- |
| Spinner        | Validation in progress.                                         |
| Green check    | Passed.                                                         |
| Red error icon | Failed. Open **View Log** for the message.                      |
| Amber warning  | Result unknown. The validation process did not report a result. |
| —              | Not yet validated.                                              |

## Custom Adapter Usage

A saved Custom Adapter Code's System ID identifies it. A config for that adapter must be active before the integration can use it. You invoke adapters from within an integration with the [`calladapter`](/reference/special_functions/calladapter.md) special function.

After the adapter is bound and active, it receives the integration's field mappings as `request.payload` and returns its `AdapterResponse` to the integration.

## Limitations and Notes

* Custom adapters provide no built-in authentication. You must implement any authentication a request requires in the adapter code, using values from the config schema. For example, store a token or password in an `encrypted_string` field. See [How to authenticate custom adapter requests](/reference/adapters/custom-adapters/authenticate-custom-adapter-requests.md).
* Adapter code must define a class named exactly `CustomAdapter`.
* The platform derives the System ID from the name and changes it when you rename the adapter.
* Deleting a Custom Adapter Code also deletes all of its configs. Before you delete, the interface reports the number of configs it removes.
* Changing an adapter's config schema can invalidate existing configs. The interface warns you before you save such a change.
* `file` fields have a 1 MB limit.
* The platform stores `encrypted_string` and `file` values encrypted at rest and masks them in the interface and API.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://glyue.docs.sandboxbanking.com/reference/adapters/custom-adapters.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
