> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tenantcore.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Sending integrations

> Create TenantCore-owned sending-platform connections and connect TenantCore-provisioned mailboxes without exposing raw provider APIs.

## Overview

The sending-integration API exposes TenantCore-defined operations for provider accounts already linked to your TenantCore account.

The authorization chain remains:

`API key → TenantCore account UUID → TenantCore-owned integration/mailbox → provider action`

The public API never accepts an arbitrary Microsoft tenant, arbitrary external mailbox, or raw provider mailbox identifier as authority to perform work.

Current public providers:

* Instantly
* PlusVibe
* Smartlead
* ManyReach

ReachKit and Email Bison are not exposed through `/v1` until their vendor-supported onboarding/OAuth access is available to TenantCore.

<Note>
  Provider credentials are submitted only when creating a TenantCore integration. TenantCore encrypts and stores them server-side. They are never returned by `/v1` list/detail endpoints.
</Note>

## Create an integration

```http theme={null}
POST /v1/integrations
```

```json theme={null}
{
  "provider": "instantly",
  "name": "Primary Instantly Workspace",
  "api_key": "provider-api-key"
}
```

PlusVibe may also require a `workspace_id`:

```json theme={null}
{
  "provider": "plusvibe",
  "name": "Client Workspace",
  "api_key": "provider-api-key",
  "workspace_id": "workspace-id"
}
```

Example:

```bash theme={null}
curl -X POST https://api.tenantcore.io/v1/integrations \
  -H "Authorization: Bearer $TENANTCORE_API_KEY" \
  -H "Idempotency-Key: integration-create-001" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "instantly",
    "name": "Primary Instantly Workspace",
    "api_key": "provider-api-key"
  }'
```

TenantCore tests the provider credentials before saving the integration.

A successful response contains only safe integration metadata:

```json theme={null}
{
  "id": "integration-uuid",
  "provider": "instantly",
  "name": "Primary Instantly Workspace",
  "provider_workspace_id": "workspace-id",
  "provider_workspace_name": null,
  "status": "connected",
  "last_tested_at": "2026-09-27T23:00:00+00:00",
  "has_error": false,
  "created_at": "2026-09-27T23:00:00+00:00",
  "updated_at": "2026-09-27T23:00:00+00:00",
  "capabilities": {
    "microsoft_oauth": true,
    "mailbox_refresh": true,
    "mailbox_disconnect": true
  }
}
```

The provider API key is never echoed back.

### PlusVibe workspace selection

If a PlusVibe key can access more than one workspace and no `workspace_id` is supplied, TenantCore returns `400 plusvibe_workspace_required` with a safe list of workspace IDs/names. Resubmit the create request with the selected `workspace_id` and a new idempotency key.

## List integrations

```http theme={null}
GET /v1/integrations
```

This returns only integrations owned by the authenticated TenantCore account.

## Get one integration

```http theme={null}
GET /v1/integrations/{integration_id}
```

The detail response includes a persisted mailbox-connection summary without calling the sending provider:

```json theme={null}
{
  "id": "integration-uuid",
  "provider": "instantly",
  "name": "Primary Instantly Workspace",
  "status": "connected",
  "has_error": false,
  "mailbox_connections": {
    "total": 12,
    "connected": 10,
    "pending": 1,
    "failed": 1,
    "disconnected": 0,
    "other": 0,
    "has_errors": true
  }
}
```

This summary is database-backed and stays in the normal read-rate bucket. Use the provider refresh endpoint only when you intentionally want TenantCore to reconcile against the upstream provider.

A guessed integration UUID owned by another TenantCore account returns the same `404 integration_not_found` shape as a nonexistent UUID.

## Test provider connectivity

```http theme={null}
POST /v1/integrations/{integration_id}/test
```

```bash theme={null}
curl -X POST \
  https://api.tenantcore.io/v1/integrations/$INTEGRATION_ID/test \
  -H "Authorization: Bearer $TENANTCORE_API_KEY" \
  -H "Idempotency-Key: integration-test-001"
```

This performs a real provider connectivity check and therefore uses the provider-heavy rate-limit bucket.

## List eligible TenantCore mailboxes

```http theme={null}
GET /v1/integrations/{integration_id}/mailboxes
```

The list is generated from the authenticated TenantCore account. It contains only TenantCore-provisioned mailboxes from owned BYOT or TenantCore-managed tenants.

Example mailbox state:

```json theme={null}
{
  "mailbox_id": "mailbox-uuid",
  "tenant_id": "microsoft-tenant-guid",
  "tenant_name": "Acme",
  "domain_name": "mail.acme.com",
  "display_name": "Taylor",
  "primary_smtp": "taylor@mail.acme.com",
  "account_enabled": true,
  "managed_by_tenantcore": true,
  "connection": {
    "id": "connection-uuid",
    "status": "connected",
    "provider_account_id": "provider-account-id",
    "provider_account_email": "taylor@mail.acme.com",
    "has_error": false,
    "connected_at": "2026-09-27T23:10:00+00:00",
    "updated_at": "2026-09-27T23:10:00+00:00"
  }
}
```

