Newsletters and Broadcasts
Sign people up to a free customer group, confirm the address with a sign-in code, and send broadcasts to its members.
In Arky a newsletter is a free customer group. A sign-up is a free order for the group, which makes the visitor a member; each issue is a broadcast to the group’s members. Joining is the “yes”; unsubscribing stops broadcasts to the address and ends no membership.
The same model covers paid newsletters and members-only email: a broadcast goes to the members of chosen groups, or of every group in an offering, free or paid.
1. The Group
A new store has no groups. Create an active permanent group without entitlements:
const updates = await sdk.eshop.customerGroup.create({
store_id: storeId,
id: crypto.randomUUID(),
key: "newsletter",
blocks: [],
term: { type: "permanent" },
entitlements: [],
purchase_requirement: null,
tax_policies: [],
status: { type: "active" },
starts_at: null,
ends_at: null,
});
A group without entitlements is free and permanent. Its sign-up order has no money, no billing address and no promotions, and it sends no order emails.
2. List It In The Catalog
The storefront signs up through a cart, so the group must be in the catalog the cart uses, priced
at zero. The store’s first market has a catalog keyed public-{market key}; any catalog the visitor
may buy from works:
const newsletter = await sdk.eshop.customerGroup.getByKey({ store_id: storeId, key: "newsletter" });
const catalog = await sdk.eshop.catalog.getByKey({ store_id: storeId, key: "public-eu" });
await sdk.eshop.catalogItem.create({
store_id: storeId,
id: crypto.randomUUID(),
catalog_id: catalog.id,
item: { type: "customer_group", customer_group_id: newsletter.id },
});
await sdk.eshop.price.create({
store_id: storeId,
id: crypto.randomUUID(),
catalog_id: catalog.id,
sellable: { type: "customer_group", customer_group_id: newsletter.id },
amount: 0,
compare_at: null,
min_quantity: 1,
max_quantity: null,
schedule: { type: "always" },
status: { type: "active" },
});
3. Sign People Up From The Site
Broadcasts reach a member only at an email they proved with the sign-in code, so nobody can sign up a stranger by typing their address. The sign-up form asks for the email, sends a code, and joins once the code is confirmed:
import { CustomerGroupJoinError, initialize } from "arky-sdk/storefront";
import { createOrderToken } from "arky-sdk/utils";
const arky = initialize("arky_pk_...", {
locale: "en",
createVisitorIdentity: () => ({
customer_id: crypto.randomUUID(),
session_id: crypto.randomUUID(),
}),
createSessionId: () => crypto.randomUUID(),
createId: () => crypto.randomUUID(),
createOrderToken,
});
const { items: catalogs } = await arky.eshop.catalog.find();
const catalog = catalogs.find((value) => value.key === "public-eu");
if (!catalog) throw new Error("Newsletter catalog unavailable");
async function startSignUp(email: string) {
const sent = await arky.newsletter.requestCode({ email, language: "en" });
return sent.type;
}
async function finishSignUp(email: string, code?: string) {
try {
const joined = await arky.newsletter.join({ email, code, key: "newsletter", catalog_id: catalog!.id });
return joined.type;
} catch (error) {
if (error instanceof CustomerGroupJoinError) return error.code;
throw error;
}
}
requestCodeemails the sign-in code in the language you name and answers{ type: "code_sent" }. When the browser is already signed in with that address it sends nothing and answers{ type: "verified" }: join without a code. Signed in with another address, it signs out first.joinconfirms the code you pass and checks that it proved this address, then signs the customer up with a free order. It makes its own cart for it, so the shopper’s cart is left alone, adds the group line, checks that the quote is free, and checks out with{ type: "free" }.- It answers
{ type: "joined", order_id }, or{ type: "already_member", customer_group_member_id }when the customer already belongs to the group, with no new order. Show both as subscribed. Joining again lifts the store’s unsubscribe for that address, so it is also how someone who unsubscribed comes back. - The cart, line and order ids and the order token come from your factories, and
joinkeeps them until a definite answer. A retry after a lost answer finishes the same sign-up without a second order; retry without the code, because the session is already signed in. joinnames the group by itskeyor itsid. The catalog must list the group for the visitor, and the group must be permanent, without entitlements and priced at zero there.- A refusal is a
CustomerGroupJoinError. Itscodeissign_in_required(the address isn’t confirmed),email_mismatch(the code proved another address),not_free, orsign_up_usedandsign_up_expired(the kept sign-up can’t finish: join again). - A customer who is already signed in with a verified email can join with
arky.eshop.customerGroup.joinFree({ key, catalog_id })(or{ id, catalog_id }), which answers the same way. - Someone the team assigned to the group gets no broadcasts until they sign up themselves; their sign-up order then reuses the same member.
- A storefront can also offer
arky.customer.unsubscribe()in the account, andresubscribe()to come back.
The arky.io footer’s newsletter form follows this flow; its source is the arky.io repository’s
src/lib/services/newsletter.ts.
4. Send A Broadcast
const draft = await sdk.broadcast.create({
store_id: storeId,
id: crypto.randomUUID(),
key: "november-news",
audience: {
include: [{ type: "customer_group", customer_group_id: newsletter.id }],
exclude: [],
conditions: [],
},
sending_address_id: news.id,
content: {
en: {
subject: "New this month",
preheader: null,
body: '<p>Here is what is new at {{store.name}}.</p><p><a href="{{unsubscribe_url}}">Unsubscribe</a></p>',
},
},
});
await sdk.broadcast.send({ store_id: storeId, id: draft.id, expected_updated_at: draft.updated_at });
Or schedule it with schedule({ send_at }), and take it back to draft with unschedule. As sending
goes on, Arky makes one email per person member of the audience who counts and joined with an
order, once per address, in the language of that order. People whose language the broadcast
doesn’t have get an email recorded not_sent; there is no fallback language. The broadcast ends sent with its
recipient_count.
Every broadcast body must contain {{unsubscribe_url}}. Arky also adds the one-click
unsubscribe headers Gmail and Yahoo require. Unsubscribing stops broadcasts to the address; it
never ends a membership, so an email link can’t cancel a paid group.
Replies to a broadcast go to its sending address’s reply-to address. Let that address receive to get them as conversations; see Support.