# Integration Development

External systems via GraphQL, tRPC, and webhooks


Connect an external system to erxes through GraphQL, HTTP webhooks, automation glue, or bulk import.

For headers and principals, see [Authentication & Permissions](https://erxes.io/docs/api-authentication). For the endpoint itself, see [GraphQL API](https://erxes.io/docs/api-graphql).

## Choose an approach

| You need | Use | Notes |
| --- | --- | --- |
| External system reads/writes erxes data | Federated GraphQL at `/graphql` with `erxes-app-token` | One supergraph across core and all enabled plugins |
| erxes calls your system on events | **Outgoing webhook** automation action, or a plugin `expressRouter` | Retried HTTP with auth headers, configured in the workflow UI |
| Your system pushes events into erxes | **Incoming webhook** automation trigger, or a plugin route (`/pl:<plugin>/<path>` via the gateway) | `POST /automation/<automationId>/<endpoint>` on the automations service |
| Public website content/tickets | `cp*` operations + `x-app-token` portal token | See [Client Portal](https://erxes.io/docs/app-client-portal) |
| Real-time updates | `graphql-ws` subscriptions on `wss://<gateway>/graphql` | Plugin-registered resolvers over Redis |
| Bulk one-off data load | Import/Export module (`/import-export/download-template`, workers) | See [Import & Export](https://erxes.io/docs/core-import-export) |
| Service-to-service inside the deployment | tRPC (`/<plugin>/trpc`) | Internal only; never expose or call across the public internet |

## GraphQL with an app token

Create the token once (`appsAdd` needs `appsManage`), then send it on every call:

```bash
curl --fail https://<gateway>/graphql \
  -H 'Content-Type: application/json' \
  -H 'erxes-app-token: <app-token>' \
  --data '{"query":"mutation { customersAdd(firstName:\"Ada\", primaryEmail:\"ada@example.com\") { _id } }"}'
```

The gateway validates the token against the `app_tokens` collection (`status: "active"`), stamps `lastUsedAt`, and synthesizes `req.user = { _id: "app:<id>", isOwner: app.allowAllPermission === true }`. That satisfies `checkLogin`/`wrapPermission` resolvers; operations gated by `checkPermission` (such as `customersAdd`, which needs `contactsCreate`) additionally need `allowAllPermission` set on the app record, otherwise they fail `Permission required`. Revoking the app (`appsRevoke`/`appsRemove`) fails closed with `401 Invalid app token`. The same record in the `x-app-api-token` header bypasses the CORS origin list for server-side calls from arbitrary origins.

### End-to-end: create a customer and a deal

```graphql
mutation CreateCustomer {
  customersAdd(
    firstName: "Ada"
    primaryEmail: "ada@example.com"
    state: "customer"
  ) {
    _id
  }
}
```

```graphql
mutation CreateDeal($customerId: String!) {
  dealsAdd(
    name: "Inbound - Ada"
    customerIds: [$customerId]
    stageId: "STAGE_ID"
    status: "active"
  ) {
    _id
    name
  }
}
```

`dealsAdd` is permission-gated (`dealsAdd`), so use a team-member token or an app record with `allowAllPermission`. Discover other operation names through introspection (dev or `INTROSPECTION=true`) or the plugin's `graphql/schemas/`.

## Webhooks

Two directions exist, both centered on the automations service:

- **Incoming**: an automation whose trigger type is `INCOMING_WEBHOOK` gets a URL of the form `POST|GET|… /automation/<automationId>/<config.endpoint>` on the automations service (default port 3302; reachable through the gateway's `/pl:` proxy when registered). The handler ([incomingWebhook.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/services/automations/src/executions/actions/webhook/incoming/incomingWebhook.ts)) matches on method + endpoint and keeps a 1 MB raw body for signature validation (`validateSecurity`). It applies per-webhook rate limits, validates the payload against the trigger's schema, and enqueues the execution. Failed lookups and security rejections are logged to the logs service as `webhook_security` events. A `/automation/<id>/health` probe and a `/<executionId>/<actionId>/continue/*` resume route for waiting executions are mounted alongside.
- **Outgoing**: the `OUTGOING_WEBHOOK` action posts to a configured URL with templated headers/body, optional auth config, and retries with backoff on 408/425/429/5xx.

Plugin HTTP endpoints also exist where a provider demands a fixed route: Frontline mounts `/facebook`, `/instagram`, `/mail/receive`, and `/callpro` routers (see [Email Delivery](https://erxes.io/docs/api-email)); other provider callbacks are part of the Enterprise Edition; `operation_api` mounts `POST /integrations/github/webhook` with signature verification in `githubWebhookHandler.ts`. Reach any of them through the gateway's `/pl:<plugin-name>/` proxy, which rewrites to the service root (for example `/pl:posclient/initial-setup`).

> [!WARNING]
> **tRPC is not a public API**
>
> `/<plugin>/trpc` exists on every service for service-to-service calls. It is not a supported external integration point; use GraphQL or webhooks at the gateway instead.

## Building your own inbound endpoint

If you are writing a plugin that must receive provider webhooks, follow the `operation_api` GitHub integration (`modules/githubIntegration/` + [utils/githubWebhookHandler.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/operation_api/src/utils/githubWebhookHandler.ts)): mount the route on the plugin's `expressRouter` in `startPlugin`, verify the provider signature against stored config, validate the payload, scope everything by `subdomain`, and make the handler idempotent; providers retry.

