Skip to main content
Live with caveats. This area is real and usable, but the docs intentionally call out operational or UX limits that still matter.

Goal

Give every merchant on your platform their own domain and email identity, then run their email marketing through the Mail API without handling DNS or sender setup yourself.

Prerequisites

  • A partner account. StackShift staff turn on partner access for your account; contact StackShift support if the partner API answers 403 with not_a_partner.
  • Enough wallet balance to cover the domain price. Add funds in Billing → Wallet. Set currency to NGN or USD on the purchase to choose which wallet pays. If you leave it out, StackShift charges the first wallet, Naira and then US dollars, that covers the price.
  • A live API token created in Settings → Security with the partner:manage scope, plus mail:manage to create audiences and campaigns and to subscribe to webhooks. Starting a transfer-out needs the separate partner:transfer scope, so give it only to a token that should be able to release domains; partner:manage alone is refused. Tokens with the * scope have both. Test credentials cannot use the partner API.
  • Each merchant’s contact details (the registrant) and postal address. The registrant becomes the owner of record for the domain, and the postal address appears in the footer of marketing email.
  • An HTTPS endpoint on your platform that can receive webhooks.

Workflow

1
Search for the domain the merchant wants with the public domain search, availability check and pricing endpoints. They need no API token.
2
Buy the domain with POST /api/v1/partner/domains and a unique Idempotency-Key. Save the id in the response next to your merchant record.
3
Create a webhook with POST /api/v1/mail/webhooks for the partner events you care about, at least partner.brand.ready and partner.domain.failed.
4
Wait for partner.brand.ready, or read the domain with GET /api/v1/partner/domains/{id} until its state is ready. Ready means setup is finished; marketing still unlocks separately, and warmup.marketingUnlocked in the same response tells you when campaigns can start.
5
From day one, send the merchant’s order and account email from the transactional sender returned as senders.transactional. After 7 clean days of transactional mail, marketing unlocks.
6
Create an audience on the merchant’s brandId and send campaigns with the existing Mail audience and campaign APIs.
7
Watch partner.brand.warning and partner.brand.paused so you can tell merchants when their list quality needs attention, then resume the brand once the rates have recovered.
8
Read GET /api/v1/partner/domains?attention=true to see only the merchants that need you, with what is wrong and the one thing to do about it.

What StackShift sets up for each merchant

A partner platform is one StackShift account that StackShift staff have enabled for partner access. When you buy a domain for a merchant, StackShift charges your wallet, registers the domain with the merchant as registrant, publishes the DNS records, adds the domain to StackShift Mail and verifies it. It then creates a Mail brand for the merchant, tagged with your merchantRef. Marketing is not available on day one: it unlocks after 7 clean days of transactional mail on the domain, and then a warm-up schedule raises the daily volume gradually.
  • Marketing email is sent from hello@news.merchantshop.com and transactional email from an address on notify.merchantshop.com, which the domain record returns as senders.transactional. Keeping the two on separate subdomains means a marketing problem does not affect receipts and password resets.
  • Replies to hello@merchantshop.com are forwarded to the merchant’s existing inbox (mode forward) or delivered to a hosted StackShift mailbox (mode mailbox). Change the name before the @ with fromLocalPart.
  • Setup moves through registering, mail_setup, verifying, replies and brand_setup, and ends in ready. A step that cannot complete after repeated attempts moves the domain to failed, and lastError says why. If a registration reports failure but the domain was in fact registered, setup continues instead of refunding you. Purchases still waiting for payment or registration after 6 hours are stopped and refunded or released.
  • If StackShift suspends your partner account, setup of your merchant domains pauses after registration until the account is reactivated.

Search before you buy

Use the same public endpoints as the dashboard. Search returns suggestions with Naira and US dollar prices, check confirms one domain, and pricing lists every extension with its sale_price_ngn and sale_price_usd.
Search and check a domain

Buy a domain for a merchant

