Back to Arky

Store Email

Connected email providers, tagged sending and receiving addresses, and explicit template selections

Email Provider

Connect the store’s own Resend account. Domain creation and DNS verification happen in that account; Arky lists the account’s domains live through the key and keeps no copy. Each domain reads not_started, pending, verified, partially_verified, partially_failed, failed or temporary_failure, with its DNS records. Local sending readiness means an active sending address and a connected provider. It does not claim that the provider has verified the domain: Resend checks the domain when it receives each email, and an email it refuses ends not_sent with Resend’s reason.

Connecting checks the key and registers a report webhook in the store’s Resend account. A store has one provider. To replace the key, with the version you read, use replaceKey: the provider keeps its id, and mail already queued under the old key’s account ends not_sent while new mail goes through the new one.

POST /v1/stores/{store_id}/email-provider
SDK: sdk.notification.emailProvider.connect()
PUT /v1/stores/{store_id}/email-provider
SDK: sdk.notification.emailProvider.replaceKey()
GET /v1/stores/{store_id}/email-provider
SDK: sdk.notification.emailProvider.get()
GET /v1/stores/{store_id}/email-provider/domains
SDK: sdk.notification.emailProvider.findDomains()
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,
});

Email Addresses

An address has one tagged type: sending, receiving or sending_and_receiving. Sending owns from_name and reply_to. Receiving owns its explicit language, initial_sending_address_id, initial_flow_id, and the returned forwarding_email. A new receiving side can be added without removing a retained sending side.

POST /v1/stores/{store_id}/email-addresses
SDK: sdk.notification.emailAddress.create()
GET /v1/stores/{store_id}/email-addresses
SDK: sdk.notification.emailAddress.find()
PUT /v1/stores/{store_id}/email-addresses/{id}
SDK: sdk.notification.emailAddress.update()
const sender = await sdk.notification.emailAddress.create({
  store_id: storeId,
  id: crypto.randomUUID(),
  email: "hello@example.com",
  type: {
    type: "sending",
    sending: { from_name: "Example", reply_to: "hello@example.com" },
  },
});

Template Families And Selections

A template has a unique authored key, a fixed family type, and content by explicit language. Content contains subject, nullable preheader, and body. There is no language fallback. Families are sign_in_code, email_change, order, fulfillment, booking, offer, form, company_invite, return, renewal, membership_cancellation and support_reply. Multiple templates can share a family. Creating a template does not select it for a business moment.

POST /v1/stores/{store_id}/email-templates
SDK: sdk.notification.template.create()
GET /v1/stores/{store_id}/email-templates
SDK: sdk.notification.template.find()
POST /v1/stores/{store_id}/email-templates/{id}/preview
SDK: sdk.notification.template.preview()
POST /v1/stores/{store_id}/email-templates/{id}/test
SDK: sdk.notification.template.test()
GET /v1/stores/{store_id}/emails
SDK: sdk.store.emails.get()
PUT /v1/stores/{store_id}/emails
SDK: sdk.store.emails.update()

StoreEmails owns sending_address_id and explicit moment selections. An Email selection is { email_template_id }; an AlertEmail adds to; a TimedEmail adds days. Update the complete settings with their expected_updated_at; null leaves a moment disabled. No store email is selected automatically.

No moment can be selected while sending_address_id is empty (409 STORE_EMAILS.SENDER_REQUIRED). Once set, the sending address can be replaced but never cleared, and a form can select its emails only while the store has one, so a selected email always has a sender.

Customer moments include sign-in code, email change, received/cancelled/refunded Order, first order, exact fulfillment dispatched/delivered/ready for pickup, booking confirmed/reminder/cancelled, offer sent/reminder, company invite, return requested/approved/declined, renewal upcoming/payment failed, and membership cancellation. Staff alerts are separate new_order_alert and new_return_alert selections.

order_received is queued at placement, including pending paid checkout, with the order’s link; a free sign-up gets none. Staff can resend an order’s receipt, which queues a new email with the current wording and a new order link; it is refused while order_received is empty (409 STORE_EMAILS.ORDER_RECEIVED_EMPTY). The new link replaces the old one only when the resent email is queued to go; a resend recorded not_sent leaves the old link working. first_order_placed goes when an order’s lines are first confirmed and no earlier order of the store with confirmed lines has the same contact email. Email change goes to the previous address. Staff cancellation, booking moves, refunds, fulfillment and return decisions carry an explicit customer-notification choice. Membership cancellation mail applies only to an ordered recurring membership.

Each email is written in the language of what it is about: the order’s for order, fulfillment, booking and return emails, the first order’s for renewal and membership emails, the language an offer was sent in, the submission’s for form emails, the request’s for the sign-in code and the email-change notice, the sender’s choice for a company invite, and each alert recipient’s own. Without text in that language the email is recorded not_sent; nothing substitutes another language.

Forms own their separate FormEmails: received, stage_changed keyed by stage ID, and alert. Each selected template has family form; receipt and stage recipients come from the submitted typed Email answer, without proving or changing a Customer. Alert recipients are { type: "store_role", store_role_id, language } or { type: "email", email, language }.

For template tests, retain one outgoing_email_id for retries, name language, and choose a ready sending address. Preview and test use the actual family data contract: each family’s sample has exactly the shape its producers send, and there are no email-only views and no storefront links Arky would invent. Offer wording reads the note at cart.offer.status.note and the expiry at cart.offer.status.expires_at, and builds its cart link from the cart’s id; receipts carry the order’s order_token. Order data lists each applied promotion with its current blocks at order.promotions. Foreign or unavailable template paths are refused. Inside {{#each list}} wording reads only this, this.<field of the item>, @index and @key.

Some fields belong to one email of a family only: order_token to the receipt, order.forms to the new-order alert, stage to a form’s stage email and customer_group_member.renewal to the renewal reminder. Selecting wording for an email, in the email settings or a form, is refused with 409 EMAIL_TEMPLATE.FIELD_NOT_SENT while it shows a field that email doesn’t carry outside an {{#if field}} on that field; editing selected wording is checked the same way. format_money and format_datetime show nothing for a missing value. Wording that still can’t be built from an email’s data, such as a subject that comes out empty, records that email not_sent with render_failed: <reason>; the action that sent it goes on. A code request whose sign_in_code wording can’t be built is refused instead. A template that the email settings, a form, a support flow or a conversation still running its flow names can’t be deleted. See delivery history for factual sent, not-sent and uncertain outcomes.