Partner Settings & Custom Domains¶
Partner settings are the self-service view of your DailyStak account: branding colors and logos, support contact details, your billing address, and the legacy webhook URL. The same settings family also manages a custom domain for your branded checkout and storefront pages, and exposes the shipping methods configured for your account.
Everything here lives under /v2/partners/settings (plus GET /v2/shipping-methods). There is no partner ID in any path — every request is scoped to the account behind your token. Reads require the order/v2.read scope; PATCH, POST, and DELETE additionally require order/v2.write. v1 API keys cannot call these endpoints — mint a v2 key in the partner portal.
Reading your settings¶
GET /v2/partners/settings returns your current settings:
curl https://api.fulfillment.sandbox.juniperhealth.com/v2/partners/settings \
-H "Authorization: Bearer $TOKEN"
{
"id": "2NxK7vPqYtR5mJnLkH3wZcB",
"name": "Acme Health",
"description": null,
"discount": "0.00",
"discountValue": 0.0,
"supportPhone": "555-123-4567",
"supportEmail": "support@acmehealth.com",
"website": "https://acmehealth.com",
"logo": null,
"logoPrimary": "logos/2NxK7vPq/primary.png",
"logoIcon": null,
"primaryColor": "#1a73e8",
"secondaryColor": "#ffffff",
"webhookUrl": "https://hooks.acmehealth.com/juniper",
"billingAddressLine1": "123 Main St",
"billingAddressLine2": null,
"billingCity": "San Francisco",
"billingState": "CA",
"billingPostalCode": "94102",
"billingCountry": "US",
"createdAt": "2026-01-10T18:22:31Z",
"updatedAt": "2026-07-20T09:14:02Z"
}
A few fields are read-only through the API: id, name, discount, and discountValue are managed by DailyStak (or in the portal) and cannot be changed here. description is a legacy field that is always null in v2, and logo is a deprecated field that is likewise always null in responses — use logoPrimary and logoIcon instead.
The webhook signing secret is deliberately not part of this response; retrieve it via its own endpoint (below).
Updating settings¶
PATCH /v2/partners/settings is a partial update with merge semantics:
- Only the fields you send are changed. Everything else keeps its current value.
nullvalues are ignored. You cannot clear a field by sendingnull— anullis treated the same as omitting the field. To clear a stored value, send an empty string"".- Unknown fields are silently ignored. Misspelled field names do not error; check the response to confirm your change landed.
- The response is the full settings object (same shape as
GET), reflecting the update.
curl -X PATCH https://api.fulfillment.sandbox.juniperhealth.com/v2/partners/settings \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"primaryColor": "#123456",
"supportEmail": "help@acmehealth.com",
"billingAddressLine2": ""
}'
What you can update:
| Group | Fields |
|---|---|
| Branding | primaryColor, secondaryColor, logoPrimary, logoIcon |
| Support contact | supportPhone, supportEmail, website |
| Billing address | billingAddressLine1, billingAddressLine2, billingCity, billingState, billingPostalCode, billingCountry |
| Webhooks (legacy) | webhookUrl |
Almost all of these are free-form strings — the API does not validate color formats, email addresses, or page URLs. What you send is stored and served back as-is (to your storefront and checkout pages, among other things), so validate on your side before writing. The one exception is webhookUrl: when set to a non-empty value it must be https:// and resolve to a public host — loopback, private-range, and link-local addresses are rejected with 422. An empty string still clears it.
Logos are storage keys, not URLs. logoPrimary and logoIcon hold the storage keys assigned when you upload logo images in the partner portal; the v2 API has no upload endpoint and does not convert keys to download URLs. In practice, manage logos in the portal and treat these fields as read-mostly.
Email toggles are portal-managed. Notification switches such as order-confirmation, subscription-payment, and change-request emails are configured in the partner portal, not through this API. They never appear in the settings response, and sending fields like sendSubscriptionPaymentEmails in a PATCH is silently ignored.
The legacy webhook URL and its secret¶
webhookUrl is the legacy notification channel: once set, DailyStak POSTs a payload of the form {"order": {...}} to that URL on every order change — no event envelope, no event types, no filtering. New integrations should use managed webhook endpoints instead (created in the partner portal), which deliver typed, enveloped events with per-endpoint secrets. See Webhooks for the event catalog and signature verification.
If you do use the legacy channel:
- Setting
webhookUrlfor the first time auto-generates a signing secret (64 hex characters). Setting the URL again later does not rotate the existing secret. - Retrieve the secret with
GET /v2/partners/settings/webhook-secret:
{ "webhookSecret": "9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e" }
If no secret exists yet you get 404 with {"message": "No webhook secret configured. Set a webhookUrl first."}.
- Rotate it with POST /v2/partners/settings/rotate-webhook-secret (no request body; returns the same shape with the new secret). The old secret becomes invalid immediately — there is no dual-validity window, so deliveries signed moments before you rotate may fail your verification. Update your verifier promptly after rotating. Rotation also works before any webhookUrl is set, which is a valid way to pre-provision a secret.
- Deliveries are signed with the X-Juniper-Signature: t={timestamp},v1={signature} header — an HMAC-SHA256 of "{timestamp}.{raw body}" using this secret. Verification details are in Webhooks. Note that managed webhook endpoints each have their own secret; the partner-level secret here signs only legacy webhookUrl deliveries.
- To stop legacy deliveries, clear the URL by sending {"webhookUrl": ""} — pending retries stop too.
Custom domains¶
A custom domain (for example checkout.acme.com) serves your branded checkout and storefront pages on a domain you own, with a TLS certificate provisioned by DailyStak. One custom domain per account.
The lifecycle: register → create every record in dnsRecords.records → verify → ACTIVE.
How many records you create depends on the domain's shape. A subdomain (checkout.acme.com) needs ONE. A root domain (acme.com — and www.acme.com, which is the same registration) needs THREE, one of them an A record. Read dnsRecords.records rather than assuming a fixed pair.
1. Register the domain¶
curl -X POST https://api.fulfillment.sandbox.juniperhealth.com/v2/partners/settings/custom-domain \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"domainName": "checkout.acme.com"}'
The domain name is lowercased and trimmed, must be a real hostname of at least two labels (domain.tld), 4–253 characters, and may not be a DailyStak platform domain (juniperhealth.com / juniper.health or any subdomain of them). Registration requests a TLS certificate for the domain and returns as soon as AWS accepts the order.
Response 201 for a subdomain — one record, and no certificate-validation record ever, because a subdomain's certificate is validated over HTTP:
{
"id": "2vGxAbCdEfGhIjKlMnOpQrS",
"domainName": "checkout.acme.com",
"status": "PENDING",
"dnsRecords": {
"records": [
{
"type": "CNAME",
"name": "checkout.acme.com",
"zone": "acme.com",
"values": ["d1234abcd5678.cloudfront.net"],
"purpose": "storefront"
}
],
"validation": null,
"routing": {
"type": "CNAME",
"name": "checkout.acme.com",
"value": "d1234abcd5678.cloudfront.net"
}
},
"validatedAt": null,
"createdAt": "2026-07-25T14:03:11.482913+00:00"
}
Response 201 for a root domain ({"domainName": "acme.com"}) — three records, here in the common case where the validation record was already published:
{
"id": "2vGxAbCdEfGhIjKlMnOpQrS",
"domainName": "acme.com",
"status": "PENDING",
"dnsRecords": {
"records": [
{
"type": "CNAME",
"name": "_3f9a1b2c.acme.com",
"zone": "acme.com",
"values": ["_8d7e6f5a.acm-validations.aws"],
"purpose": "certificate-validation"
},
{
"type": "CNAME",
"name": "www.acme.com",
"zone": "acme.com",
"values": ["d1234abcd5678.cloudfront.net"],
"purpose": "storefront"
},
{
"type": "A",
"name": "acme.com",
"zone": "acme.com",
"values": ["75.2.1.1", "99.83.1.1"],
"purpose": "apex-redirect"
}
],
"validation": null,
"routing": null
},
"validatedAt": null,
"createdAt": "2026-07-25T14:03:11.482913+00:00"
}
validation and routing are legacy fields kept for clients written against the old two-slot response. routing is populated for subdomain registrations only; both are always null for a root domain, because a slot with one value cannot express an A record with two addresses. Read records.
dnsRecords may be null, or may arrive without its certificate-validation record, on a successful 201. The certificate authority publishes that record a few seconds after accepting the order, and registration no longer waits for it. The domain IS registered — do not POST again, which would return 409 ("already has a custom domain"). Re-GET the domain instead; the record appears on the first GET made after the CA publishes it, usually within seconds:
curl https://api.fulfillment.sandbox.juniperhealth.com/v2/partners/settings/custom-domain \
-H "Authorization: Bearer $TOKEN"
If it is still missing after a few minutes, that is a defect on our side, not something you can fix with DNS — open a support ticket rather than deleting and re-registering.
Root domains are not available in every environment. Serving one needs the redirect tier that answers its A record. Where that tier is not deployed, registering a root domain is refused with a 503 naming a subdomain to use instead, and nothing is created — no certificate and no reservation of the name — so you can register the subdomain immediately. The 503 is not transient: the tier is per-environment configuration, so retrying the same root domain will not start working on its own.
2. Create the DNS records¶
Create every entry in dnsRecords.records, in the order they are listed. For a root domain that puts www in place before the root starts redirecting to it, so your site is never briefly pointing at a name that does not resolve.
purpose |
What it does |
|---|---|
certificate-validation |
Proves you own the domain so the TLS certificate can be issued. Root-domain registrations only. Create it within 72 hours — after that the certificate request expires and verification can never succeed (see the recovery note below). |
storefront |
Points the served host at the DailyStak platform so traffic reaches your pages. For a subdomain that is the name you registered; for a root domain it is www.<your-domain>, the canonical host your site is served from. Its value is known from the moment you register. |
apex-redirect |
Root-domain registrations only: an A record at the root, pointing at our redirect tier's static addresses, which 301s to www. Create both addresses. |
Why the root is an A record and not a CNAME: RFC 1034 forbids a CNAME at a zone apex — it cannot coexist with the SOA and NS records that must exist there. Route 53 rejects the change outright, and a provider that accepts it anyway displaces your MX records and takes your email down with it. So the root points at fixed addresses and your site is served from www.
name vs zone. Route 53 and Cloudflare want the fully-qualified name. GoDaddy, Namecheap and Squarespace name fields are zone-relative — @ for the root, www for www, within the zone given by zone. Pasting the FQDN into one of those creates www.acme.com.acme.com, a real-looking record that resolves nowhere. Both forms are returned because splitting shop.acme.co.uk into host and zone needs the Public Suffix List, so we do it for you.
You can re-fetch the records at any time with GET /v2/partners/settings/custom-domain (the dnsRecords object is returned at every status, including ACTIVE). Each entry is emitted only once its value is known, so treat a missing entry as "not published yet", never as "not required".
3. Verify¶
Once the records have propagated (give DNS a few minutes to an hour), call:
curl -X POST https://api.fulfillment.sandbox.juniperhealth.com/v2/partners/settings/custom-domain/verify \
-H "Authorization: Bearer $TOKEN"
No request body. The response is the domain object without dnsRecords ("dnsRecords": null on this endpoint only). Possible outcomes:
| Result | Meaning | What to do |
|---|---|---|
200, status: "PENDING" |
The certificate isn't issued yet — your records haven't been detected | Wait a few minutes and call verify again |
200, status: "ACTIVE" |
Certificate issued; domain attached to the platform | Test your storefront on the domain (see below) |
400, status becomes FAILED |
Certificate validation failed | Terminal — DELETE the domain and register again |
404 |
No custom domain registered | Register first |
Verify is idempotent — calling it on an already-ACTIVE domain returns 200 / ACTIVE.
Verify checks only the certificate. It confirms the TLS certificate was issued; it never checks the storefront or apex-redirect records. A domain can therefore reach ACTIVE while the site is still unreachable because one of those is missing or wrong. After activation, always load your storefront on the custom domain to confirm end-to-end — for a root domain, load both https://acme.com (which must redirect) and https://www.acme.com. Also allow a few minutes after a successful verify — attaching the domain is a global change that takes a short while to propagate.
Stuck domains. Two situations require a reset — DELETE /v2/partners/settings/custom-domain, then register again:
status: "FAILED"— terminal; repeated verifies keep returning400.- The
certificate-validationrecord was never added within the 72-hour window (root domains only — a subdomain has no such record). The certificate request expires, and verify then returns200/PENDINGindefinitely without ever progressing. If a domain has beenPENDINGfor days despite correct DNS, treat it as expired and re-register.
Removing a domain¶
DELETE /v2/partners/settings/custom-domain (no body) detaches the domain, deletes its certificate, and stops serving traffic on it. Returns 204 with an empty body, or 404 if no domain is configured. If the call fails with a 500, the teardown may be partial — simply retry the DELETE. After deletion you can register a new domain (including the same name again).
Domain statuses¶
| Status | Meaning |
|---|---|
PENDING |
Registered; waiting for you to add DNS records and verify |
ACTIVE |
Certificate issued and domain attached; validatedAt is set |
FAILED |
Certificate validation failed — delete and re-register |
Shipping methods¶
GET /v2/shipping-methods lists the shipping methods active on your account:
curl https://api.fulfillment.sandbox.juniperhealth.com/v2/shipping-methods \
-H "Authorization: Bearer $TOKEN"
{
"shippingMethods": [
{
"name": "STANDARD",
"description": "5-8 business days",
"price": "9.99",
"createdAt": "2026-03-02T10:15:00",
"updatedAt": "2026-03-02T10:15:00"
}
]
}
- The response is a wrapper object, not a bare array;
shippingMethodsis[](never404) when nothing is configured. nameis the identifier the rest of the API validates against: theshippingMethodyou pass toPOST /v2/ordersmust match one of these names exactly, or order creation fails with400listing your available methods. See Orders.priceis a decimal serialized as a JSON string (e.g."9.99"), consistent with money elsewhere in the API.- The list is small and unpaginated, and requires only
order/v2.read(not the catalog scope). - Shipping methods are configured by DailyStak for your account — they are not writable through the API. Contact your DailyStak representative to add or change methods.
Errors¶
Bodies are {"message", "code"} — branch on code — except the gateway's own 401/403.
| Status | Where | code |
Meaning |
|---|---|---|---|
401 |
all endpoints | — (gateway) or UNAUTHORIZED |
Missing or invalid token |
403 |
all endpoints | — (gateway) | Token lacks a required scope, or the account is suspended |
404 |
settings | PARTNER_NOT_FOUND |
Account no longer exists |
404 |
webhook-secret | WEBHOOK_SECRET_NOT_CONFIGURED |
No secret configured yet (set a webhookUrl first, or rotate) |
404 |
custom-domain endpoints | DOMAIN_NOT_FOUND |
No custom domain configured |
400 |
custom-domain register | INVALID_DOMAIN |
Invalid domain name, platform domain, or transient certificate-provisioning failure — the message says which |
400 |
custom-domain verify | DOMAIN_VALIDATION_FAILED |
Validation failed |
409 |
custom-domain register | DOMAIN_ALREADY_EXISTS |
You already have a custom domain (any status) — delete it first |
409 |
custom-domain register | DOMAIN_CLAIMED |
Another account already holds this domain |
409 |
custom-domain register | APEX_CERTIFICATE_LIMIT_REACHED |
The root-domain tier has no room for another certificate; contact support |
503 |
custom-domain register | APEX_REDIRECT_NOT_CONFIGURED / APEX_REDIRECT_UNAVAILABLE |
A root domain where the apex redirect tier is not deployed (use the subdomain the message names), or the tier could not be reached (retry) |
503 |
custom-domain delete | DOMAIN_TEARDOWN_INCOMPLETE |
Part of the teardown did not complete; retry the DELETE |
422 |
any request body | VALIDATION_FAILED |
Body failed validation. details.errors lists each failure by type, e.g. [{"type": "string_too_short", "loc": []}]; no field location or value is sent back |
Good to know¶
PATCHcannot clear vianull.null(or omitting a field) means "leave unchanged"; an empty string""overwrites. This is the single most common surprise in this API family.- No server-side format validation on settings. A typo'd hex color or malformed support email is stored verbatim and rendered wherever your branding appears.
- Read-after-write is briefly eventual. In rare cases the settings object returned by
PATCH(or an immediateGET) may not yet reflect the write. Re-read a moment later if you see stale data. - Timestamp formats vary slightly. Most timestamps are UTC with a
Zsuffix, but custom-domaincreatedAt/validatedAtuse a+00:00offset (with microseconds), and shipping-method timestamps may carry no timezone suffix. Parse all of them as ISO-8601 without assuming theZform. logoPrimary/logoIconround-trip as raw storage keys, not fetchable URLs — don't put them directly in an<img src>.- One domain per account, ever-present
409. Even aFAILEDor never-verified domain blocks registration of a new one until youDELETEit. - The legacy
webhookUrlis all-or-nothing. Every order change is delivered, unfiltered. For typed events, retries you can reason about, and per-endpoint secrets, use managed webhook endpoints — see Webhooks.