Back to Arky

Store Email

Connect the store's Resend account, add a sending address, and pick the email of each moment.

Arky sends a store’s email itself, signed with the store’s own domain, the way Shopify, Klaviyo and Ghost do. A store sends through its own Resend account, as it takes card payments through its own Stripe account: Arky decides who gets what and when, and Resend only sends. There is no shared Arky address: each store carries its own sending reputation, and Arky never sends a store’s mail under its own name.

A new store sends no email until it connects its Resend account, adds a sending address and picks the template of each moment. That includes order receipts and the sign-in codes storefront customers use, so make this part of every launch.

1. Connect The Store’s Resend Account

Add and verify the store’s domain in its own Resend account, then connect the account with a full-access API key:

import { createAdmin } from "arky-sdk/admin";

const sdk = createAdmin({ apiUrl: "https://api.arky.io" });

await sdk.notification.emailProvider.connect({ store_id: storeId, id: crypto.randomUUID(), api_key: resendApiKey });

const { items: domains } = await sdk.notification.emailProvider.findDomains({ store_id: storeId });
const domain = domains.find((value) => value.name === "northwind.example");
if (domain?.status !== "verified") console.log("Finish the domain's DNS records in Resend first");
  • Connecting checks the key and registers a report webhook in the store’s Resend account, so delivery, bounce and complaint reports come back to Arky.
  • The domain list is read live from Resend; Arky keeps no copy. Resend checks the domain when it receives each email: an email from a domain it hasn’t verified ends not_sent with Resend’s reason.
  • replaceKey swaps the key, with the version you read. The provider keeps its id, and mail already queued under the old key’s account ends not_sent.

2. Add A Sending Address

const orders = await sdk.notification.emailAddress.create({
	store_id: storeId,
	id: crypto.randomUUID(),
	email: "orders@northwind.example",
	type: { type: "sending", sending: { from_name: "Northwind", reply_to: "orders@northwind.example" } },
});

Customers see Northwind <orders@northwind.example>, and their replies go to the reply_to address, the store’s own inbox.

A store can have more addresses, such as news@ for broadcasts and support@ for support, and an address can also receive (see Support). The email settings and each broadcast name their sending address, and nothing ever switches address on its own: if an address can’t send, its emails end not_sent and staff see why.

3. Pick The Email Of Each Moment

A template is the wording of one email, with a fixed type and its text per language. Creating one selects nothing: the store’s email settings (StoreEmails) name the sending address and the template each moment sends. sdk.notification.template.findDefaults({ store_id }) lists Arky’s default wording to copy into a new template.

const receipt = await sdk.notification.template.create({
	store_id: storeId,
	id: crypto.randomUUID(),
	key: "order-receipt",
	type: { type: "order" },
	content: {
		en: {
			subject: "Thanks for your order {{order.number}}",
			preheader: null,
			body: '<p>{{media "6f4c2f0e-1d2b-4c1a-9a51-6a2a3d0f9b10"}}</p><p>We got your order {{order.number}}.</p>',
		},
	},
});

const { id: _id, store_id: _store, created_at: _created, updated_at, ...current } = await sdk.store.emails.get({
	store_id: storeId,
});

await sdk.store.emails.update({
	...current,
	store_id: storeId,
	expected_updated_at: updated_at,
	sending_address_id: orders.id,
	order_received: { email_template_id: receipt.id },
});
MomentSelection
a customer asks for a sign-in codesign_in_code
an order is placed, including pending paymentorder_received
a contact’s first order is confirmedfirst_order_placed
a shipment leaves, arrives, or a pickup is readyorder_dispatched, order_delivered, order_ready_for_pickup
an order is cancelled or refundedorder_cancelled, order_refunded
a booking is confirmed or moved, before it, or cancelledbooking_confirmed, booking_reminder, booking_cancelled
an offer is sent or revised, or still waitsoffer_sent, offer_reminder
a customer sends a form, or it reaches a stageFormEmails.received, FormEmails.stage_changed
someone is added to a companycompany_invite
a return is requested, approved or declinedreturn_requested, return_approved, return_declined
a customer group renewal comes up or its payment failsrenewal_upcoming, renewal_payment_failed
a recurring membership is cancelledmembership_cancelled
a customer proves a new email addressemail_changed, to the old address
the team should hear about an order, return or formnew_order_alert, new_return_alert, FormEmails.alert
  • No moment can be selected while the settings have no sending address (409 STORE_EMAILS.SENDER_REQUIRED); once set, the address can be replaced but never cleared, so a selected email always has a sender.
  • Each email goes in the language of what it’s about: an order’s emails in the order’s language, a form’s in the submission’s, the sign-in code in the language its request names. A template without text in that language sends nothing for it: the email ends not_sent.
  • An unselected moment sends nothing. To stop one, set its selection to null.
  • Staff choose whether the customer gets an email each time they cancel, move a booking, refund, mark a shipment, or decide a return.
  • Preview and test send fill the body with a sample of exactly the data the email will get.
  • Some fields come with only one moment of a template family, such as the receipt’s order_token. Wording that shows such a field outside an {{#if}} on it can’t be selected for a moment that doesn’t carry it (409 EMAIL_TEMPLATE.FIELD_NOT_SENT). A selected email whose wording still can’t be built from its data is recorded not_sent with render_failed: <reason>, and the action that sent it goes on; a code request whose sign_in_code wording can’t be built is refused instead.

Storefront Sign-In Codes

Storefronts never send email themselves. A customer sign-in code goes out with the template the store selected for sign_in_code; the storefront only asks for it, in the language it names:

import { 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,
});

await arky.customer.requestCode({
	email: "ana@example.com",
	language: "en",
});

await arky.customer.verify({ code: "123456" });
  • The code email’s id comes from your createId. A retry after a lost answer reuses it, so one email goes out; a request after an answer, such as “send me another code”, gets a new one. Pass id to pick it yourself. verify takes the id of the signed-in session the code makes from createSessionId, and keeps it when it retries that verification.
  • Code requests are refused at once while the store has no sign_in_code email selected, its template has no text in the language, the sending address isn’t ready or the store’s email is paused, so show your own message then. With a bot check on the publishable key, send the widget’s token as bot_check_token.
  • The same email sends the code that proves a new address when a signed-in customer changes their email.

What Arky Handles

  • Bounces and spam complaints come from the store’s Resend reports and block the address for that store. Unsubscribes from broadcast emails stop broadcasts to the address.
  • Nothing is stopped. An email that waits is never cancelled. Right before each try Arky checks the deadline, the store, its email provider, the sending address and the store’s suppressions, and an email that may no longer go ends not_sent with its reason.
  • Pausing. The store’s owner, or a platform administrator, can pause all of a store’s email, such as to catch a mistaken broadcast: emails wait until sending is allowed again. See Pause Store Email.
  • History. Every email is an outgoing email: staff search by address and see each email’s type and status. A finished one is kept 30 days.
  • Arky’s own mail, the staff sign-in code, the team invite and store billing mail, goes through Arky’s own Resend account from Arky’s address.
Tip

Support email works the other way round: the store forwards its support address to an Arky receiving address, and the team replies from Admin through a sending address. See Support. For newsletters, see Newsletters and Broadcasts.