Repository navigation
Description
Background
CourseLit still sends its own campaigns with the email marketing engine that SendLit was extracted from. We want CourseLit to send campaigns through SendLit instead, so there's one engine to maintain and CourseLit becomes the reference platform customer (see apps/api/docs/platform-customer.md).
To do that, CourseLit needs a server-side TypeScript SDK it can import. There isn't one today. @sendlit/api-contract (ts-rest contract plus Zod schemas) is private and only used by apps/web, and the "no published SDK" gap has been flagged in earlier reviews. We also plan a SendLit CLI, so the SDK should be the shared core that the CLI builds on, the same way MediaLit does it.
MediaLit's setup to copy (codelitdev/medialit):
packages/medialitis published asmedialit, the server SDK. It's built with tsup to ESM, CJS and types, MIT licensed, with Node >= 18. It has aMediaLitclass configured withapiKeyoraccessTokenandendpoint, falling back to theMEDIALIT_API_KEYandMEDIALIT_ENDPOINTenv vars and a default cloud endpoint. It throws in the browser and validates IDs before building URLs.packages/cliis published as@medialit/cli(binmedialit) and depends on the SDK throughworkspace:^.- Releases go through changesets.
Target flow (first use case)
- In SendLit, create a "CourseLit" team and add an ESP to it.
- In that team, set up the onboarding campaign as a sequence with trigger
subscriber:added(EventType.SUBSCRIBER_ADDED) and start it. - In the CourseLit app (
app.courselit.app, repocourselit-subscriptions), import the SDK and callsendlit.contacts.create({ email, name, tags?, customFields? })when a user signs up. This replaces the current contact creation through CourseLit's REST API (repocourselit). - SendLit enrolls the new contact in the running sequence and starts sending.
The server already supports this, so the flow needs no API changes:
POST /contactsis find-or-create per team. A new contact firessubscriber:added(apps/api/src/contacts/queries.ts, thenautomation/fire-event.ts), which enrolls it in matching active sequences.- If the email already exists, the existing contact is returned and no event fires, so a re-signup won't re-enroll. That's the expected behaviour, but the SDK docs should say so.
- Tags passed on create don't fire
tag:added, sosubscriber:addedis the trigger to use for onboarding.
MVP scope: the client (auth, team, endpoint, errors) plus contacts.create, get, update, addTag and removeTag. That's all CourseLit needs for this flow. The rest of the resources listed below are the follow-up scope for the full SDK and the CLI.
Proposed fix
Add packages/sendlit, published as the sendlit package (or @sendlit/sdk if we prefer a scope), and keep room for a later packages/cli (@sendlit/cli, bin sendlit) that depends on it.
Client and config
- Usage looks like
new SendLit({ apiKey, accessToken?, endpoint?, teamId? }), with fallbacks toSENDLIT_API_KEY,SENDLIT_ENDPOINTandSENDLIT_TEAM_ID. The default endpoint is the hosted API. - Auth uses the
x-sendlit-apikeyheader, orAuthorization: Bearerfor an OAuth access token.x-sendlit-team-idis sent when a team is set. - Server-only: it throws in browser environments. Path params are validated before requests, as MediaLit does.
- Errors are typed (
SendLitErrorwith status, code and message). The client supports retries with backoff on 429 and 5xx for idempotent calls, and an optional idempotency key on sends.
Typed from the contract
- Generate types and requests from
@sendlit/api-contract, so the SDK can't drift from the server. Either bundle the contract into the SDK build or publish it as a dependency.
Resources (first version, scoped to what CourseLit needs)
contacts: create, list, get, update, remove, addTag, removeTag. Add an upsert-by-email if the API supports it; otherwise file a follow-up.segments: CRUD.templates: list, get, create, update, duplicate, remove.sequences, covering broadcasts and sequences (type: "broadcast" | "sequence"): create, list, get, update, remove, addEmail, updateEmail, removeEmail, start, pause, stats.emails(transactional): send, get, list.provisioning(org key): create, get, update and delete teams, plus team keys, suspend, resume and usage. This lets CourseLit provision one SendLit team per CourseLit school.suppressionsanddeliveries, read access for showing status in CourseLit.- Paginated list helpers, including an async iterator.
Packaging and docs
- tsup ESM, CJS and
.d.ts, MIT, Node >= 18,files: ["dist"], public publish through changesets. - README with a quickstart and a CourseLit-style example: provision a team, sync contacts, create and start a broadcast.
- Add an SDK page in
apps/docs.
CLI readiness (not in this ticket)
- Keep all API logic in the SDK so the future CLI is a thin wrapper. Add a separate ticket for
@sendlit/cli.
Acceptance criteria
-
packages/sendlitexists, builds with tsup to ESM, CJS and types, and is publishable to npm through changesets. - The client accepts an API key or access token, an endpoint and a team ID, with env var fallbacks, and throws in the browser.
- The listed resources work against the API and are typed from
@sendlit/api-contract(no hand-written duplicate types). - Typed errors, plus retries on 429 and 5xx for safe methods.
- Tests cover auth headers, ID validation, error mapping and at least one call per resource, against a mocked server or the integration test stack.
- README and docs page with a quickstart and a platform/provisioning example.
- End to end: a test signup in
courselit-subscriptionscallscontacts.createthrough the SDK, the contact appears in the CourseLit team, and thesubscriber:addedsequence sends its first email through the team's ESP. - Signing up again with an existing email returns the existing contact and doesn't re-enroll it (documented).
- MVP (client + contacts) can ship on its own, before the remaining resources.
- The package layout leaves room for a later
packages/clithat depends on the SDK throughworkspace:^.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsBacklog
Activity