Send the purchase with an Idempotency-Key header of at most 200 characters. A new purchase answers 202 Accepted with the merchant domain in the registering state. Repeating the same key with the same request body answers 200 with the existing record, so it is safe to retry after a timeout. The same key with a different body answers 409 idempotency_conflict. Each partner can start 30 purchases per minute and a number of domains per rolling 24 hours that StackShift sets, 25 by default and between 0 and 1000, shown as dailyPurchaseLimit on GET /api/v1/partner.
  • domain: the full name, such as merchantshop.com. years: 1 to 10, default 1. merchantRef: your own identifier for the merchant, at most 200 characters.
  • currency (optional): NGN or USD. The domain is charged at its listed price in that currency, and renewals are charged in the same currency. When it is left out, the first wallet that covers the price pays, Naira before US dollars. Only currencies the extension is priced in are used.
  • registrant: first_name, last_name, email, phone, address, city, state, postal_code and a 2-letter country code. organization is optional.
  • brand: name and postalAddress are required. fromLocalPart defaults to hello. website must be an HTTPS address. supportEmail is optional.
  • replies: mode forward uses forwardTo, which defaults to the registrant email and must be outside the new domain. Mode mailbox uses mailboxOwnerEmail, which defaults to the registrant email, and an optional mailboxPlan. The hosted mailbox is paid from your NGN wallet or, when NGN does not cover it, from your USD wallet at the order’s Naira price converted at the StackShift rate and rounded up to the cent. A renewal switches to USD only when your USD wallet covers the full amount.
Purchase request
Purchase with the JavaScript SDK
Purchase with the Python SDK

Follow setup with webhooks

Subscribe with the Mail webhooks API. Every partner event carries partnerDomainId, merchantRef, domain and state, plus brandId once the brand exists, so you can match it to your merchant record. These keys are always present and are never replaced by other fields. An event keeps the same id when a delivery is retried, and the delivery id is stable per event and endpoint, so use the id to ignore duplicates. A partner event that cannot be published after 20 attempts is set aside for StackShift staff. Verify the signature on each delivery the same way as for other Mail events.
  • partner.domain.registered: the domain was registered. The payload adds expiryDate and years.
  • partner.domain.dns_verified: sending and receiving DNS was verified.
  • partner.brand.ready: the brand is ready to send. Start campaigns from here.
  • partner.domain.failed: a step failed. The payload adds step, reason and refunded, which tells you whether the wallet charge was returned.
  • partner.warmup.progress: warm-up reached a new day. The payload adds day, totalDays, dailyLimit and marketingDailyLimit.
  • partner.brand.warning: bounces or complaints reached half of the pause limit. partner.brand.paused: a limit was reached and marketing is paused. partner.brand.resumed: marketing was resumed. All three add bounceRate, complaintRate, recipients, last24h, bounceLimit and complaintLimit; paused also adds reason, complaint_rate or bounce_rate.
  • partner.brand.bounced and partner.brand.complained: an individual message from the merchant’s domain bounced or drew a complaint.
  • partner.domain.renewal_due, partner.domain.renewed and partner.domain.renewal_failed: renewal progress, sent within about an hour of the change. renewal_due adds expiryDate and autoRenew. renewed adds expiryDate and previousExpiryDate. renewal_failed adds expiryDate, reason (plain text you can show), amountMinor and currency. Renewals are charged to your wallet in the currency the domain was bought in, so keep that wallet funded.
Subscribe to partner events

Send email for the merchant

