Skip to content

Generic webhook

Use the generic webhook when your helpdesk doesn’t have a native adapter, or when you’d rather drive SafeHanded from a script or an automation platform. If it can send an HTTP POST, it can work with SafeHanded.

What you get: credentials purged when the ticket closes, and (optionally) requests created from a ticket. What you don’t get: private notes, in-ticket replies or the certificate attached to the ticket — SafeHanded can’t write to your system, so those need a native adapter.

Connecting the generic webhook gives you two URLs:

URLYour system calls it when…What SafeHanded does
Webhook URLa ticket’s status changesFinds the request linked to that ticket and, if the ticket is now closed, purges the credential.
Trigger URL (optional, Team plan and above)you want a request created for a ticketCreates the request and links it to the ticket, with no one in SafeHanded.

Each URL has a secret built in, so treat both like passwords. SafeHanded can’t look the ticket up in your system to double-check what you send, so it acts on your posts as they arrive — which is why purging needs one extra switch (step 2).

  • A SafeHanded Owner or Admin. The generic webhook is available on every plan, including Free.
  • A system that can send an HTTP POST when a ticket’s status changes — for example a helpdesk automation rule, a workflow tool, or a script.
  • About fifteen minutes.
  1. Go to Settings → Integrations → Connect → Generic webhook and give it a name (for example, the name of your helpdesk).
  2. Open it and go to the Activity tab. The Webhook URL is shown there, and the Trigger URL is under Values for your Generic webhook setup below it. Use Rotate secret if a URL ever leaks — it changes both URLs, so update your system with the new ones.

There’s no Test connection — the integration only receives posts. You’ll test it in step 5.

Because SafeHanded can’t verify these posts, they update the ticket card but never destroy anything until you allow it:

  1. Go to Settings → Policy → Tickets.
  2. Open Advanced and turn on Trust unverified webhooks.

While you’re there, check Purge submitted credentials (which statuses count as closed — resolved or closed by default) and the grace period.

SafeHanded only acts on tickets that are linked to a request. The link is the ticket reference:

  • When you create a request by hand, type the ticket’s id in Ticket reference exactly as your system will send it — same capitals, same prefix (12345 and INC-12345 are different references).
  • Requests created through the trigger URL (step 6) are linked automatically.

Set your system to POST to the Webhook URL whenever a ticket’s status changes, with this JSON body:

{
"ticket_id": "12345",
"status": "closed"
}
  • ticket_id — the ticket’s id, matching the request’s ticket reference.
  • status — the ticket’s status as your system names it. open, new, pending, on hold, resolved, solved, closed, complete and completed are understood out of the box, in any capitals. Anything else shows as unknown (and never purges) until you map it under Configuration → Status mapping — numbers are fine, just map the number.

Form-encoded bodies and query-string parameters work too.

If your system sends its own body and you can’t change the shape, tell SafeHanded where to find the two values. On the Configuration tab, set Ticket id field and Status field to their position in the body, using dots for nesting. For example, if your system sends:

{
"ticket": {
"number": "INC-5",
"status": { "name": "Closed" }
}
}

set Ticket id field to ticket.number and Status field to ticket.status.name.

Not sure what your system sends? Send one post, then open Activity: Recent posts SafeHanded couldn’t use shows the body it received and why it couldn’t be used.

  1. Create a test request and put TEST-1 in Ticket reference.

  2. Send a status change for that ticket. From a terminal (replace the URL with your Webhook URL):

    Terminal window
    curl -X POST 'https://<your webhook URL>' \
    -H 'Content-Type: application/json' \
    -d '{"ticket_id": "TEST-1", "status": "closed"}'
  3. Check that the request’s ticket card shows closed. If the request was submitted and you turned on Trust unverified webhooks, it’s purged (after any grace period). An unsubmitted request is revoked if your policy says so.

SafeHanded answers 202 Accepted to every post it receives — even one for a ticket it doesn’t know — so check the result in SafeHanded rather than relying on the response code.

6. Create requests from a ticket (optional)

Section titled “6. Create requests from a ticket (optional)”

There are two ways.

A link on the ticket (any plan). A link or button on the ticket opens a new SafeHanded request linked to that ticket:

  1. In your ticket system, add a link or button to the ticket screen (most systems have a custom button, a custom link or a URL field).
  2. Paste the link below as its address. Then replace TICKET_ID at the end with your system’s placeholder for the ticket’s ID, so each ticket gets its own link. Use the same ID your system sends in its posts.
  3. Save. Technicians click it on a ticket to open a new SafeHanded request linked to that ticket.
