# Register a mail trigger

POST

/v1/mail-triggers

```sh
curl --request POST \
  --url https://example.com/v1/mail-triggers \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --data '{ "flowId": "example", "flowVersion": "example", "agentId": "example", "agentVersion": "example", "projectId": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "runAsServiceAccountId": "example", "runAsProject": true, "config": { "mailbox": { "app": "google", "as": "company" }, "folder": "example", "pollSeconds": 1, "runsPerHour": 1 }, "label": "example", "toolsAllowedUnattended": [ "example" ] }'
```

Each new mail in the mailbox’s folder starts a run of the trigger’s agent or flow, with the mail as the run’s input, as the trigger’s owner. A trigger on a person’s mailbox (`config.mailbox.as: person`) reads the caller’s own mail through their connection, so the caller owns it: it needs `write` on the project, and a service account can’t own it. One on the company’s mailbox (`company`) reads it through the company’s connection and needs `admin` on the project; `runAsServiceAccountId` makes it shared, owned by that service account (which needs `act_as` on it), and `runAsProject` makes it shared, owned by the project’s own account (made the first time, with the `editor` role on the project). Both need `execute` on what it runs. A trigger that names no `projectId` goes in the tenant’s Default project. In its runs, every call that changes something, or uses the run’s own mailbox, waits for a person’s yes, unless `toolsAllowedUnattended` names it: naming it takes `admin` on the project, each id must be a tool the target calls, and it’s audited (`trigger-tools-allowed-unattended-set`).

## Authorizations