The partner API stops at the brand. Everything after that uses the Mail API you already know. Read the domain with GET /api/v1/partner/domains/{id} to get brandId and the senders object with the marketing, transactional and reply-to addresses. Create audiences with that brandId, import consented contacts and send campaigns as described in the Mail guides. Send from the transactional sender on day one: the merchant’s order confirmations, receipts and account email. After 7 clean days of transactional mail on the domain, marketing unlocks and the warm-up schedule limits how much you can send each day. The first campaign is capped at 1,000 recipients, and a larger list on a domain that is still warming up waits for review. Read warmup on GET /api/v1/partner/domains/{id} to see where the domain stands: marketingUnlocked turns true when campaigns can start, cleanTransactionalDays counts toward requiredCleanDays, firstCampaignCap is the first campaign’s recipient limit, and dailyLimit and marketingDailyLimit are today’s allowances. The brand also shows sendingPausedAt and sendingPauseReason while it is paused, suspendedAt while StackShift has suspended your partner account, and merchantRef so you can find a merchant’s brand in your own Mail brand list.
Create an audience for a merchant brand

Protect sending reputation

StackShift checks each ready brand over two windows, the last 24 hours and the last 7 days, once at least 200 recipients have been attempted. By default it pauses marketing when complaints reach 0.3% or bounces reach 5% in either window. You receive partner.brand.warning at half of either limit. The bounce rate is bounced divided by attempted recipients, and the complaint rate is complaints divided by accepted recipients. Each merchant’s sending domain family can use at most the larger of 25% and an equal share of your account’s daily and hourly sending limits, so a single merchant cannot use up the allowance of the others once you have several. One merchant can use all of it, two can use half each, and from four merchants each is capped at 25%. Your own domains that are not merchant domains are not capped. StackShift can change the 25% and raise your account’s limits. Lower the limits for your whole platform with PUT /api/v1/partner/settings and complaintPauseRate and bouncePauseRate as fractions, such as 0.002 and 0.04. The values cannot be higher than the defaults. A paused brand blocks marketing only. Transactional email keeps flowing. While a brand’s sending is paused or suspended, you cannot change its sending domain or merchantRef or archive it; the Mail brands API answers 400 brand_sending_locked until sending is resumed. A new send, batch, scheduled send or automation from a paused or suspended sender is refused with brand_sending_paused or sender_suspended. Mail that was already accepted when the pause landed, such as running campaigns and automations, scheduled sends coming due and batches created earlier, is held, shown as delayed with code sender_paused once, and goes out automatically when sending resumes. If sending stays paused for 7 days, held messages fail with code sender_paused and a running campaign is stopped, with status failed and a mail.campaign.stopped event. A suspension blocks every send from the merchant’s domain family; a pause blocks the brand’s marketing domain. Reactivating a suspended partner restores sending and auto-renew and keeps any earlier bounce or complaint pause. When the merchant has cleaned their list (see Clean a list after a pause), call POST /api/v1/partner/brands/{brandId}/resume. It answers 409 brand_rates_still_high while the rates are still over the limit and 409 brand_not_paused when the brand is not paused. GET /api/v1/partner/domains/{id} returns the brand status, health, the delivery numbers of the marketing (news.) domain for the last 7 days with the last 24 hours under health.last24h, which are the numbers used for pauses, transactionalHealth, the same shape for the merchant’s root domain and notify. (order and account mail) combined, and a warmup object.

See which merchants need you

