> Source: https://wexa.ai/docs/connectors/save-user-connector

# save-user-connector

Create a user-defined connector as a private draft, or update one you may edit. Needs the `fabric:skill.write` grant.

**Save a connector's credentials**

**Wexa MCP server**

```bash
curl -sS -X POST https://fabric.wexa.ai/mcp/$FABRIC_PROJECT_ID \
  -H "authorization: Bearer $FABRIC_API_KEY" \
  -H "Mcp-Session-Id: $MCP_SESSION_ID" \
  -H "content-type: application/json" -H "accept: application/json" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"save-user-connector","arguments":{"connector_id":"6b1a3d5e7f9c0b2a4d6e8f02","credentials":{"api_key":"<api-key>"}}}}'
```

```json
{
  "content": [
    {
      "type": "text",
      "text": "{\"lifecycle_id\":\"qlc_000017\",\"result\":{\"connector_id\":\"6b1a3d5e7f9c0b2a4d6e8f02\",\"name\":\"Payroll\",\"category\":\"user_payroll\",\"mode\":\"live\",\"base_url\":\"https://payroll.example.com/v1\",\"lifecycle\":\"draft\",\"access_scope\":\"individual\",\"has_credentials\":true,\"actions\":[{\"key\":\"get_employee\",\"name\":\"Get employee\"}]}}"
    }
  ],
  "isError": false
}
```

**REST API**

```bash
curl -sS https://fabric.wexa.ai/v1/save-user-connector \
  -H "authorization: Bearer $FABRIC_API_KEY" \
  -H "content-type: application/json" \
  -d '{"connector_id":"6b1a3d5e7f9c0b2a4d6e8f02","credentials":{"api_key":"<api-key>"}}'
```

```json
{
  "lifecycle_id": "qlc_000017",
  "result": {
    "connector_id": "6b1a3d5e7f9c0b2a4d6e8f02",
    "name": "Payroll",
    "category": "user_payroll",
    "mode": "live",
    "base_url": "https://payroll.example.com/v1",
    "lifecycle": "draft",
    "access_scope": "individual",
    "has_credentials": true,
    "actions": [
      {
        "key": "get_employee",
        "name": "Get employee"
      }
    ]
  }
}
```

**TypeScript SDK**

```ts
import { Fabric } from '@wexa-fabric/sdk'

const fabric = new Fabric()
const result = await fabric.saveUserConnector({
  connector_id: '6b1a3d5e7f9c0b2a4d6e8f02',
  credentials: {
    api_key: '<api-key>'
  }
})
```

```ts
// result is the payload itself, with no `.result` to read
{
  connector_id: '6b1a3d5e7f9c0b2a4d6e8f02',
  name: 'Payroll',
  category: 'user_payroll',
  mode: 'live',
  base_url: 'https://payroll.example.com/v1',
  lifecycle: 'draft',
  access_scope: 'individual',
  has_credentials: true,
  actions: [
    {
      key: 'get_employee',
      name: 'Get employee'
    }
  ]
}
```

**Python SDK**

```python
from wexa import Fabric

fabric = Fabric()
out = fabric.save_user_connector(
    connector_id="6b1a3d5e7f9c0b2a4d6e8f02",
    credentials={
        "api_key": "<api-key>"
    },
)
```

```python
# out is the result, already unwrapped
{
    "connector_id": "6b1a3d5e7f9c0b2a4d6e8f02",
    "name": "Payroll",
    "category": "user_payroll",
    "mode": "live",
    "base_url": "https://payroll.example.com/v1",
    "lifecycle": "draft",
    "access_scope": "individual",
    "has_credentials": True,
    "actions": [
        {
            "key": "get_employee",
            "name": "Get employee"
        }
    ]
}
```

## Parameters

| Parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `connector_id` | string | Optional | Omit to create a draft; set to update that connector. An unknown id is refused (404). One you may not edit (another person's draft, or a published one when you are neither its author nor a project admin) is refused (502, `user connector not found`). |
| `credentials` | object | Optional | Write-only secrets: `api_key`, `token`, `username`, `password`. Saved to the vault, never returned. A call needs `definition`, `credentials` or both. |
| `definition` | object | Optional | The connector's definition. Required to create one: leave `connector_id` out and pass it. A definition the platform refuses gets its own reason (422). |

**Errors**

- `UpstreamError` (502): the credentials could not be verified in the vault. Save them again.

**Show full response**

An example response, after saving an API key for a draft.

**Result of save-user-connector**

**REST API**

```json
{
  "lifecycle_id": "qlc_000017",
  "result": {
    "connector_id": "6b1a3d5e7f9c0b2a4d6e8f02",
    "name": "Payroll",
    "category": "user_payroll",
    "description": "Read employees from the payroll API.",
    "logo": null,
    "mode": "live",
    "base_url": "https://payroll.example.com/v1",
    "auth": {
      "type": "api_key",
      "name": "X-API-Key",
      "location": "header"
    },
    "headers": {},
    "connected": false,
    "lifecycle": "draft",
    "access_scope": "individual",
    "has_credentials": true,
    "user_id": "6a0f1e2d3c4b5a6978877665",
    "project_id": "6a0f1e2d3c4b5a6978877601",
    "org_id": "6a0f1e2d3c4b5a6978877600",
    "actions": [
      {
        "key": "get_employee",
        "name": "Get employee",
        "description": "One employee by id.",
        "input_schema": {
          "fields": [
            {
              "name": "employee_id",
              "type": "string",
              "required": true
            }
          ]
        },
        "output_schema": {
          "fields": [
            {
              "name": "id",
              "type": "string"
            },
            {
              "name": "name",
              "type": "string"
            }
          ]
        },
        "fault": null,
        "request": {
          "method": "GET",
          "path": "/employees/{employee_id}",
          "query": [],
          "headers": {},
          "body": "none",
          "result_path": null
        }
      }
    ]
  }
}
```

**Wexa MCP server**

```json
{
  "content": [
    {
      "type": "text",
      "text": "{\"lifecycle_id\":\"qlc_000017\",\"result\":{\"connector_id\":\"6b1a3d5e7f9c0b2a4d6e8f02\",\"name\":\"Payroll\",\"category\":\"user_payroll\",\"description\":\"Read employees from the payroll API.\",\"logo\":null,\"mode\":\"live\",\"base_url\":\"https://payroll.example.com/v1\",\"auth\":{\"type\":\"api_key\",\"name\":\"X-API-Key\",\"location\":\"header\"},\"headers\":{},\"connected\":false,\"lifecycle\":\"draft\",\"access_scope\":\"individual\",\"has_credentials\":true,\"user_id\":\"6a0f1e2d3c4b5a6978877665\",\"project_id\":\"6a0f1e2d3c4b5a6978877601\",\"org_id\":\"6a0f1e2d3c4b5a6978877600\",\"actions\":[{\"key\":\"get_employee\",\"name\":\"Get employee\",\"description\":\"One employee by id.\",\"input_schema\":{\"fields\":[{\"name\":\"employee_id\",\"type\":\"string\",\"required\":true}]},\"output_schema\":{\"fields\":[{\"name\":\"id\",\"type\":\"string\"},{\"name\":\"name\",\"type\":\"string\"}]},\"fault\":null,\"request\":{\"method\":\"GET\",\"path\":\"/employees/{employee_id}\",\"query\":[],\"headers\":{},\"body\":\"none\",\"result_path\":null}}]}}"
    }
  ],
  "isError": false
}
```

**TypeScript SDK**

```ts
// the object you get back IS the result — there is no `.result` to read
{
  "connector_id": "6b1a3d5e7f9c0b2a4d6e8f02",
  "name": "Payroll",
  "category": "user_payroll",
  "description": "Read employees from the payroll API.",
  "logo": null,
  "mode": "live",
  "base_url": "https://payroll.example.com/v1",
  "auth": {
    "type": "api_key",
    "name": "X-API-Key",
    "location": "header"
  },
  "headers": {},
  "connected": false,
  "lifecycle": "draft",
  "access_scope": "individual",
  "has_credentials": true,
  "user_id": "6a0f1e2d3c4b5a6978877665",
  "project_id": "6a0f1e2d3c4b5a6978877601",
  "org_id": "6a0f1e2d3c4b5a6978877600",
  "actions": [
    {
      "key": "get_employee",
      "name": "Get employee",
      "description": "One employee by id.",
      "input_schema": {
        "fields": [
          {
            "name": "employee_id",
            "type": "string",
            "required": true
          }
        ]
      },
      "output_schema": {
        "fields": [
          {
            "name": "id",
            "type": "string"
          },
          {
            "name": "name",
            "type": "string"
          }
        ]
      },
      "fault": null,
      "request": {
        "method": "GET",
        "path": "/employees/{employee_id}",
        "query": [],
        "headers": {},
        "body": "none",
        "result_path": null
      }
    }
  ]
}

// The TS SDK names these camelCase — lifecycleId, traceId — where Python uses
// lifecycle_id and trace_id. Both are non-enumerable: readable, but omitted by
// Object.keys() and JSON.stringify(). `result.lifecycleId` here is undefined.
result.lifecycleId // 'qlc_000017'
```

**Python SDK**

```python
# a dict subclass; `result` is already unwrapped
{
 "connector_id": "6b1a3d5e7f9c0b2a4d6e8f02",
 "name": "Payroll",
 "category": "user_payroll",
 "description": "Read employees from the payroll API.",
 "logo": None,
 "mode": "live",
 "base_url": "https://payroll.example.com/v1",
 "auth": {
  "type": "api_key",
  "name": "X-API-Key",
  "location": "header"
 },
 "headers": {},
 "connected": False,
 "lifecycle": "draft",
 "access_scope": "individual",
 "has_credentials": True,
 "user_id": "6a0f1e2d3c4b5a6978877665",
 "project_id": "6a0f1e2d3c4b5a6978877601",
 "org_id": "6a0f1e2d3c4b5a6978877600",
 "actions": [
  {
   "key": "get_employee",
   "name": "Get employee",
   "description": "One employee by id.",
   "input_schema": {
    "fields": [
     {
      "name": "employee_id",
      "type": "string",
      "required": True
     }
    ]
   },
   "output_schema": {
    "fields": [
     {
      "name": "id",
      "type": "string"
     },
     {
      "name": "name",
      "type": "string"
     }
    ]
   },
   "fault": None,
   "request": {
    "method": "GET",
    "path": "/employees/{employee_id}",
    "query": [],
    "headers": {},
    "body": "none",
    "result_path": None
   }
  }
 ]
}

out.lifecycle_id  # 'qlc_000017' — an attribute, not a key
```

Keeping the `lifecycle_id` is what lets you ask later why a call was allowed, refused or held.

**More details**

**Response fields**

The connector as it now stands, the same as [`get-user-connector`](/docs/connectors/get-user-connector) returns it:

- `connector_id`: The connector's id. Pass it to the other user-defined connector tools.
- `name`: Its name.
- `category`: Its category. A user-defined connector's category starts with `user_`.
- `description`: Its description; empty when it has none.
- `logo`: Its logo, or null.
- `mode`: `live` (calls the real API) or `simulated`.
- `base_url`: A live connector's base URL; every action's request path is appended to it.
- `auth`: How requests authenticate: `type` is `none`, `api_key`, `bearer` or `basic`; for `api_key`, `name` is the header or query parameter and `location` is `header` or `query`.
- `headers`: Static headers sent on every request.
- `connected`: Whether its actions are skills agents can use. Publishing connects it.
- `lifecycle`: `draft` or `published`.
- `access_scope`: `individual` (a draft, its author's alone), `project` or `organization`.
- `has_credentials`: Whether credentials are saved. The credentials themselves are never returned.
- `user_id`: Its author.
- `project_id`: The project it belongs to.
- `org_id`: The organization it belongs to.
- `actions`: Its actions. Each has a `key`, a `name`, a `description`, an `input_schema`, an `output_schema`, a `fault` and, for a live connector, the `request` it makes (`method`, `path`, `query`, `headers`, `body`, `result_path`).

To create a connector, pass its `definition` and leave `connector_id` out. To update one, pass its `connector_id` and a new `definition`, new `credentials`, or both. You may edit your own connectors, and a project admin may also edit a published one in this project.

The definition is `name`, `description`, `mode` (`live` or `simulated`), `base_url`, `auth` (`type` is `none`, `api_key`, `bearer` or `basic`, with `name` and `location` for an API key), `headers`, and `actions`. Each action has a `key`, a `name`, a `description`, an `input_schema`, an `output_schema` and, for a live connector, the `request` it makes (`method`, `path`, `query`, `headers`, `body`, `result_path`). [`import-connector-spec`](/docs/connectors/import-connector-spec) drafts one from an OpenAPI document.

`credentials` are write-only. Wexa keeps `api_key`, `token`, `username` and `password`, saves them to its vault, and never returns them. Do not put secrets in `headers`. The `credentials` argument is masked wherever Wexa records the call's arguments, and the answer never carries it.

The connector id in the example is illustrative. Swap it for one `list-user-connectors` returned to you.

Not marked consequential: a draft is its author's alone, so saving one affects nobody else, and the default write gate does not hold it. Publishing is the consequential step; see [`publish-user-connector`](/docs/connectors/publish-user-connector).

Every tool call works the same way: [How every tool call works](/docs/sdks-and-apis/how-every-tool-call-works).
