Managing Contacts & Templates
This page shows you how to wrap contacts and templates in your service layer. Both follow the same shape as sending messages: a thin service method wraps the SDK resource, maps the snake_case response to your camelCase contract, and lets the controller stay dumb.
Samples on this page are shown in TypeScript, Python, and Go; the patterns are framework-agnostic and transfer directly to the other supported SDK languages.
Concepts first: contacts and templates. Guides: managing contacts and working with templates. This page is about wiring them into your service layer.
Contacts
Three operations: contacts.list, contacts.create, contacts.delete. The mapping is small
but non-negotiable: phone_number → phoneNumber, created_at → createdAt, and it happens in
the service, never in the controller.
// services/sent.service.ts
async listContacts(): Promise<ContactItem[]> {
const response = await this.client.contacts.list({ page: 1, page_size: 100 });
const contacts = response.data?.contacts ?? [];
return contacts.map((c) => ({
id: c.id,
phoneNumber: c.phone_number, // ← snake → camel
createdAt: c.created_at,
}));
}
async createContact(phoneNumber: string, sandbox?: boolean): Promise<ContactItem> {
const response = await this.client.contacts.create({
phone_number: phoneNumber,
sandbox: sandbox ?? false,
});
const c = response.data ?? {};
return { id: c.id, phoneNumber: c.phone_number, createdAt: c.created_at };
}
async deleteContact(id: string): Promise<void> {
await this.client.contacts.delete(id, {});
}The controller just delegates and picks status codes (201 on create, 204 on delete).
validateBody and asyncHandler are the
shared helpers from project setup:
// controllers/contacts.controller.ts
router.get('/', asyncHandler(async (req, res) => {
const { sentService } = servicesForRequest(req);
res.status(200).json({ items: await sentService.listContacts() });
}));
router.post('/', validateBody(CreateContactSchema), asyncHandler(async (req, res) => {
const { sentService } = servicesForRequest(req);
const contact = await sentService.createContact(req.body.phoneNumber, req.body.sandbox);
res.status(201).json(contact);
}));
router.delete('/:id', asyncHandler(async (req, res) => {
const { sentService } = servicesForRequest(req);
await sentService.deleteContact(String(req.params.id));
res.status(204).send();
}));Templates
Templates are read-only from your integration's side. You list them and retrieve them; you
don't create them here (that's a console/approval workflow). Two operations: templates.list
and templates.retrieve.
Two mapping gotchas to handle explicitly:
- The SDK exposes
channels(an array); your contract wants a singlechannel, so takechannels[0]. - The SDK
Templatemodel carries no renderedbody, so emitnullfor it. Don't invent it.
// services/sent.service.ts
async listTemplates(): Promise<TemplateItem[]> {
const response = await this.client.templates.list({ page: 1, page_size: 100 });
const templates = response.data?.templates ?? [];
return templates.map((t) => ({
id: t.id,
name: t.name,
channel: t.channels?.[0], // ← channels[] → channel
status: t.status,
body: null, // ← SDK has no rendered body — always null
category: t.category,
language: t.language,
}));
}
async getTemplate(id: string): Promise<TemplateItem> {
const response = await this.client.templates.retrieve(id);
const t = response.data ?? {};
return {
id: t.id, name: t.name, channel: t.channels?.[0], status: t.status,
body: null, category: t.category, language: t.language,
};
}Looking up templates by id or name
The send API accepts a template id or name. You don't need both. Prefer resolving by
name and letting templates.list give you the id when you need it, rather than pasting UUIDs
around your codebase.
// Resolve a stable, human-readable name to whatever the current template is.
async function resolveTemplate(sent: SentService, name: string): Promise<TemplateItem> {
const templates = await sent.listTemplates();
const match = templates.find((t) => t.name === name);
if (!match) throw new ApiError(404, 'TemplateNotFound', `No template named "${name}"`);
return match;
}Don't ship hard-coded template UUIDs scattered through your business logic. In real code, map
a small set of stable names (welcome, order_update) to templates, resolve them at the
edge, and let the id float. When a template is re-approved with a new id, nothing in your code
changes. Docs snippets inline an id (templateId: 'welcome-template-id') only for brevity.
Validate at send time, too: a template must be approved for the channel you're sending on.
Check status === "APPROVED" (and that the resolved channel matches your intended channel)
before you call send, so you fail with a clear error instead of a cryptic API rejection.
Budget real time for approval before your go-live date. WhatsApp template approval typically
takes 24-48 hours (see Create your first template).
SMS templates aren't gated the same way, so if you're SMS-only you can usually send immediately.
Build and test against a sandbox: true send while waiting; don't let approval latency be a
surprise on launch day.
resolveTemplate as shown makes a full listTemplates round trip on every send. Fine for a
first pass, but templates change rarely, so a short-TTL cache (30-60s in memory, or your shared
store once you're on multiple instances) removes that extra call and its rate-limit cost from
your hot send path without meaningfully delaying a template re-approval from taking effect.
Why the wrapper matters
Every method here does the same three things: call one SDK resource, map snake→camel, translate errors. That uniformity is the point.
- Controllers depend on your service interface, never on SDK types, so the SDK is mockable and swappable in tests.
- The contract stays stable even when the SDK's field names or shapes drift.
- The client is still built per request (
servicesForRequest) from the bearer key. These reads are just as scoped as the sends. See Authentication.
Next steps
Sending Messages with the Sent SDK: The Outbound Path
A thin, testable service layer over messages.send: the request shape, multi-recipient response mapping, channels, sandbox sends, and where status comes from.
Error Handling & Resilience
Turn SDK exceptions into your own error envelope, catch the right exception types per language, and build in retries, rate-limit backoff, and idempotency.