https://safehanded.com/app/requests/new?ticket=TICKET_ID

The link is the same for every ticket system: your SafeHanded address followed by /app/requests/new?ticket= and the ticket’s ID.

Choosing a preset (optional). A preset is a saved request — the reason, how long the credential is kept, who receives it and how the link is sent. To start from one, add &preset= and the preset’s slug to the end of the link, for example &preset=password-reset. Each preset’s slug is shown, with a copy button, under Settings → Request presets. With no preset in the link, SafeHanded uses the client’s default preset, then the organisation default (Settings → Policy → Default preset for new requests), and otherwise the policy defaults.

The trigger URL (Team plan and above). Your automation POSTs to the Trigger URL and SafeHanded creates the request, links it to the ticket and sends the link by email or text as the preset says, with no one in SafeHanded. (The generic webhook can’t post to the ticket, so a preset set to reply in the ticket creates the request without sending the link: use New link on the request and send it yourself.)

{
"ticket_id": "12345",
"subject": { "name": "Jane Smith", "email": "jane@example.com", "phone": "+447700900123" },
"preset": "password-reset",
"requester_email": "tech@yourcompany.com"
}
FieldRequired?What it’s for
ticket_idYesThe ticket to link. Uses the same Ticket id field as step 4.
subject.nameYesThe person who’ll send the credential.
subject.emailIf the preset sends an emailed code or emails the linkThe subject’s email address.
subject.phoneIf the preset texts the link or a codeThe subject’s mobile number.
presetNoThe preset’s slug. Without it, the client’s default preset is used, then the organisation default.
requester_emailNoThe technician who’ll own the request, if the preset takes the requester from the ticket. Otherwise the preset’s fallback requester is used.

If your body is shaped differently, map each field on the Configuration tab (the Trigger: fields), just like step 4. Posting the trigger twice for the same ticket and preset won’t create a duplicate while the first request is still open.

If a request can’t be created — no preset, no requester, or no subject name — SafeHanded records a Ticket trigger failed event in the audit log with the reason. (The generic webhook can’t post a note on the ticket, so check the audit log.)

The secret in each URL is enough on its own. If your system can add a header or sign its posts, you can also require one of these on the Configuration tab. Both apply to the webhook URL and the trigger URL.

Shared secret header — a fixed header your system sends on every post. Set Shared secret header to its name (for example X-Webhook-Token) and Shared secret value to its value.

HMAC signature — set HMAC signing secret to a long random string, then sign each post with an X-SafeHanded-Signature header:

X-SafeHanded-Signature: t=<unix timestamp>,v1=<signature>

where <signature> is the lowercase hex HMAC-SHA256 of <timestamp>.<raw body> using your signing secret. The timestamp must be within five minutes of the current time. For example:

Terminal window
body='{"ticket_id": "TEST-1", "status": "closed"}'
t=$(date +%s)
sig=$(printf '%s' "$t.$body" | openssl dgst -sha256 -hmac "$SIGNING_SECRET" -hex | sed 's/^.* //')
curl -X POST 'https://<your webhook URL>' \
-H 'Content-Type: application/json' \
-H "X-SafeHanded-Signature: t=$t,v1=$sig" \
-d "$body"

Sign the exact bytes you send — re-formatting the JSON after signing breaks the signature.

SymptomCause / fix
Ticket card never changesThe ticket id didn’t match a linked request. Check Recent posts SafeHanded couldn’t use on Activity, and that the request’s Ticket reference is exactly what your system sends.
Ignored response, or “No ticket id or status found”SafeHanded couldn’t find the ticket id or status in your body. Set Ticket id field / Status field (step 4).
Card shows closed but nothing is purgedTurn on Trust unverified webhooks (step 2), and check the status meets Purge submitted credentials. A grace period delays the purge.
Card shows unknownMap that status under Configuration → Status mapping.
401A shared secret header or HMAC signature is set on the integration and the post didn’t match. For HMAC, check the clock and that you signed the exact body.
404The URL is wrong, or the secret was rotated. Copy it again from Activity.
409The integration is disabled.
429Too many posts. Send only status changes, not every ticket update, and retry after the Retry-After time.
Trigger post accepted but no requestCheck the audit log for Ticket trigger failed, and that you’re on the Team plan or above.