> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dmly.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect Stripe

> Connect your agency's own Stripe account so client workspaces can subscribe and pay you online, and set up the webhook that keeps DMLY in sync.

This page is about the Stripe account **you, the agency**, connect so your client workspaces
can pay *you* for their plans. It is not the Stripe gateway a business connects under
Finance to collect invoice payments from its own customers — for that, see
[Payments & gateways](/finance/payments-and-gateways).

## What connecting Stripe unlocks

You can run an agency without Stripe: you assign plans to clients by hand from
**Sub-accounts**, grant add-ons, and collect payment however you like outside DMLY. What
Stripe adds is the self-serve loop — clients subscribing and paying you online, without you
touching anything.

<Columns cols={2}>
  <Card title="Without Stripe" icon="hand">
    Your clients' **Plans & billing** page shows your plans, but every button is disabled
    and the page reads **Online billing isn't available yet.** with your support email. You
    assign plans manually from **Sub-accounts** and handle money outside DMLY.
  </Card>

  <Card title="With Stripe" icon="bolt">
    **Choose plan** goes live. A client picks a cycle, lands on a Stripe-hosted checkout
    against **your** Stripe account, and pays you directly. DMLY never sees the money and
    takes no cut.
  </Card>
</Columns>

Two things stay the same either way: add-ons for client workspaces are always granted by
you from **Sub-accounts** — clients can't buy them at checkout — and charges, refunds,
disputes, and payouts live in your Stripe dashboard, not in DMLY.

Connecting Stripe has two halves, and **both matter**. The keys let DMLY start a checkout;
the webhook is how Stripe tells DMLY the payment happened. Skip the webhook and clients can
pay you while DMLY never hears about it.

## Enter your keys in DMLY

<Steps>
  <Step title="Get your API keys from Stripe">
    In your Stripe Dashboard, find your API keys under the developers section. You need two
    values: the **secret key** (starts `sk_live_`) and the **publishable key** (starts
    `pk_live_`).
  </Step>

  <Step title="Open the Stripe panel in your agency settings">
    In your agency dashboard, go to **Configuration → Settings** and find the
    **Stripe (charge your customers)** panel. The badge next to the heading reads
    **Not connected** until a secret key is saved.
  </Step>

  <Step title="Paste the keys and save">
    Fill in **Secret key** and **Publishable key**, then select **Save Stripe keys**. You
    can leave **Webhook signing secret** empty for now — you'll get that value from Stripe
    in the next section and come back.

    On success you see *Stripe keys saved. Your customers will be billed through your own
    Stripe account.* and the badge switches to **Connected**.
  </Step>
</Steps>

Your keys are stored encrypted. The **Secret key** and **Webhook signing secret** are never
shown back to you — those fields always render empty — and only the publishable key is
prefilled on your next visit.

<Warning>
  DMLY does not test your keys when you save them. A mistyped secret key saves fine and the
  badge still says **Connected** — the error only surfaces later, when a client tries to
  check out. Copy the keys carefully, and run the end-to-end test below before pointing a
  real client at the page.
</Warning>

## Create the webhook endpoint at Stripe

After a client pays, Stripe reports the subscription back to DMLY over a webhook. Without
it, DMLY records nothing — no subscription, no receipts — even though the charge went
through.

