Confirming your payment

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

Help by DeltaBreen
PricingDocsConnect
Sign inStart free
← Docs

The API

Read and work your tickets from your own code: list them, read one, reply, add a private note, change it, and open a new one for a customer. It's on Pro and higher.

Want an AI assistant to do this instead? See Connect your own AI. Want your code told when something happens? Webhooks are below.

Your key

Make a key under Settings → API and pick what it may do. Send it with every request:

Authorization: Bearer hpk_…

A key belongs to one workspace and acts as its owner. Keep it on your server: anyone who has it can read your customers' messages.

PermissionLets the key
workspace:readSay who it is, and list your sites
tickets:readList and read tickets
tickets:writeReply, add a private note, change a ticket, open a ticket

Everything is JSON over https://help.deltabreen.eu. A key may make 120 requests a minute.

When we say no

A refusal is always {"error": "<code>", "message": "<a sentence>"}. The code stays the same; the sentence may change.

StatusCodeWhat happened
400bad_body and othersSomething in what you sent is wrong. The sentence says what
401unauthorizedNo key, or one we don't know
402api_requires_proYour plan doesn't have the API. The key works again when the plan does
403missing_scopeThe key wasn't given that permission
404not_foundNo such ticket or site in your workspace
409product_paused, ticket_is_mergedIt can't be done to this one right now
429rate_limitedToo many requests. Retry-After says how many seconds to wait

A key we don't know in a body is refused, not ignored, so a typo never sends a private note to a customer.

Who am I

GET /api/v1/whoami

Works on every plan. It answers the workspace, its plan (tier) and the key's permissions (scopes).

curl https://help.deltabreen.eu/api/v1/whoami -H "Authorization: Bearer hpk_…"

Sites

GET /api/v1/products

Your sites, each with its id and name (the API calls a site a product). You need a site's id to open a ticket in it.

curl https://help.deltabreen.eu/api/v1/products -H "Authorization: Bearer hpk_…"

Tickets

GET /api/v1/tickets

A page of thirty tickets, newest change first.

QueryMeans
viewopen (the default), on_hold, pending, closed, all, spam or due
productOnly this product's, by id
assigneenone, me (the owner), someone (anyone on the team), or a teammate's id
prioritylow, normal, high or urgent
tagOnly tickets with this tag
qWords to find in the subject or any message, or a ticket's number
fieldkey:value, for a form field with a fixed set of answers
customerEvery ticket from one person, by their id
statusOnly tickets in one of your own statuses, by its id (Lite and higher)
teamA team's queue by its id, mine for your teams', or none for no team's (Pro and higher)
companyOnly tickets from one company's people, by its id (Pro and higher)
tfieldkey:value, for a team field (Lite and higher)
modelive (the default) or test
sortrecent (the default) or priority
beforeThe next of the page before
curl "https://help.deltabreen.eu/api/v1/tickets?view=open&priority=urgent" -H "Authorization: Bearer hpk_…"
{"tickets":[{"id":"6f1c…","number":1042,"subject":"Where is my order?","status":"open","mode":"live",
  "priority":"urgent","tags":["refund"],"product_id":"…","product_name":"Acme shop",
  "assignee_user_id":null,"assignee_email":null,"assignee_name":null,"external_id":"42","preview":"Order A-100 never came.",
  "status_id":null,"status_name":null,"team_id":"…","team_name":"Billing","company_id":null,"company_name":null,
  "created_at":"2026-10-13T09:10:00Z","updated_at":"2026-10-13T09:12:00Z"}],
 "next":"2026-10-13T09:12:00.000000Z~6f1c…",
 "counts":{"open":12,"on_hold":2,"pending":4,"closed":230}}

next is empty on the last page. status is open (waiting on you), on_hold (waiting on someone else), pending (waiting on the customer) or closed; status_name is the name of one of your own statuses when the ticket is in one, which is always of the kind status says. team_name is the team it's with, and company_name the company its customer is with. external_id is the id your own site signed for a logged-in user; a guest has none. assignee_name is the name the person it is assigned to gave themselves under Settings → Team, empty until they give one.

GET /api/v1/tickets/{id}

One ticket with its whole thread. On top of what the list shows:

