Currency

Channels

A channel is a configured delivery surface — a Resend/SendGrid email setup, an SMS provider, a Meta/WhatsApp Business connection. Marketing campaigns reference a channel for "how do we actually get the bytes out". This page covers ripllo.channels on the Node SDK. For the HTTP surface and per-provider field tables, see API → Channels.

Namespace

ripllo.channels.list()
ripllo.channels.get(id)
ripllo.channels.create(input)
ripllo.channels.update(id, patch)
ripllo.channels.delete(id)
ripllo.channels.test(id)
ripllo.channels.dnsRecords(id)
ripllo.channels.oauthStart(provider)

// Short alias:
ripllo.channels.dns(id)        // === dnsRecords(id)

Standard CRUD plus three operational methods: test queues a real one-shot message through the channel; dnsRecords returns the SPF/DKIM/DMARC guidance for email channels; oauthStart is the browser entry point for providers that need OAuth (Meta, LinkedIn, X, YouTube, Pinterest, Threads).

There is no kind field and no short provider slugs anywhere in this API — a channel is identified by one composite provider slug like email_resend or sms_twilio, listed under API → Channels → Providers.

Methods

channels.create

Signature. ripllo.channels.create(input): Promise<Channel>

Creates a static-credential channel. The body is the same shape for every provider — provider, displayName and credentials are required; the per-provider variation lives inside credentials and config:

// Email via Resend
await ripllo.channels.create({
  provider: 'email_resend',
  displayName: 'Brand email',
  credentials: { apiKey: process.env.RESEND_API_KEY },
  config: { fromEmail: 'hello@brand.com', fromName: 'Brand' },
});

// Email via the merchant's own SendGrid
await ripllo.channels.create({
  provider: 'email_sendgrid',
  displayName: 'Brand SendGrid',
  credentials: { apiKey: 'SG.xxx' },
  config: { fromEmail: 'hello@brand.com', fromName: 'Brand' },
});

// SMS via Twilio (BYO)
await ripllo.channels.create({
  provider: 'sms_twilio',
  displayName: 'Brand SMS',
  externalId: 'MG...',                       // optional messagingServiceSid
  credentials: { accountSid: 'AC...', authToken: '...' },
  config: { fromNumber: '+1...' },
});

Credentials go in credentials (encrypted at rest), never in configconfig is returned in the clear on every read. Omitting credentials fails with VALIDATION; the six OAuth-only providers (meta_business, linkedin, twitter, youtube, pinterest, threads) fail with USE_OAUTH and must go through oauthStart.

The 201 body is trimmed: { id, provider, displayName, status, externalId, config, createdAt }. Follow with get(id) for the rest.

channels.list / channels.get

const channels = await ripllo.channels.list();   // resolves to an ARRAY
const single = await ripllo.channels.get(channels[0].id);

list() resolves to a plain array of channels, newest first — there is no { channels } wrapper to destructure, despite the declared type. Credentials are omitted from both responses.

There is no verified flag. Use status (pending | active | expired | revoked) to badge channels in a dashboard.

channels.update

await ripllo.channels.update(id, {
  displayName: 'Brand Indonesia',
  config: { fromEmail: 'hello@brand.com', fromName: 'Brand Indonesia' },
});

PATCH, and only displayName and config are accepted. config is replaced wholesale, not deep-merged — send the whole object. Any other key (status, credentials, provider, scopesGranted) is stripped, so the call returns 200 having ignored it. Resolves to { id }.

To rotate credentials, create a replacement channel and revoke the old one.

channels.delete

await ripllo.channels.delete(id);   // → { id, status: 'revoked' }

Soft revoke, not a hard delete: the row survives with status: 'revoked', and the encrypted credentials are retained. Campaigns that reference the channel fail at send with NO_CHANNEL. There is no API to un-revoke — reconnect instead.

channels.test

Signature. ripllo.channels.test(id)

Queues one real message through the channel — same adapter a campaign send uses — so the merchant can confirm their credentials work. It is not a synchronous probe: the response is { messageId, queued: true, hint }, and the outcome lands on the message row moments later.

The endpoint requires a recipient in the body, and the channel must be active.

channels.test(id) posts an empty body, so it currently fails with 400 VALIDATION: recipient required every time. Until the signature grows a second argument, call the endpoint through the client's raw request helper:

const r = await ripllo.request<{ messageId: string; queued: boolean; hint: string }>({
  method: 'POST',
  path: `/api/v1/channels/${id}/test`,
  body: { recipient: 'ops@brand.com' },
});

channels.dnsRecords

Signature. ripllo.channels.dnsRecords(id)

For email channels, returns the SPF / DKIM / DMARC records the merchant should publish on the domain of their config.fromEmail. Resolves to { provider, domain, records }.

