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.
How it works
Section titled “How it works”Connecting the generic webhook gives you two URLs:
| URL | Your system calls it when… | What SafeHanded does |
|---|---|---|
| Webhook URL | a ticket’s status changes | Finds 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 ticket | Creates 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).
Before you start
Section titled “Before you start”- 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. Connect
Section titled “1. Connect”- Go to Settings → Integrations → Connect → Generic webhook and give it a name (for example, the name of your helpdesk).
- 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.
2. Allow it to purge
Section titled “2. Allow it to purge”Because SafeHanded can’t verify these posts, they update the ticket card but never destroy anything until you allow it:
- Go to Settings → Policy → Tickets.
- 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.
3. Link requests to tickets
Section titled “3. Link requests to tickets”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 (
12345andINC-12345are different references). - Requests created through the trigger URL (step 6) are linked automatically.
4. Send status changes
Section titled “4. Send status changes”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,completeandcompletedare 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.
Using your system’s own format
Section titled “Using your system’s own format”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.
5. Test it
Section titled “5. Test it”-
Create a test request and put
TEST-1in Ticket reference. -
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"}' -
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:
- 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).
- Paste the link below as its address. Then replace
TICKET_IDat 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. - 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_IDThe 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"}| Field | Required? | What it’s for |
|---|---|---|
ticket_id | Yes | The ticket to link. Uses the same Ticket id field as step 4. |
subject.name | Yes | The person who’ll send the credential. |
subject.email | If the preset sends an emailed code or emails the link | The subject’s email address. |
subject.phone | If the preset texts the link or a code | The subject’s mobile number. |
preset | No | The preset’s slug. Without it, the client’s default preset is used, then the organisation default. |
requester_email | No | The 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.)
7. Secure it (optional)
Section titled “7. Secure it (optional)”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:
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.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause / fix |
|---|---|
| Ticket card never changes | The 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 purged | Turn on Trust unverified webhooks (step 2), and check the status meets Purge submitted credentials. A grace period delays the purge. |
| Card shows unknown | Map that status under Configuration → Status mapping. |
401 | A 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. |
404 | The URL is wrong, or the secret was rotated. Copy it again from Activity. |
409 | The integration is disabled. |
429 | Too many posts. Send only status changes, not every ticket update, and retry after the Retry-After time. |
| Trigger post accepted but no request | Check the audit log for Ticket trigger failed, and that you’re on the Team plan or above. |