FieldHolds
messagesEach with seq, author_kind (end_user or agent), author_email and author_name (a teammate's, empty for the customer), visibility (public, or internal for a private note), body, created_at, mail (what became of the email that carried a reply) and sent_by (api when a key sent it, ai when a person's AI assistant did, empty from the portal)
requesterThe customer: id, email, external_id, verified, email_confirmed, email_stopped
fieldsWhat they answered in your form's own fields
closed_at, purge_after, legal_holdWhen it closed, when auto-delete removes it, and whether you're keeping it
team_valuesYour team fields on it, by key. Only your team sees them
issuesThe GitHub, Linear or Jira issues made from it or linked to it: kind, key, url, title
curl https://help.deltabreen.eu/api/v1/tickets/6f1c… -H "Authorization: Bearer hpk_…"

POST /api/v1/tickets

Open a ticket for a customer, from your contact form, your back office or another tool.

BodyMeans
product_idThe product it belongs to. Needed
emailWho it's from. Needed
bodyTheir message. Needed
subjectLeft out, it's the start of the message
fieldsAnswers to your form's own fields, by key: {"order_id": "A-100"}
submission_idYour own name for this ticket: 16 to 64 letters, digits, - or _. Send the same one again and you get the same ticket back, not a second one
testtrue makes a test ticket: nobody is emailed and it's removed after a week
curl https://help.deltabreen.eu/api/v1/tickets -H "Authorization: Bearer hpk_…" \
  -H "Content-Type: application/json" \
  -d '{"product_id":"…","email":"[email protected]","subject":"Refund","body":"Order A-100 never came.","submission_id":"refund-order-A-100"}'
{"id":"6f1c…","number":1043,"created":true}
  • The customer is always a guest. A key can't say someone is a logged-in user of your site: only the token your site signs does that, so a ticket opened here never shows the side panel or buttons.
  • The first email they get asks them to confirm their address. Your replies are emailed once they have.
  • The email names your site's verified domain, or its verified support address (Settings on the site) when it has no domain. With neither, nothing is emailed until one is verified.
  • It counts as one of your plan's tickets for the month.

POST /api/v1/tickets/{id}/messages

Reply to the customer, or add a private note for your team.

BodyMeans
bodyThe text. Needed
visibilitypublic for a reply (the default), internal for a private note. "internal": true means the same
statusWhere the ticket goes next. Left out, a reply makes it pending, and one on_hold stays on hold
curl https://help.deltabreen.eu/api/v1/tickets/6f1c…/messages -H "Authorization: Bearer hpk_…" \
  -H "Content-Type: application/json" \
  -d '{"body":"It ships tomorrow.","status":"closed"}'

It answers {"message": {…}}, the message as the ticket now holds it.

PATCH /api/v1/tickets/{id}

Change a ticket. Send only what changes; it answers the ticket as it is afterwards.

BodyMeans
statusopen, on_hold, pending or closed
status_idOne of your own statuses, by its id. It moves the ticket to that status's kind
assignee_user_idA teammate's id, or null for nobody
team_idA team's id, or null for no team
team_valuesTeam fields by key, as {"refund": "12.50"}: only those sent change, and null clears one
prioritylow, normal, high or urgent
tagsAll of the ticket's tags. This replaces the list
spamtrue or false
legal_holdtrue keeps the ticket past auto-delete (Business)
seen_updated_atOptional. The ticket's updated_at as you last read it: if the ticket has changed since, a status or assignee_user_id sent with it changes nothing and the answer is 409 ticket_changed
curl -X PATCH https://help.deltabreen.eu/api/v1/tickets/6f1c… -H "Authorization: Bearer hpk_…" \
  -H "Content-Type: application/json" \
  -d '{"priority":"urgent","tags":["refund"]}'

Webhooks

A webhook tells your code when something happens on a ticket, so it doesn't have to keep asking. Add one under Settings → Webhooks and choose what it's sent. Pro has 3 webhooks, Business 10. Only an owner or an admin can add one, and only in the portal.

You can send to one of four places:

  • Your URL. A URL on your own server. We send it nothing until it passes the connection test below.
  • Zapier. Paste the URL of a "Webhooks by Zapier" Catch Hook.
  • Make. Paste the URL of a "Custom webhook".
  • n8n Cloud. Paste the production URL of a Webhook node set to POST. A self-hosted n8n is your URL: see the connection test below.

We never show a saved URL again. To change it, give the webhook a new one.

What we send