<Steps>
  <Step title="Copy your webhook URL">
    Your webhook URL is shown in the same Stripe panel, under **Your webhook endpoint** —
    copy it from there. It looks like:

    ```text theme={"dark"}
    https://dash.dmly.io/agency/<your-agency-id>/stripe/payment-webhook
    ```

    Use the URL exactly as shown. It's built on DMLY's own host on purpose, so it keeps
    working even if you later change your agency's custom domain.
  </Step>

  <Step title="Add the endpoint in Stripe">
    In your Stripe Dashboard, create a new webhook endpoint (under the developers /
    webhooks section) and paste the URL above as the destination.
  </Step>

  <Step title="Select exactly these event types">
    When Stripe asks which events to send, select these seven — this is the part that most
    often goes wrong:

    * `invoice.payment_succeeded` — records a payment receipt in DMLY
    * `customer.subscription.created` — records the new subscription
    * `customer.subscription.updated` — keeps its status, price, and trial in sync
    * `customer.subscription.deleted` — marks the subscription cancelled
    * `customer.updated` — refreshes the stored payment method
    * `customer.deleted` — clears the customer link from the workspace
    * `payment_method.automatically_updated` — keeps card details current

    Don't select "all events" instead: DMLY quietly accepts event types it doesn't handle,
    so every delivery looks successful in Stripe while the noise buries the ones that
    matter.
  </Step>

  <Step title="Copy the signing secret back into DMLY">
    Stripe shows a **signing secret** for the new endpoint, starting `whsec_`. Copy it,
    return to **Configuration → Settings → Stripe (charge your customers)**, paste it into
    **Webhook signing secret**, re-enter your **Secret key**, and select **Save Stripe
    keys** again.
  </Step>
</Steps>

<Warning>
  Saving the Stripe panel replaces **all three values at once**. The **Secret key** and
  **Webhook signing secret** fields always start empty, so whenever you save — to rotate a
  key, to fix a typo — fill in *both* again. Saving with a blank **Webhook signing secret**
  silently erases the stored one, and from that moment every webhook delivery is rejected
  until you paste it back.
</Warning>

## Give your plans Stripe prices

A plan can only be bought online when its billing cycle carries a Stripe price. In Stripe,
create a recurring price for each plan and cycle you sell; then, on your agency **Plans**
page, paste each price's ID (starts `price_`) into **Monthly Stripe price ID** or
**Yearly Stripe price ID** on the matching plan. A cycle without both a price ID and an
amount shows a disabled **Unavailable** button on your clients' billing page.

See [Price and bill your clients](/agency/plans-and-billing) for building plans and setting
their limits.

## Test it end to end

Prove the loop before a real client pays.

<Steps>
  <Step title="Ping the webhook from Stripe">
    Most Stripe webhook tooling lets you send a test event to an endpoint. Send one — any
    of the seven types — and check the response DMLY returns:

    * **2xx** — the URL and signing secret are both right. You're wired up.
    * **403 Invalid webhook signature** — the signing secret saved in DMLY doesn't match
      this endpoint (or was never saved). Re-copy it and re-save.
    * **404 Unknown agency** — the agency ID in the URL is wrong.
    * **422 Agency Stripe is not configured** — no secret key is saved in DMLY yet.
  </Step>

  <Step title="Run a real checkout">
    From a client workspace (a test sub-account works well — see
    [Client workspaces](/agency/client-workspaces)), open **Plans & billing** and select
    **Choose plan** on a priced plan. You should land on a Stripe-hosted checkout page
    showing your plan's price. Complete the payment.
  </Step>

  <Step title="Confirm the deliveries">
    In your Stripe dashboard: the subscription exists, and each webhook delivery for it
    shows a 2xx response with the body **Webhook Handled** — that body means DMLY
    processed the event. An empty response body is still a 2xx, but it means the event
    type isn't one DMLY handles. DMLY records the payment internally but doesn't
    currently display it to you, so the Stripe delivery log is your confirmation. If
    Stripe shows the money but the deliveries failed — or never happened — work through
    the failure modes below.
  </Step>

  <Step title="Assign the plan">
    Paying does not switch the client's plan by itself. After checkout, open
    **Sub-accounts** and assign the plan the client bought — that is what applies the
    plan's limits and updates the **Current** badge on their billing page. Make this part
    of your routine whenever a client subscribes or changes plan.
  </Step>
</Steps>

<Note>
  A checkout against live keys charges the card for real — if you test with your own card,
  refund it afterwards from your Stripe dashboard. DMLY also accepts Stripe **test-mode**
  keys (`sk_test_`), which run the whole flow in Stripe's test mode with test cards. If you
  rehearse that way, remember keys, price IDs, webhook endpoint, and signing secret all
  belong to one mode: switching to live means re-entering live keys, live price IDs, and a
  live-mode webhook endpoint with its own signing secret.
