Channels
A channel integration is the merchant's connection to a delivery provider — Resend for email, Twilio for SMS, the Meta Graph API for Instagram and Facebook posts, and so on. Channels are credentials plus configuration; once a channel is active, marketing campaigns can dispatch through it.
There are two ways to connect:
- Static credentials — for providers that authenticate with API keys (Resend, SendGrid, WA Cloud, Twilio, TikTok). POST the credentials directly.
- OAuth flow — for providers that require browser hops (Meta, LinkedIn, Twitter, YouTube, Pinterest, Threads). Start at
POST /channels/oauth/startand complete via the provider's callback.
This page covers the static-credential surface. The OAuth surface lives at /api/v1/channels/oauth/*; consult the Portal → Channels walkthrough for the dashboard equivalent.
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/channels |
List channels |
POST |
/api/v1/channels |
Connect a static-credential channel |
GET |
/api/v1/channels/:id |
Retrieve a channel |
PATCH |
/api/v1/channels/:id |
Update a channel |
DELETE |
/api/v1/channels/:id |
Revoke a channel |
All endpoints require the merchant role.
List channels
GET /api/v1/channels
Returns every channel integration in the workspace, newest first. Credentials are never included — the response is the public-safe view.
{
"data": [
{
"id": "chi_01HX...",
"provider": "email_resend",
"externalId": null,
"displayName": "Production transactional",
"status": "active",
"config": { "fromEmail": "hello@example.com", "fromName": "ExampleCo" },
"scopesGranted": [],
"lastSyncedAt": "2026-05-13T10:42:00.000Z",
"lastError": null,
"expiresAt": null,
"createdAt": "2026-05-01T10:42:00.000Z"
}
],
"error": null,
"meta": { "requestId": "...", "timestamp": "..." }
}
Connect a channel
POST /api/v1/channels
For static-credential providers only. The credentials are wrapped using the per-merchant channel encryption key (channel-crypto.ts) before they hit the database, and decrypted only at dispatch time inside the worker.
OAuth-only providers (meta_business, linkedin, twitter, youtube, pinterest, threads) return 400 USE_OAUTH from this endpoint — they require the OAuth flow.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
provider |
enum | yes | See the provider list below. |
displayName |
string (1–120) | yes | Human label, e.g. "Production transactional". Shown in the dashboard and campaign picker. |
externalId |
string (≤120) | null | no | Provider-side identifier, e.g. Twilio's messagingServiceSid, WhatsApp's phoneNumberId. |
credentials |
object (string-to-string) | yes | Raw provider credentials. The exact keys depend on the provider — see Credentials per provider. |
config |
object | no | Provider-specific configuration (e.g., fromEmail, fromName for email). |
scopesGranted |
string[] | no | Recorded for audit; not used at runtime. |
Response — 201 Created
The full channel object (sans credentials).
Errors
| Status | error.code |
When |
|---|---|---|
400 |
VALIDATION |
Shape wrong. |
400 |
USE_OAUTH |
Tried to POST credentials for an OAuth-only provider. |
Examples
await ripllo.channels.create({
provider: 'email_resend',
displayName: 'Production transactional',
credentials: { apiKey: process.env.RESEND_API_KEY },
config: { fromEmail: 'hello@example.com', fromName: 'ExampleCo' },
});
Providers
| Category | Providers |
|---|---|
email_resend, email_sendgrid, email_mailgun, email_postmark, email_ses |
|
| SMS | sms_twilio, sms_vonage |
| Messaging | whatsapp_cloud, whatsapp_twilio, telegram_bot, line_business, discord_webhook, slack_webhook |
| Push | push_onesignal, push_fcm |
| Social (OAuth only) | meta_business, linkedin, tiktok_business, twitter, youtube, pinterest, threads |
| Generic | webhook_generic |
tiktok_business straddles the two paths: it accepts static credentials (long-lived access token from the developer portal) but most merchants reach it via OAuth.
Credentials per provider
The exact keys to put in credentials:
| Provider | Keys |
|---|---|
email_resend |
apiKey |
email_sendgrid |
apiKey |
email_mailgun |
apiKey, domain |
email_postmark |
serverToken |
email_ses |
accessKeyId, secretAccessKey, region |
sms_twilio |
accountSid, authToken |
sms_vonage |
apiKey, apiSecret |
whatsapp_cloud |
accessToken, phoneNumberId (the latter usually also as externalId) |
whatsapp_twilio |
accountSid, authToken, fromNumber (E.164 with whatsapp: prefix, e.g. whatsapp:+14155238886) |
telegram_bot |
botToken |
line_business |
channelAccessToken, channelSecret |
discord_webhook |
webhookUrl |
slack_webhook |
webhookUrl |
push_onesignal |
appId, apiKey |
push_fcm |
serverKey (FCM legacy) or serviceAccountJson (FCM v1) |
webhook_generic |
url, secret (optional, used to sign outbound) |
The schema accepts any string-keyed string-valued object — if you pass extra keys, they're stored and ignored. The runtime worker only reads what it needs.
Config per provider
config is opaque to Ripllo's persistence layer; per-provider workers read what they need. Common keys:
- Email:
fromEmail,fromName,replyTo. - WhatsApp Cloud:
templateNamespace(Meta's template namespace UUID). - WhatsApp Twilio: send-time
content.contentSid+content.contentVariablesmap to a pre-approved Twilio Content template; passcontent.textinstead for freeform replies inside the 24h customer window. Falls back toconfig.fromNumberif not stored incredentials. - Push (OneSignal):
appNamefor routing.
Retrieve a channel
GET /api/v1/channels/:id
Returns one channel by ID, credentials omitted.
Update a channel
PATCH /api/v1/channels/:id
Partial. Mutable fields: displayName, config, scopesGranted, status. To rotate credentials, mint a new channel and revoke the old one — in-place credential edits are not supported because they'd leave the old encrypted blob recoverable via DB snapshots.
Revoke a channel
DELETE /api/v1/channels/:id
Soft-revoke. The channel's status transitions to revoked, and any subsequent campaign send that references this channel returns 400 NO_CHANNEL. The credentials are zeroed (overwritten with the literal string "revoked" rather than null, so DB queries can audit the revoke happened). The row stays for historical reference.
The channel integration object
| Field | Type | Nullable | Notes |
|---|---|---|---|
id |
string | no | chi_ + ULID. |
accountId |
string | no | Owning workspace. |
provider |
enum | no | See Providers. |
externalId |
string | yes | Provider-side identifier. |
displayName |
string | no | Human label. |
status |
enum | no | active, errored, revoked. |
config |
object | no | Free-form per-provider config. |
credentials |
(encrypted) | no | Never returned over the wire. |
scopesGranted |
string[] | no | Audit-only. |
lastSyncedAt, lastError |
ISO 8601 / string | yes | Set by the worker on each dispatch. |
expiresAt |
ISO 8601 | yes | For OAuth tokens with refresh-token expiry. |
createdAt, updatedAt |
ISO 8601 | no |
OAuth flow (overview)
For OAuth providers, the flow is:
POST /api/v1/channels/oauth/startwith{ provider, redirectUri }. Returns a provider-specific authorization URL.- The merchant clicks through, authenticates, and is redirected back to your
redirectUriwith acode+statequery string. POST /api/v1/channels/oauth/callbackwith{ provider, code, state }. Ripllo exchanges the code for tokens, encrypts them, and creates the channel row.
Token refresh is automatic: the worker re-issues refresh tokens when it sees an expiring access token. Errored tokens flip the channel to status: "errored" and surface lastError.
Full OAuth-flow documentation is on the roadmap; the underlying routes are documented inline in channel-oauth.ts.
Events
| Event type | Fires on | Status |
|---|---|---|
ripllo.channel.connected.v1 |
New channel created (static or OAuth callback). | Reserved — not currently emitted. |
ripllo.channel.errored.v1 |
Worker flips a channel to errored after repeated failures. |
Reserved. |
ripllo.channel.revoked.v1 |
Channel deleted. | Reserved. |
Next
- Marketing campaigns — what you do with a connected channel.
- Contacts — how recipient identifiers map to channels at dispatch time.