Raw provider errors are not returned in the mailbox list.

## Start Microsoft authorization for a mailbox

Use the TenantCore mailbox UUID returned from the mailbox API/integration mailbox list:

```http theme={null}
POST /v1/integrations/{integration_id}/mailboxes/{mailbox_id}/oauth/microsoft/init
```

Example:

```bash theme={null}
curl -X POST \
  https://api.tenantcore.io/v1/integrations/$INTEGRATION_ID/mailboxes/$MAILBOX_ID/oauth/microsoft/init \
  -H "Authorization: Bearer $TENANTCORE_API_KEY" \
  -H "Idempotency-Key: mailbox-oauth-start-001"
```

TenantCore validates both resources before contacting the provider:

1. `integration_id` must belong to the authenticated TenantCore account.
2. `mailbox_id` must be a TenantCore mailbox UUID.
3. The mailbox's parent tenant must be owned by the same account.
4. The mailbox must have been provisioned by TenantCore.
5. A mailbox already connected/pending on another sending platform is rejected.

Example response:

```json theme={null}
{
  "status": "pending",
  "integration_id": "integration-uuid",
  "provider": "instantly",
  "mailbox_id": "mailbox-uuid",
  "mailbox": "taylor@mail.acme.com",
  "session_id": "opaque-provider-session",
  "auth_url": "https://...",
  "expires_at": "2026-09-27T23:30:00+00:00"
}
```

Open `auth_url` in an interactive browser/popup. Do not attempt to parse or synthesize the URL/state yourself.

## Check Microsoft authorization status

```http theme={null}
GET /v1/integrations/{integration_id}/mailboxes/{mailbox_id}/oauth/microsoft/status?session_id=...
```

Possible stable states:

* `pending`
* `connected`
* `failed`
* `expired`

When connected, the response can include the provider account ID/name and connection timestamp.

<Warning>
  This status check can require an upstream provider request and uses TenantCore's provider-heavy rate-limit bucket. Do not poll in a tight loop. Respect `X-RateLimit-*` and `Retry-After` headers.
</Warning>

If a session becomes `failed` or `expired`, start a new authorization session with a new `Idempotency-Key`.

## Reconcile provider state

```http theme={null}
POST /v1/integrations/{integration_id}/mailboxes/refresh
```

Refresh is intended for recovery/reconciliation, for example when an OAuth popup was closed after the provider completed the connection.

It scans only TenantCore-owned, TenantCore-provisioned mailboxes and reconciles provider state back into TenantCore.

Example response:

```json theme={null}
{
  "integration_id": "integration-uuid",
  "provider": "instantly",
  "checked": 4,
  "connected": 10,
  "newly_connected": 1,
  "unchanged": 3,
  "error_count": 0,
  "status": "success"
}
```

Provider-specific raw errors are intentionally not returned in this aggregate endpoint.

## Disconnect a mailbox

```http theme={null}
DELETE /v1/integrations/{integration_id}/mailboxes/{mailbox_id}
```

```bash theme={null}
curl -X DELETE \
  https://api.tenantcore.io/v1/integrations/$INTEGRATION_ID/mailboxes/$MAILBOX_ID \
  -H "Authorization: Bearer $TENANTCORE_API_KEY" \
  -H "Idempotency-Key: mailbox-disconnect-001"
```

TenantCore verifies integration ownership and mailbox ownership before asking the provider to remove/disconnect the mailbox.

## Delete an integration

```http theme={null}
DELETE /v1/integrations/{integration_id}
```

Deleting the TenantCore integration removes its stored provider credential record. It does not authorize access to any other provider account or TenantCore account.

## One sending platform per mailbox

TenantCore enforces one active/pending sending-platform connection per mailbox. If a mailbox is already connected or connecting through another integration, a new connection attempt returns `409 mailbox_already_connected_elsewhere`.

Disconnect/switch the existing connection first.

## Rate limiting and idempotency

Provider-backed integration writes are classified as Microsoft/provider-heavy operations. The current default is **12 operations per 10 minutes per TenantCore account**, with a separate concurrency ceiling.

Every integration `POST` or `DELETE` request requires `Idempotency-Key`.

OAuth status reads are also provider-heavy because checking status may call the provider. Ordinary list/detail reads remain in the normal read bucket.

## Security boundaries

The public sending-integration API does **not** expose:

* raw provider API passthrough
* encrypted provider credentials
* stored provider API keys
* mailbox passwords
* TOTP/OATH seeds or current MFA codes
* arbitrary external mailbox connection by email address
* arbitrary Microsoft tenant connection by tenant GUID
* managed-tenant provisioning or CSP operations

If a provider connection needs mailbox credentials, TenantCore keeps those credentials server-side and does not return them through this API.