</Note>

## When something goes wrong

<Accordion title="A client paid, but nothing shows up in DMLY">
  The nastiest one, because checkout itself works: the money is in your Stripe account, but
  DMLY has no record of the subscription or payment. It means Stripe's events never reached
  DMLY — the webhook endpoint was never created, points at the wrong URL, or doesn't
  include the subscription events.

  Diagnose it from your Stripe dashboard, in the webhook deliveries for your endpoint —
  DMLY has no log of traffic that never arrived. No endpoint at all? Create it as above.
  Deliveries listed but failing? Match the response code against the test step above.
  Deliveries succeeding but nothing recorded? Check the event types — see below.

  The client is not locked out while you sort this out: a client workspace's access follows
  your agency's standing, not its own subscription state. Once the webhook is fixed, Stripe
  can resend the missed events from the delivery history, and DMLY records each event only
  once — resending is safe.
</Accordion>

<Accordion title="Every webhook delivery returns 403 Invalid webhook signature">
  The signing secret saved in DMLY doesn't match the endpoint — or was never saved at all.
  The **Webhook signing secret** field is optional at save time, so it's easy to save your
  keys without it and not notice: everything else says **Connected**, and every single
  delivery is rejected.

  This also happens after a key rotation, because re-saving the panel with a blank
  **Webhook signing secret** erases the stored one (see the warning above).

  Fix: copy the `whsec_` secret from the endpoint in Stripe and re-save the Stripe panel in
  DMLY with the secret key and the signing secret both filled in.
</Accordion>

<Accordion title="Deliveries succeed in Stripe, but subscriptions never appear">
  DMLY returns a success response even for event types it doesn't handle, so a
  wrongly-configured endpoint can look healthy in Stripe while doing nothing. Open the
  endpoint in Stripe and check the selected events against the list of seven above — in
  particular, `customer.subscription.created` and `invoice.payment_succeeded` must be
  selected, or no subscription or receipt is ever recorded.
</Accordion>

<Accordion title="A client hits an error when choosing a plan">
  If the page flashes *This agency has not connected Stripe yet.*, no secret key is saved —
  finish the first section above. If it flashes a missing-price message, the plan's cycle
  has no Stripe price ID — add it on **Plans**.

  If checkout fails with a generic server error, the saved secret key is wrong, or the key
  and the plan's price IDs belong to different modes (a live key with test-mode `price_`
  IDs, or the reverse). DMLY doesn't validate keys at save time, so this is where a bad key
  first shows itself. Re-enter the key and confirm every price ID comes from the same
  Stripe mode as the key.
</Accordion>

<Accordion title="The client paid but their plan and limits didn't change">
  Expected — payment and plan assignment are separate steps. Checkout records the
  subscription and the payment; it does not change the workspace's plan. Assign the plan
  from **Sub-accounts** after the payment lands. Until you do, the client's **Plans &
  billing** page keeps the old **Current** badge and their limits stay as they were.
</Accordion>

<Accordion title="A client stopped paying but still has access">
  Also expected: a client workspace is never locked out automatically over its own failed
  or cancelled payments — access follows your agency's standing with DMLY. Chasing payment
  and suspending the workspace is your call, from **Sub-accounts**. See
  [Client workspaces](/agency/client-workspaces).
</Accordion>

## Related

<Columns cols={2}>
  <Card title="Price and bill your clients" icon="credit-card" href="/agency/plans-and-billing">
    Build the plans and add-ons you sell, and what clients see.
  </Card>

  <Card title="Client workspaces" icon="users" href="/agency/client-workspaces">
    Assign plans, suspend, restore, and open a client's workspace.
  </Card>

  <Card title="Agency overview" icon="building" href="/agency/overview">
    What the whitelabel layer includes.
  </Card>

  <Card title="Payments & gateways" icon="money-bill-transfer" href="/finance/payments-and-gateways">
    The other Stripe: how a workspace collects payments from its own customers.
  </Card>
</Columns>