- **[bearerAuth](https://docs.kindgi.com/v0.3.1/reference/api.md#bearerauth)**

## Parameters

### Header Parameters

**Idempotency-Key**

string

\>= 1 characters

Caller-supplied idempotency key, scoped to the caller: the same key from someone else in the tenant is their own request (from 0.1.5). Retries with the same key return the original response byte-identical (per `docs/API-ROUTE-CONVENTIONS.md` §3.1). A retry sent while the first request still runs is answered `409 idempotency-key-in-flight` with `Retry-After`, on a runtime whose store holds keys (from 0.1.5); retry after it, and you get the first request’s answer. An answer that carries a secret (a new API key or public run token, a session token, a generated signing secret) isn’t kept: a retry gets `409 idempotency-key-replay-withheld`, with the first request’s status and when it succeeded (from 0.1.5). A request whose key store can’t be read before it runs is answered `503 idempotency-store-unavailable` with `Retry-After`: nothing ran, so retry with the same key (from 0.3.0).

## Request Bodyrequired

application/json

Name what it runs: `flowId` with `flowVersion`, or `agentId` (with an optional `agentVersion`). Its runs’ input is the mail, so it takes none. A trigger on a person’s mailbox reads the caller’s own mail and is theirs: it needs `write` on the project. One on the company’s mailbox needs `admin` on the project. Both need `execute` on what it runs.

object

**flowId**

Run a flow (with `flowVersion`).

string

\>= 1 characters

**flowVersion**

string

\>= 1 characters

**agentId**

Run an agent (instead of a flow): at `agentVersion`, else its live version.

string

\>= 1 characters

**agentVersion**

string

\>= 1 characters

**projectId**

The trigger’s project. Absent → the tenant’s default project.

string format: uuid

**runAsServiceAccountId**

On the company’s mailbox only: a service account that owns the trigger, so it’s shared, named by id instead of the caller. Needs `act_as` on that account, which a tenant admin holds, and the account must itself hold the access the target needs. With an authorization store, that’s checked when the account is named, and again when the trigger’s target changes: an account that can’t is refused (`409 trigger-owner-cannot-run-target`). Not a project’s own account (409 `service-account-project-owned`: name it with `runAsProject`). Absent: the caller owns it.

string

\>= 1 characters

**runAsProject**

On the company’s mailbox only. `true`: the trigger’s runs act as its project’s own service account, made the first time with the `editor` role on the project, so it’s shared. It runs only what the project’s editors can. With an authorization store, that’s checked when it’s named, and again when the trigger’s target changes: if it can’t run the target, the request is refused (`409 trigger-owner-cannot-run-target`). Takes `admin` on the trigger’s project, as the company’s mailbox does (without an authorization store, a tenant-admin key), not `act_as`. Not with `runAsServiceAccountId`, on a person’s mailbox, or without a project (400). A runtime that can’t make the account answers 501 `trigger-operation-unsupported`. `false` or absent: as `runAsServiceAccountId` says.

boolean

**config**

required

A new mail trigger’s mailbox, folder and pace.

object

**mailbox**

required

The mailbox a mail trigger reads: its app (`google`: Gmail; `microsoft`: Outlook) and whose it is (`company`: the company’s mailbox, through the company’s connection of that app; `person`: the trigger owner’s own, through their connection).

object

**app**

required

string

Allowed values: google microsoft

**as**

required

string

Allowed values: company person

**folder**

The Gmail label id, or the Outlook folder id or well-known name, it watches. Absent: the inbox.

string

\>= 1 characters <= 512 characters

**pollSeconds**

How often it looks for new mail, in seconds (default 60).

integer

\>= 30 <= 3600

**runsPerHour**

The most runs its mail starts in an hour (default 60). Past it, a mail’s fire is `refused` (`detail` starts `over-limit:`), and that mail doesn’t run later.

integer

\>= 1 <= 600

**label**

string

**toolsAllowedUnattended**

The tools whose calls in this trigger’s runs needn’t wait for a person’s yes. Naming it, `[]` included, takes `admin` on the trigger’s project. Each tool by its id, with no wildcard or pattern, and only tools this trigger’s flow or agent calls. It lifts only that wait: the tenant’s approval rules, the agent’s own rules, the permission check (as the trigger’s owner) and the tool’s own limits all still apply.

Array\<string>

<= 100 items unique items

## Responses

### 201

Mail trigger registered.

application/json

A mail trigger: each new mail in its mailbox’s folder starts a run of its agent or flow, with the mail as the run’s input, as its owner. In such a run, every call that changes something, or uses the run’s own mailbox, waits for a person’s yes unless the trigger lists it (`toolsAllowedUnattended`).

object

**triggerId**

required

string

**flowId**

Run a flow (with `flowVersion`).

string

\>= 1 characters

**flowVersion**

string

\>= 1 characters

**agentId**

Run an agent (instead of a flow): at `agentVersion`, else its live version.

string

\>= 1 characters

**agentVersion**

string

\>= 1 characters

**projectId**

required

The trigger’s project: its runs are this project’s.

string format: uuid

**owner**

required

Who its runs act as, with their connections. A service account owns a shared trigger, on the company’s mailbox: one named with `runAsServiceAccountId`, or the project’s own (`runAsProject`, `isProjectAccount`). A person owns a personal one: every trigger on a person’s mailbox, and one on the company’s mailbox registered without either. No edit moves it; only a take-over (`POST …/owner`) does.

object

**kind**

required

string

Allowed values: user service

**id**

required

string

**displayName**

The owner’s name at the time of the response: the person’s display name, or the service account’s name. Absent when it can’t be read (no directory, a removed account) and from a runtime before Kindgi 0.1.6: show the id then.

string

**isProjectAccount**

Set when the owner is the trigger’s project’s own service account (`runAsProject`): the trigger is shared, and runs as the project. Show it as “This project”. Absent otherwise.

boolean

**mailbox**

required

The mailbox a mail trigger reads: its app (`google`: Gmail; `microsoft`: Outlook) and whose it is (`company`: the company’s mailbox, through the company’s connection of that app; `person`: the trigger owner’s own, through their connection).

object

**app**

required

string

Allowed values: google microsoft

**as**

required

string

Allowed values: company person

**folder**

The Gmail label id, or the Outlook folder id or well-known name, it watches. Absent: the inbox.

string

\>= 1 characters <= 512 characters

**pollSeconds**

required

How often it looks for new mail, in seconds (default 60).

integer

\>= 30 <= 3600

**runsPerHour**

required

The most runs its mail starts in an hour (default 60). Past it, a mail’s fire is `refused` (`detail` starts `over-limit:`), and that mail doesn’t run later.

integer

\>= 1 <= 600

**label**

required

string | null

**status**

required

Lifecycle status. Only `active` triggers fire. Tombstoned rows are excluded from every read path.

string

Allowed values: active paused

**statusReason**

Why it isn’t polling as it should: `connection-missing` (the mailbox’s connection is gone or lapsed; polls go on, and work again once it’s back), `owner-refused` (its owner was removed or may no longer run the target; polls go on and read nothing until that changes or an admin takes it over), or why the runtime paused it.

string

**version**

The trigger’s newest version (`GET …/versions`): one more for each change to what it runs, when, as whom or with what. Absent until its first version.

integer

\>= 1

**account**

The mailbox’s account at its last poll: the connection’s account id and its label (the address). Absent before its first poll.

object

**id**

required

string

**label**

required

string

**lastPolledAt**

When it last polled the mailbox and got an answer.

string format: date-time

**toolsAllowedUnattended**

The tools whose calls in this trigger’s runs don’t wait for a person’s yes, though a run a mail started asks for one for every call that changes something or uses its mailbox. Absent: none. Each tool by its id, with no wildcard or pattern, and only tools this trigger’s flow or agent calls. It lifts only that wait: the tenant’s approval rules, the agent’s own rules, the permission check (as the trigger’s owner) and the tool’s own limits all still apply.

Array\<string>

<= 100 items unique items

**lastFiredAt**

required

string | null format: date-time

**createdAt**

required

string format: date-time

**updatedAt**

required

string format: date-time

##### Example

```json
{
  "owner": {
    "kind": "user",
    "isProjectAccount": true
  },
  "mailbox": {
    "app": "google",
    "as": "company"
  },
  "status": "active"
}
```

### 400

A body that fails validation (`bad-input`): no target or two, a `config` with anything but `mailbox`, `folder`, `pollSeconds` and `runsPerHour`, a service account named or calling or `runAsProject` for a person’s mailbox, or `runAsProject` with `runAsServiceAccountId` or with no project. Or `reference-out-of-scope`: what it runs isn’t the project’s to reach (`details.issues`, at `/target`), and nothing is registered.

application/json

object

**error**

required

object

**code**

required

Stable machine-readable discriminant. Values match domain error codes (see `docs/API-ROUTE-CONVENTIONS.md` §4.3).

string

**message**

required

string

**details**

Optional, kind-specific.

object

**_key_**

additional properties

any

**requestId**

required

Server-assigned request id; also echoed via `X-Request-Id` header.

string

##### Examplegenerated

```json
{
  "error": {
    "code": "example",
    "message": "example",
    "details": {},
    "requestId": "example"
  }
}
```

### 401

Missing / malformed / expired / revoked bearer token.

application/json

object

**error**

required

object

**code**

required

Stable machine-readable discriminant. Values match domain error codes (see `docs/API-ROUTE-CONVENTIONS.md` §4.3).

string

**message**

required

string

**details**

Optional, kind-specific.

object

**_key_**

additional properties

any

**requestId**

required

Server-assigned request id; also echoed via `X-Request-Id` header.

string

##### Examplegenerated

```json
{
  "error": {
    "code": "example",
    "message": "example",
    "details": {},
    "requestId": "example"
  }
}
```

### 409

`runAsServiceAccountId` names an unregistered account (`service-account-unregistered`), a project’s own account (`service-account-project-owned`: name it with `runAsProject`), or one that can’t run what the trigger runs (`trigger-owner-cannot-run-target`). Or, with `runAsProject`, the project’s own account can’t run what the trigger runs either (`trigger-owner-cannot-run-target`; the account stays, for the next `runAsProject`), or an active account someone made already has the project account’s name, `project-<projectId>` (`service-account-name-taken`: a tenant admin can unregister it). Or the Idempotency-Key was reused with a different body, or a resource-state conflict.

application/json

object

**error**

required

object

**code**

required

Stable machine-readable discriminant. Values match domain error codes (see `docs/API-ROUTE-CONVENTIONS.md` §4.3).

string

**message**

required

string

**details**

Optional, kind-specific.

object

**_key_**

additional properties

any

**requestId**

required

Server-assigned request id; also echoed via `X-Request-Id` header.

string

##### Examplegenerated

```json
{
  "error": {
    "code": "example",
    "message": "example",
    "details": {},
    "requestId": "example"
  }
}
```

### 413

The request body is over the API’s limit (`request-too-large`; `details.maxBytes` says how much).

application/json

object

**error**

required

object

**code**

required

Stable machine-readable discriminant. Values match domain error codes (see `docs/API-ROUTE-CONVENTIONS.md` §4.3).

string

**message**

required

string

**details**

Optional, kind-specific.

object

**_key_**

additional properties

any

**requestId**

required

Server-assigned request id; also echoed via `X-Request-Id` header.

string

##### Examplegenerated

```json
{
  "error": {
    "code": "example",
    "message": "example",
    "details": {},
    "requestId": "example"
  }
}
```

### 500

Server error (unmapped domain code or framework crash).

application/json

object

**error**

required

object

**code**

required

Stable machine-readable discriminant. Values match domain error codes (see `docs/API-ROUTE-CONVENTIONS.md` §4.3).

string

**message**

required

string

**details**

Optional, kind-specific.

object

**_key_**

additional properties

any

**requestId**

required

Server-assigned request id; also echoed via `X-Request-Id` header.

string

##### Examplegenerated

```json
{
  "error": {
    "code": "example",
    "message": "example",
    "details": {},
    "requestId": "example"
  }
}
```

### 501

`trigger-operation-unsupported`: `runAsProject` on a runtime that can’t make a project’s own account.

application/json

object

**error**

required

object

**code**

required

Stable machine-readable discriminant. Values match domain error codes (see `docs/API-ROUTE-CONVENTIONS.md` §4.3).

string

**message**

required

string

**details**

Optional, kind-specific.

object

**_key_**

additional properties

any

**requestId**

required

Server-assigned request id; also echoed via `X-Request-Id` header.

string

##### Examplegenerated

```json
{
  "error": {
    "code": "example",
    "message": "example",
    "details": {},
    "requestId": "example"
  }
}
```

### 503

The `Idempotency-Key` store couldn’t be read before the request ran (`idempotency-store-unavailable`), so nothing was changed; `Retry-After` (whole seconds) says when to try again with the same key.

application/json

object

**error**

required

object

**code**

required

Stable machine-readable discriminant. Values match domain error codes (see `docs/API-ROUTE-CONVENTIONS.md` §4.3).

string

**message**

required

string

**details**

Optional, kind-specific.

object

**_key_**

additional properties

any

**requestId**

required

Server-assigned request id; also echoed via `X-Request-Id` header.

string

##### Examplegenerated

```json
{
  "error": {
    "code": "example",
    "message": "example",
    "details": {},
    "requestId": "example"
  }
}
```

## See also

- [`GET /v1/mail-triggers`: List mail triggers](https://docs.kindgi.com/v0.3.1/reference/api/operations/mailtriggerslist.md)
- [Error code `bad-input`](https://docs.kindgi.com/v0.3.1/reference/errors.md#bad-input)
- [Error code `idempotency-store-unavailable`](https://docs.kindgi.com/v0.3.1/reference/errors.md#idempotency-store-unavailable)
- [Error code `reference-out-of-scope`](https://docs.kindgi.com/v0.3.1/reference/errors.md#reference-out-of-scope)
- [Error code `request-too-large`](https://docs.kindgi.com/v0.3.1/reference/errors.md#request-too-large)
- [Error code `service-account-name-taken`](https://docs.kindgi.com/v0.3.1/reference/errors.md#service-account-name-taken)
- [Error code `service-account-project-owned`](https://docs.kindgi.com/v0.3.1/reference/errors.md#service-account-project-owned)
- [Error code `service-account-unregistered`](https://docs.kindgi.com/v0.3.1/reference/errors.md#service-account-unregistered)
- [Error code `trigger-operation-unsupported`](https://docs.kindgi.com/v0.3.1/reference/errors.md#trigger-operation-unsupported)
- [Error code `trigger-owner-cannot-run-target`](https://docs.kindgi.com/v0.3.1/reference/errors.md#trigger-owner-cannot-run-target)
- [Every error code](https://docs.kindgi.com/v0.3.1/reference/errors.md)