GET /api/v1/partner/domains answers in one call for a merchant dashboard. Search with q, which matches the domain, the merchant reference and the brand name and can be up to 253 characters, and add attention=true to list only the merchants that need you. A merchant needs you when its brand is paused or suspended, when a setup failed and was not refunded, or when a running step has an error or is waiting for wallet funds. total counts the list you asked for, and attentionTotal counts the merchants that need you whatever q and paging say, so you can show the number without a second call.
  • failure says what is stuck, in plain language: a code (nameservers_changed, dns_not_visible, wallet_short, registry_taken, registration_rejected, registration_timed_out, purchase_interrupted, mailbox_unavailable, mail_setup_rejected, dns_provider_error, registration_mismatch, sending_domains_missing or internal_retrying), a short headline and an explanation for a list, a title and facts for a detail page, since for when it started, canRetry for whether retry is allowed, and fix for the one thing you can do now. A fix is either an action StackShift performs for you, restore_nameservers, or a link such as the page to top up your wallet. It is null when nothing is stuck.
  • diagnosis is Stackie’s read of a merchant that needs attention, with checkedAt and a summary, refreshed by an hourly check and cleared once the merchant is fine. GET /api/v1/partner/domains/{id}/diagnosis returns the same object, or 404 no_diagnosis.
  • waitingFor says which wallet a step is waiting on and how much it needs: kind wallet, currency, requiredMinor and availableMinor. lastAttemptAt is when StackShift last tried the step. refunded is true when the registration was returned to your wallet.
  • sentByDay lists the messages accepted for the merchant on each of the last 7 days, oldest first, ending today, for a small chart. warmup gives status (new, warming or trusted), day, totalDays, marketingUnlocked and marketingOpensInDays, the clean days still needed before marketing unlocks.
  • pause is set while marketing is paused: the reason (complaint_rate or bounce_rate), the rate and the limit, the window (last_7_days or last_24_hours), the campaign and contact list behind most of it when that can be worked out, and recoversAt, the time the rates should fall under the limits. recoversAt is null when they are already under, which means you can resume now.
  • GET /api/v1/partner/domains/{id} returns all of this plus records, the DNS records StackShift publishes with whether each is verified and what it is for (dkim, spf, return_path, dmarc, reply_mx, reply_ownership, mta_sts, mta_sts_policy or tls_reporting), refund with the amount and when it was returned, webhookDeliveredAt for when your platform received the partner.domain.failed event, replies.mailboxOwnerName for a hosted mailbox, and warmup.marketingSentToday and warmup.marketingOpensInDays.
List the merchants that need you

Fix a stuck merchant

  • When failure.fix.action is restore_nameservers, the merchant changed the domain’s nameservers away from StackShift. POST /api/v1/partner/domains/{id}/restore-dns points them back and returns the merchant domain with its refreshed failure. It answers 409 dns_restore_not_allowed for a domain that is not registered with StackShift or has been transferred out, and 409 dns_restore_not_needed unless the failure code is nameservers_changed, so a domain the merchant moved on purpose is never pointed back.
  • When failure.canRetry is true, POST /api/v1/partner/domains/{id}/retry runs the step again. A setup that is still running can be retried when its current step has an error or was tried before, and it answers 409 setup_busy while StackShift is working on it.
  • When failure.fix is a link, such as topping up the wallet, the setup continues on its own once the cause is fixed.
Point a merchant's nameservers back to StackShift

Clean a list after a pause

A pause says which contact list drove it. To see where the complaints came from, read the brand’s complaints: GET /api/v1/partner/brands/{brandId}/complaints returns the complaints of the last 7 days and how many addresses are suppressed, and GET /api/v1/partner/brands/{brandId}/complaints/sources lists the contact lists with the most complaints first, each with its contacts, how many never opened or clicked any marketing (neverOpened) and how many complained. POST /api/v1/partner/brands/{brandId}/suppressions/bulk with audienceId and reason never_opened unsubscribes the contacts in that list who never opened or clicked, so the brand’s campaigns stop reaching them. Nothing is added to the suppression list, so order receipts, one-time codes and other merchants still reach them. It answers 201 with an id, and POST /api/v1/partner/brands/{brandId}/suppressions/bulk/{bulkId}/undo puts them back on the list as they were and returns how many came back; contacts who changed their own status since are left as they are. Both are written to the audit log. To see the complaint suppressions themselves, call GET /api/v1/mail/suppressions with brandId and reason=complaint.
Stop emailing contacts who never opened

Ask StackShift for more

POST /api/v1/partner/requests asks for a higher domain purchase limit (kind purchase_limit) or for your own sending addresses (kind dedicated_addresses), with an optional message of up to 2,000 characters. StackShift staff answer it and mark it done or declined. GET /api/v1/partner/requests lists your requests, newest first, with their status. You can have one open request of each kind at a time: a second one answers 409 request_already_open. Requests are limited to 5 per minute.