POST <your URL>
Content-Type: application/json
User-Agent: DeltaBreen-Help/1 (+https://help.deltabreen.eu/docs/integrations)
X-DeltaBreen-Request-Id: evt_…
X-DeltaBreen-Signature: t=<unix seconds>,v1=<hex>
{"id":"evt_8841","type":"ticket.customer_replied","at":"2026-10-13T09:12:00Z","workspace":"acme",
 "ticket":{"id":"6f1c…","number":1042,"mode":"live","status":"open","priority":"high","tags":["refund"],
  "subject":"Where is my order?","product":{"id":"…","name":"Acme shop"},
  "assignee":{"id":"…","email":"[email protected]"},"fields":[],"own_status":null,"team":{"id":"…","name":"Billing"},
  "company":{"id":"…","name":"Acme Ltd"},"team_fields":{"refund":"12.50"},"created_at":"2026-10-13T09:10:00Z",
  "url":"https://help.deltabreen.eu/app/w/acme/tickets/6f1c…"},
 "message":{"id":"…","from":"customer","visibility":"public","body":"Order A-100 never came.","at":"2026-10-13T09:12:00Z"},
 "customer":{"id":"…","email":"[email protected]","external_id":"42","verified":true}}
typeSent whenAlso carries
ticket.createdA customer opens a ticketmessage, their first
ticket.customer_repliedThe customer repliesmessage
ticket.agent_repliedYour team repliesmessage
ticket.note_addedSomeone adds a private notemessage, with visibility internal
ticket.status_changedA ticket is opened, put on hold, set waiting or closed, or put in one of your own statuseschange: from, to, and own (its name) for your own
ticket.assignedA ticket is given to someone, or to nobodychange: to
ticket.priority_changedThe priority changeschange: from, to
ticket.tags_changedTags are added or removedchange: added, removed
ticket.team_changedA ticket is given to a team, or to nonechange: from, to, to_name
ticket.lateA reply is late for your reply times
ticket.ratedThe customer rates a closed ticketrating: stars (1 to 5), words
pingYou press Test connectionNothing: no ticket, no customer
  • ticket is the ticket as it is when we send, which may be a moment after the event. at is when it happened.
  • customer.external_id is the id your site signed for a logged-in user. A guest has none, and verified is false.
  • ticket.status is open, on_hold (waiting on someone else), pending or closed; own_status is one of your own statuses when the ticket is in one. team, company and team_fields are null or empty when there are none.
  • A private note is only sent to a webhook that asks for ticket.note_added.
  • Test tickets are only sent to a webhook with "Send test tickets too" on. Their ticket.mode is test. Spam is never sent.
  • A very long message is cut to fit, and the body then has "cut": true. Read the whole ticket with the API.

Check it's from us

Every request is signed with the webhook's secret, the same way as an integration's. See Check the signature first: check the signature against the raw body before you parse it, and turn away a t more than five minutes old.

You get the secret once, when you add the webhook. "Get a new secret" makes another; for a day we sign with both, so you can change your server without missing anything.

The connection test

Before your own URL is sent any event, it must prove it holds the secret. Press Test connection and we send:

{"id":"req_…","type":"handshake","mode":"live","product":"","sent_at":"2026-10-13T09:00:00Z","challenge":"…"}

Answer with {"challenge_response": "<hex HMAC-SHA256(secret, challenge)>"}. It's the same test an integration passes, so one handler can serve both. Change the URL and we run the test again before sending to the new one.

Zapier, Make and n8n Cloud can't answer the test, so we don't ask them. Their URL must be one of theirs.

A self-hosted n8n can answer it. In the workflow, set the Webhook node to respond with "Respond to Webhook". Then, when the body's type is handshake, a Crypto node makes an HMAC (SHA256, hex) of challenge with the webhook's secret. Answer {"challenge_response": "<that hex>"}. Send any other request on through the rest of the workflow.

When your URL is down

  • Answer with any 2xx within five seconds and the event is done. We don't read what you answer.
  • Anything else and we try again: after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. Then we give up on that event.
  • An event can arrive twice, or out of order after a retry. Use id to act on each one once.
  • If your URL answers 404 or 410, or we give up on twenty events in a row, we turn the webhook off and email the workspace's owner, once. Turn it on again under Settings → Webhooks, where you can also see what was sent in the last three days and the events we gave up on in the last two weeks, and send one of those again.
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