The records are static provider guidance — Ripllo performs no DNS lookup, so there is no verified flag and no checkedAt. Each record is { kind, name, value, hint, providerDashboard? }, where kind is SPF | DKIM | DMARC | CNAME and values only the provider can mint come back as placeholders alongside a dashboard link.

const { domain, records } = await ripllo.channels.dnsRecords(id);
for (const r of records) {
  console.log(`${r.kind} ${r.name} → ${r.value}`);
}

Non-email channels return 409 NOT_EMAIL; a channel with no config.fromEmail returns 409 NO_FROM_EMAIL.

The short alias ripllo.channels.dns(id) is identical.

channels.oauthStart

Signature. ripllo.channels.oauthStart(provider: string)

Hits GET /api/v1/channels/oauth/:provider/start. Pass the full provider slug (meta_business, linkedin, twitter, youtube, pinterest, threads).

This is a browser redirect, not a URL-returning API. The endpoint answers 302 straight to the provider's authorize page; it does not return { url }. Called from a server-side SDK, fetch follows the redirect and then fails parsing the provider's HTML as JSON. Send the merchant's browser to the start URL instead:

app.get('/dashboard/channels/connect/meta', (req, res) =>
  res.redirect('https://ripllo.com/api/v1/channels/oauth/meta_business/start'),
);

The callback lives on Ripllo: after the provider hop the merchant lands back on /dashboard/channels?connected=<provider> with the channel already created (status: 'active', externalId: 'pending' until asset selection ships). Refresh the channel list there.

Start errors: 404 UNKNOWN_PROVIDER, 503 OAUTH_NOT_CONFIGURED when that provider's client-id env var isn't set on the server.

Types

type ChannelProvider =
  | 'email_resend' | 'email_sendgrid' | 'email_mailgun' | 'email_postmark' | 'email_ses'
  | 'sms_twilio' | 'sms_vonage'
  | 'whatsapp_cloud' | 'whatsapp_twilio' | 'telegram_bot' | 'line_business'
  | 'discord_webhook' | 'slack_webhook'
  | 'push_onesignal' | 'push_fcm'
  | 'meta_business' | 'linkedin' | 'tiktok_business' | 'twitter' | 'youtube'
  | 'pinterest' | 'threads'
  | 'webhook_generic';

type ChannelStatus = 'pending' | 'active' | 'expired' | 'revoked';

interface Channel {
  id: string;                  // opaque cuid
  provider: ChannelProvider;
  externalId: string | null;
  displayName: string;
  status: ChannelStatus;
  config: Record<string, unknown>;   // returned in the clear — no secrets here
  scopesGranted: string[];
  lastSyncedAt: string | null;       // stamped at connect only
  lastError: string | null;          // currently always null
  expiresAt: string | null;
  createdAt: string;
}

interface DnsRecord {
  kind: 'SPF' | 'DKIM' | 'DMARC' | 'CNAME';
  name: string;
  value: string;
  hint: string;
  providerDashboard?: string;
}

credentials is never returned by any endpoint. config is not redacted — whatever you put there is readable, which is why secrets belong in credentials.

Common patterns

Onboard a custom email domain

const ch = await ripllo.channels.create({
  provider: 'email_resend',
  displayName: 'Brand',
  credentials: { apiKey: process.env.RESEND_API_KEY },
  config: { fromEmail: 'hello@brand.com', fromName: 'Brand' },
});
const { records } = await ripllo.channels.dnsRecords(ch.id);
// Show the records to the merchant; they add them to their DNS.
// Then send yourself a real test message:
await ripllo.request({
  method: 'POST',
  path: `/api/v1/channels/${ch.id}/test`,
  body: { recipient: 'ops@brand.com' },
});

OAuth handoff

app.get('/dashboard/channels/connect/meta', (req, res) => {
  // Redirect the merchant's browser at Ripllo's start endpoint; it 302s on to Meta.
  res.redirect('https://ripllo.com/api/v1/channels/oauth/meta_business/start');
});
// The merchant returns to /dashboard/channels?connected=meta_business — refresh the list there.

Errors

Code Status Cause
VALIDATION 400 Bad body shape, unknown provider, missing credentials, or test without a recipient.
USE_OAUTH 400 Posted credentials for an OAuth-only provider.
NOT_FOUND 404 Channel ID doesn't exist in this workspace.
BAD_STATE 409 test on a channel whose status isn't active.
NOT_EMAIL / NO_FROM_EMAIL 409 dnsRecords on a non-email channel, or one with no config.fromEmail.
WRONG_ROLE 403 Principal isn't a merchant.
INSUFFICIENT_SCOPE 403 API key lacks the read (GET) or write (mutation) scope.

See Errors for handling.

Next

CurrencyRupiah is paid by transfer or QRIS; US dollars settle through PayPal.