Transfers, retries and usage

  • POST /api/v1/partner/domains/{id}/transfer-out unlocks the domain, turns off auto-renew and returns the authCode the merchant gives their new registrar. It needs the partner:transfer scope (or *); partner:manage alone is refused. StackShift records transferredOutAt on the merchant domain, writes an audit entry and emails your account owner. Do this only when the merchant is leaving.
  • POST /api/v1/partner/domains/{id}/retry restarts a failed setup step after registration. It answers 409 retry_not_allowed for a registration that failed and was refunded. A registration that reports failure but did complete continues setup and is not refunded.
  • GET /api/v1/partner/domains lists your merchant domains and accepts merchantRef, q, attention, limit and offset. Each item includes expiryDate, brandPausedAt, brandPauseReason (complaint_rate, bounce_rate or empty) and health and transactionalHealth for the brand, plus the fields described in See which merchants need you, so one call is enough for a merchant dashboard.
  • GET /api/v1/partner/usage returns, for each merchant, the brand name, marketing and transactional recipients and domain charges in the period. Pass from and to as RFC 3339 timestamps. The default is the current month so far, and a period can span at most one year. An invalid period answers 400 invalid_period. domainChargesMinor does not count charges that were refunded, and refundedMinor is what was returned to your wallet, so a merchant with 0 charged and a refund above 0 was refunded.
  • GET /api/v1/partner gives limits with the numbers behind each merchant’s share of your sending: merchantSharePercent, merchantCount, dailySendingLimit and hourlySendingLimit, so a merchant can send dailySendingLimit * merchantSharePercent / 100 a day. warningFraction is the share of a pause limit at which a brand warns, 0.5 for half.
  • The same operations are available from the CLI under stackshift partner, for example stackshift partner domain-purchase with —data and an optional —idempotency-key, stackshift partner domain-diagnosis, stackshift partner domain-restore-dns, stackshift partner brand-complaints, stackshift partner brand-suppress-never-opened and stackshift partner requests.

Expected result

Each merchant domain reaches the ready state. Its Mail brand carries your merchant reference, transactional email goes out from an address on notify.merchantshop.com straight away, marketing email goes out from hello@news.merchantshop.com once it unlocks, and replies sent to hello@merchantshop.com reach the merchant.

