@wtfalch/integrations
v0.2.0
Published
Public client SDK and API types for the Integrations service.
Readme
@wtfalch/integrations
Client SDK for the Integrations service. An organisation connects a Slack or Discord channel, and your app posts messages into it. Requires Node 22+ or a browser with Fetch and AbortSignal.timeout support. The client has no runtime package dependencies.
npm install @wtfalch/integrationsimport { ApiError, createIntegrationsClient } from '@wtfalch/integrations';
const integrations = createIntegrationsClient({
url: serviceOrigin,
credential: () => serviceKey,
organisationId,
});
// 1. Send a person to Slack or Discord to pick a channel.
const { url } = await integrations.startInstall({
registrationId, // your product's Slack or Discord registration
returnUrl: 'https://app.example.com/settings/integrations',
});
// 2. The service redirects back to returnUrl with ?integration=<connectionId>,
// or ?integration_error=<code> when the install failed.
// 3. Queue a message. Repeating the same idempotencyKey returns the same message.
const message = await integrations.send({
connectionId,
idempotencyKey: `order-${orderId}-paid`,
title: 'Order paid',
text: 'Order 1042 was paid.',
url: 'https://app.example.com/orders/1042',
});Methods
| Method | Does |
|---|---|
| createRegistration(input) | Registers a new vendor OAuth app. Returns { id, redirectUri }. Estate operators only; refused for a customer-tenant credential. |
| startInstall({ registrationId, returnUrl }) | Returns { url, expiresAt }. The URL is valid for ten minutes. returnUrl's origin must be on the registration's allowlist. |
| listConnections() | The organisation's connections. |
| getConnection(id) | One connection. state is active, broken or removed. |
| deleteConnection(id) | Disconnects the channel and removes the stored webhook. |
| checkConnection(id) | Proactively verifies the connection with the vendor, without sending a visible message; marks it broken immediately on a definitive revocation instead of waiting for the next failed send. |
| send(input) | Queues a message and returns it with state: 'queued'. |
| getMessage(id) | One message. state is queued, sent or failed. |
| listMessages(connectionId) | A connection's messages, oldest first. |
| retryMessage(id) | Requeues a failed message with a fresh attempt budget, same id and history. |
Messages
text is 1 to 1800 characters. title is at most 200. url is an absolute https URL of at most 500 characters. The service renders them for each vendor and never lets a message ping @channel, @everyone or anyone else.
Delivery is asynchronous. A vendor rate limit delays a message. After eight failed attempts a message is failed; retryMessage requeues it with a fresh attempt budget rather than sending it as a brand-new message. When a customer removes the app or deletes the channel, the connection becomes broken and its queued messages fail; read brokenReason and ask them to connect again.
State callbacks
Give a registration a callbackUrl and a callbackSecret (at least 32 characters) when you create it. The service then POSTs a signed ConnectionStateCallback to that URL each time one of the registration's connections becomes active, broken or removed. It does not follow redirects. A non-2xx answer is retried with backoff, up to eight attempts. One connection's callbacks arrive in order, each at least once, so dedupe on id.
Verify each one against the raw body before trusting it:
import { verifyCallback } from '@wtfalch/integrations';
const event = await verifyCallback({
secret: process.env.INTEGRATIONS_CALLBACK_SECRET,
body: await request.text(),
signature: request.headers.get('x-integrations-signature'),
});verifyCallback throws on a bad signature, or on a timestamp more than five minutes away.
Errors
API failures throw ApiError with status and code. 409 connection_unavailable means the connection is not active. 409 idempotency_conflict means the key was used with a different body. A network failure or a 5xx is retried up to twice more with backoff before it throws ApiError with status: 0, code: 'network_error' (network) or the upstream status (persistent 5xx). A 4xx is never retried.
Credentials
Use the deployed service's HTTPS origin and a scoped service credential. The credential callback runs for every request, so rotation does not need a new client. The client refuses redirects and omits browser cookies. An HTTP origin is accepted only for localhost development. Do not put a service credential in browser code.
The service implementation is in a private repository. This package holds only the client and its types.
