Currency

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:

  1. Static credentials — for providers that authenticate with API keys (Resend, SendGrid, WA Cloud, Twilio, TikTok). POST the credentials directly.
  2. OAuth flow — for providers that require browser hops (Meta, LinkedIn, Twitter, YouTube, Pinterest, Threads). Send the merchant's browser to GET /channels/oauth/:provider/start and let the provider's callback complete it.

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
POST /api/v1/channels/:id/test Send a test message
GET /api/v1/channels/:id/dns-records Email DNS guidance

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": "clx3k9v0000...",
      "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

A trimmed view of the new channel — exactly seven fields: id, provider, displayName, status (always active on this path), externalId, config, createdAt. The remaining readable fields (scopesGranted, lastSyncedAt, lastError, expiresAt) are only returned by GET /api/v1/channels/:id, so follow up with a retrieve if you need them.

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 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.contentVariables map to a pre-approved Twilio Content template; pass content.text instead for freeform replies inside the 24h customer window. Falls back to config.fromNumber if not stored in credentials.
  • Push (OneSignal): appName for 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 and config only. Any other key — status, scopesGranted, credentials, provider — is stripped by the schema, so the request returns 200 having changed nothing. There is no API path to set status, which means a revoked or expired channel cannot be reactivated through this endpoint: reconnect it instead (POST /api/v1/channels, or the OAuth start for OAuth providers).

Returns { "id": "…" }, not the updated object.

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, and status-only. The handler writes exactly one field: status: "revoked". Any subsequent campaign send that references this channel returns 400 NO_CHANNEL, and POST /:id/test returns 409 BAD_STATE. Returns { "id": "…", "status": "revoked" }.

Revoking does not delete the credentials. The encrypted credentials blob stays on the row indefinitely — the row is kept for historical reference and nothing scrubs it. If a merchant disconnects because a provider key leaked, tell them to rotate or delete that key at the provider; revoking in Ripllo only stops Ripllo from using it.

Send a test message

POST /api/v1/channels/:id/test

The primary way to verify credentials actually work. Bypasses contacts, audiences and campaigns: it queues one real MarketingMessage to the recipient you name, dispatched by the worker through the same adapter a campaign send would use. Bad credentials show up as a failed message row with a meaningful error, not as an error on this call.

Request body

Field Type Required Notes
recipient string yes Where to send: an email address, an E.164 phone number, a chat id — whatever the provider addresses.

The message body is generated for you, shaped per provider family (subject/html/text for email, text for SMS and chat, title/body for push).

Response — 200 OK

{
  "data": {
    "messageId": "clx3k9v0000...",
    "queued": true,
    "hint": "check the message in /audit-log or /webhooks for delivery status within ~10s"
  },
  "error": null,
  "meta": { "requestId": "...", "timestamp": "..." }
}

This is asynchronousqueued: true means accepted for dispatch, not delivered. Poll the message or watch the audit log for the outcome.

Errors

Status error.code When
400 VALIDATION recipient missing or blank.
404 NOT_FOUND No such channel in this workspace.
409 BAD_STATE Channel status is not active (message names the actual status).

Email DNS guidance

GET /api/v1/channels/:id/dns-records

Email channels only. Returns the SPF / DKIM / DMARC records the merchant should publish on the domain of their config.fromEmail, so mail doesn't land in spam.

The records are static, provider-specific guidance — Ripllo does not resolve DNS and does not check whether you've published them. Values that only the provider can mint (DKIM selectors and keys) come back as placeholders with a providerDashboard deep link.

Response — 200 OK

{
  "data": {
    "provider": "email_resend",
    "domain": "example.com",
    "records": [
      { "kind": "SPF", "name": "example.com", "value": "v=spf1 include:amazonses.com ~all", "hint": "Single TXT record at the domain root..." },
      { "kind": "DKIM", "name": "resend._domainkey.example.com", "value": "<copy from Resend dashboard>", "hint": "Resend mints the public key on first verify.", "providerDashboard": "https://resend.com/domains" },
      { "kind": "DMARC", "name": "_dmarc.example.com", "value": "v=DMARC1; p=quarantine; ...", "hint": "Ramp `p` from `none` → `quarantine` → `reject`..." }
    ]
  },
  "error": null,
  "meta": { "requestId": "...", "timestamp": "..." }
}

kind is one of SPF, DKIM, DMARC, CNAME.

Errors

Status error.code When
404 NOT_FOUND No such channel in this workspace.
409 NOT_EMAIL Channel provider isn't an email_* one.
409 NO_FROM_EMAIL config.fromEmail isn't set, so there's no domain to build records for.

The channel integration object

Field Type Nullable Notes
id string no Opaque cuid (clx3k9v0000…), no prefix.
accountId string no Owning workspace. Stored, but not included in API responses.
provider enum no See Providers.
externalId string yes Provider-side identifier. OAuth-created channels carry the literal "pending" until asset selection lands.
displayName string no Human label.
status enum no pending (column default), active, expired, revoked. There is no errored status — don't branch on one.
config object no Free-form per-provider config.
credentials (encrypted) no Never returned over the wire.
scopesGranted string[] no Audit-only. Filled from the OAuth token response on that path.
lastSyncedAt ISO 8601 yes Stamped at connect time only (static create, or OAuth callback). Nothing updates it per dispatch.
lastError string yes Currently always null — no code writes it. Per-send failures live on the MarketingMessage row instead, which is what the test endpoint's hint points you at.
expiresAt ISO 8601 yes Access-token expiry, from the OAuth token response.
createdAt ISO 8601 no
updatedAt ISO 8601 no Stored, but not included in API responses.

OAuth flow (overview)

For OAuth providers, the flow is a pair of browser GETs, not JSON POSTs. The redirect URI is derived server-side from RIPLLO_OAUTH_REDIRECT_BASE — you never supply one.

  1. Send the merchant's browser to GET /api/v1/channels/oauth/:provider/start (session-authenticated, merchant role). It doesn't return a URL — it responds 302 straight to the provider's authorize page, with state carrying the workspace id.
  2. The merchant authenticates at the provider and is redirected to GET /api/v1/channels/oauth/:provider/callback?code=…&state=….
  3. Ripllo exchanges the code for tokens, encrypts them, upserts the ChannelIntegration with status: "active" and externalId: "pending", then 302s the merchant to /dashboard/channels?connected=<provider>.

Start-endpoint errors: 404 UNKNOWN_PROVIDER for a provider that isn't wired, 503 OAUTH_NOT_CONFIGURED when the provider's client-id env var is unset. The callback replies in plain text (not the JSON envelope) on failure.

There is no token refresh. The refreshToken is stored at callback time and never read again — no job scans expiresAt, and nothing flips a channel to expired. When a provider's access token lapses, sends start failing at the adapter and the merchant has to re-run the OAuth start. Refresh is roadmap, not shipped.

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 Intended for a channel going unusable after repeated failures. Reserved — nothing emits it, and no code path marks a channel unusable today.
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.
CurrencyRupiah is paid by transfer or QRIS; US dollars settle through PayPal.