Common failures

  • 402 insufficient_wallet_balance: the response details give requiredMinor, availableMinor and currency. Add funds in Billing → Wallet and send the purchase again with a new Idempotency-Key.
  • 409 domain_unavailable: someone registered the domain after your search. Search again and offer the merchant another name.
  • 409 idempotency_conflict: the Idempotency-Key was already used for a different purchase, including the same key sent with a different body. Use a new key for each purchase and reuse a key only to retry the exact same request.
  • 400 validation errors such as invalid_domain, invalid_years, invalid_merchant_ref, invalid_currency, idempotency_key_required, invalid_registrant, invalid_brand, postal_address_required, invalid_from_local_part, invalid_brand_website, invalid_brand_email, invalid_forward_to, invalid_mailbox_owner and invalid_replies: the message names the field to fix. Nothing was charged.
  • 400 tld_not_sold: StackShift does not sell that domain extension. Search for a supported one.
  • 400 currency_unavailable: the extension has no price in the currency you chose. Pick the other currency or leave currency out.
  • 429 daily_purchase_limit: your account has reached its purchase cap for the rolling 24 hours, 25 domains by default. Wait for the window to pass or ask StackShift to raise it. Beyond 30 purchases per minute you get a 429 with X-RateLimit headers instead; slow down and retry.
  • 503 wallet_unavailable or replies_unavailable: wallet payments or reply handling are not available right now. Nothing was charged. Try again shortly.
  • The domain stays in verifying: DNS for a StackShift-registered domain is written for you, so this usually clears within minutes. If it ends in failed with a verification message, call POST /api/v1/partner/domains/{id}/retry.
  • Retry answers 409 retry_not_allowed: a failed registration was refunded to your wallet, or never charged, and cannot be retried. Buy again with a new Idempotency-Key once you have fixed the cause. A registration can be retried only when it actually completed, and a registration that stalls for more than 6 hours is stopped and refunded. A setup that is still running can be retried only when its current step has an error or has already been tried; a ready merchant, a purchase that has not started charging and a running step with no error answer retry_not_allowed too.
  • Retry answers 409 setup_busy: StackShift is working on that merchant right now. Try again in a minute.
  • The merchant list answers 400 invalid_query: the search in q is longer than 253 characters.
  • Restore DNS answers 409 dns_restore_not_allowed: the domain is not registered with StackShift, or it has already been transferred out, so its nameservers cannot be pointed back. It answers 503 dns_restore_unavailable when this deployment cannot restore DNS, and 429 beyond 10 requests per minute.
  • GET /api/v1/partner/domains/{id}/diagnosis answers 404 no_diagnosis: Stackie has not checked that merchant yet, or it no longer needs attention. Use the failure on the domain instead.
  • Bulk suppression answers 400 invalid_suppression_reason or invalid_audience for a bad body, and 404 audience_not_found when the contact list does not belong to that merchant. Undo answers 404 bulk_suppression_not_found when the change does not exist or was already undone.
  • A request to StackShift answers 400 invalid_request_kind or request_message_too_long for a bad body, 409 request_already_open while a request of that kind is still open, and 429 beyond 5 requests per minute.
  • Transfer-out answers 403: the token has partner:manage but not partner:transfer. Use a token with partner:transfer or *. It answers 409 domain_not_registered until registration has finished, and 429 beyond 10 requests per minute.
  • A marketing campaign is refused or waits right after setup: marketing unlocks only after 7 clean days of transactional mail on the domain, and the first campaign is limited to 1,000 recipients. Send the merchant’s order and account email from senders.transactional, then try again.
  • Campaigns are rejected with brand_sending_paused: the brand crossed its bounce or complaint limit. Clean the audience, then call POST /api/v1/partner/brands/{brandId}/resume.
  • A brand is refused with 400 brand_sending_locked when you change its sendingDomainId or merchantRef, or archive it: while the brand’s sending is paused (sendingPausedAt) or suspended (suspendedAt) those changes are not allowed. Resume sending first.
  • Sending is refused with sender_suspended: StackShift suspended your partner account, which blocks every send from your merchants’ domains until it is reactivated. A health pause blocks only the brand’s marketing domain, with brand_sending_paused.
  • Mail shows as delayed with code sender_paused: it was accepted before sending was paused, so it is held and goes out automatically when sending resumes. If sending stays paused for 7 days, held messages fail with code sender_paused and a running campaign is stopped with the mail.campaign.stopped event. A new send, batch, scheduled send or automation from a paused or suspended sender is still refused right away.
  • A send is refused with merchant_share_exceeded: that merchant has used its share of your account’s sending limits. Wait for the window to pass or ask StackShift to raise your limits.
  • Resume answers 409 brand_rates_still_high: the rates over the last 24 hours or the last 7 days are still at or above the limit. Wait for them to drop, or reduce the sending that caused them, and try again.

Domain search, purchase, and portfolio

Find and acquire domains, then understand how they appear in the domain portfolio.

Transfers, email, orders, and settings

Use the broader domain-management surfaces for transfers, orders, and settings, and find where mailboxes for a domain are managed.

Sender domains and DNS

Create and verify outbound sender domains, inspect SPF, DKIM, DMARC, and return-path record status, and know what the domain status fields mean.

Streams, audiences, and campaigns

Separate transactional and broadcast traffic, retain consent evidence, and build private campaign drafts with unsubscribe protection.

Events, webhooks, and timelines

List mail events, inspect per-message timelines, subscribe webhooks, rotate secrets, retry deliveries, and verify webhook signatures.