Confirming your payment

Paddle has your payment. This usually takes under a minute.

Help by DeltaBreen
PricingDocsConnect
Sign inStart free
← Docs

Integrations

An integration is one URL on your server. We send it a signed request when an agent opens a ticket, so we can show who the customer is, and when an agent presses a button, so you can do something in your system. We show your answer to the agent. We don't store it or log it.

Want to be told when a ticket arrives or changes instead? That's a webhook: see The API. It's signed the same way and passes the same connection test.

Want to start from working code? Here's a whole endpoint in one file, with tests: examples/customer-backend/server.mjs.

The request

POST <your integration's URL>
Content-Type: application/json
User-Agent: DeltaBreen-Help/1 (+https://help.deltabreen.eu/docs/integrations)
X-DeltaBreen-Request-Id: req_…
X-DeltaBreen-Signature: t=<unix seconds>,v1=<hex>
  • It's always a POST to the one URL you gave us, with this body. Ids are in the body, not the URL.
  • Your URL must be https on port 443, on a public address. We don't follow redirects.
  • If you set a header of your own for the URL, we send it too, next to ours.
  • type is handshake, hydrate or action. A fourth, import, is sent only to our WordPress plugin, to bring a site's old tickets over, and never to a URL of your own.
{"id":"req_…","type":"hydrate","mode":"live","product":"prd_…","sent_at":"2026-10-12T12:00:00Z",
 "user":{"id":"42"},"ticket":{"id":"…","number":1042},"agent":{"id":"…"}}
typeCarriesAnswer with
handshakechallenge{"challenge_response": "<hex HMAC-SHA256(secret, challenge)>"}
hydrateuser, agent, usually ticketAny JSON object: whatever you want shown next to the ticket
actionuser, agent, action: {key, params, idempotency_key}{"ok": true}. You can add reference (we keep it in the audit log, up to 64 characters) and message

user.id is the id your own server signed for that person, so it can be trusted. mode is live or test.

Check the signature first

v1 is HMAC-SHA256, in lowercase hex, over "<t>." + the raw request body, using the integration's secret as the key. Check it against the bytes you received, before you parse them. Turn away any request whose t is more than 300 seconds from your clock. After you get a new secret, the header has two v1 values for a day, one for each secret. Accept either.

Test your code against these: docs/signature-vectors.json.

// Node. rawBody is the exact bytes received.
import { createHmac, timingSafeEqual } from 'node:crypto'
function verify(header, rawBody, secret) {
  const parts = (header ?? '').split(',').map((p) => p.trim())
  const t = parts.find((p) => p.startsWith('t='))?.slice(2)
  if (!t || !/^\d+$/.test(t) || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false
  const want = Buffer.from(createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest('hex'))
  return parts.filter((p) => p.startsWith('v1=')).map((p) => Buffer.from(p.slice(3)))
    .some((got) => got.length === want.length && timingSafeEqual(got, want))
}

The connection test

Before we send your URL anything about a customer, it has to show it holds the integration's secret. We send a handshake with a random challenge, and your URL answers with the HMAC of that challenge, using the secret as the key. Sending the challenge straight back doesn't count. This is what Test connection does in the portal, and it's why an integration can only point at a system that has your secret, never at someone else's API.

Buttons

A button sends a request to the same URL as everything else, with type set to action and the button's key. You add buttons on the integration's Buttons tab in the portal.

{"id":"req_…","type":"action","mode":"live","product":"prd_…","sent_at":"2026-10-12T12:00:00Z",
 "user":{"id":"42"},"ticket":{"id":"…","number":1042},"agent":{"id":"…"},
 "action":{"key":"cancel_subscription","params":{"reason":"Asked by email"},"idempotency_key":"…"}}

Do the work, then answer {"ok": true}, or {"ok": false} if you won't do it. Add a message and we show it to the agent. Either way we add a private note to the ticket.

An agent has to type a word before a button runs. idempotency_key is the same for every retry of one press, so do the thing once, and answer a repeat with the first result. We never retry a press ourselves.

Limits

Time to answer8 s for hydrate and handshake, 15 s for action
Answer size256 KB
Answer typeapplication/json, UTF-8
RedirectsNot followed
Our addressRequests come from the address shown on your integration's Connection tab, so your firewall can allow it

Stripe and Shopify, without a URL of your own

On Pro and higher you can show what Stripe or your Shopify shop knows about a customer without writing anything. On the Integrations page press Add Stripe or Add Shopify:

You giveWe read
StripeA restricted key (rk_live_…), with read access to customers, subscriptions and chargesThe customer with that email, their subscriptions and their last charges
ShopifyYour shop's ….myshopify.com address and an Admin API access token from a custom app that may read customers and ordersThe customer with that email and their last orders
  • We look a customer up only by an email their site signed or that they confirmed. A guest who only typed an address is never looked up.
  • The key is tried before it is kept, then kept sealed and never shown again. Stripe's secret keys (sk_…) are refused: a restricted key can only read.
  • What Stripe or Shopify answers is shown to the agent and kept nowhere, as your own URL's answer is.
  • You choose what the side panel shows on its Side panel tab, as with any integration. They have no buttons.
Help by DeltaBreen

A helpdesk that lives inside your own site. One price per workspace, never per agent.

Get started

PricingDocsConnect

Legal

TermsRefund policyPrivacyData processing agreementSecurity

Company

[email protected]© 2026 DeltaBreen Limited · Company number 826627 · Gorey, Co. Wexford, Ireland