Two-way help-desk sync
Most universities already run a support system their teams live in: ServiceNow, Jira Service Management, Zendesk, a counselling case system. First Six is built to route a student's request to that system (through outbound webhooks and email-to-case queues) and then get out of the way. This endpoint is the returning half: your system tells First Six where the case is up to, so the staff inbox and the student's "My requests" reflect reality instead of saying "new" forever.
Status truth, not conversation. Your team acknowledges, works, and resolves the case in your system; this endpoint mirrors those state changes back. Replies to the student happen wherever you normally reply to students.
The safety boundary
An external credential can never:
- Change a request's priority. Crisis can never be downgraded from outside; the write path does not touch priority at all.
- Assign or unassign staff. Assignment stays an internal judgement.
- Read anything. The endpoint returns only the outcome of its own writes.
There is no GET, and a write against another institution's request returns
the same
not_foundas a request that does not exist.
Every inbound update lands in the append-only audit trail attributed to your integration's name, so a reviewer can always tell a machine's write from a person's.
Authentication
An institution admin issues the sync secret in the console under Settings → Integrations → Two-way help-desk sync (requires the Manage integrations permission). The secret is shown once and stored as a hash. Send it as a Bearer token:
POST /api/integrations/help-sync
Authorization: Bearer <your sync secret>
Content-Type: application/jsonThere is deliberately no platform-wide fallback credential: until your admin issues a secret, this surface does not exist for your institution. Revoking it in the console shuts the door immediately.
The request
{
"institution": "your-slug",
"actor": "ServiceNow (Student Services)",
"updates": [
{
"help_request_id": "6b1e0c2a-…",
"action": "acknowledge",
"external_ref": "SN-CASE-0042"
},
{
"help_request_id": "8c2f1d3b-…",
"action": "resolve",
"note": "Student contacted and supported; case closed in ServiceNow."
}
]
}institution: your institution slug (required).actor: how your updates are named in the audit trail and inbox timeline. Set it to something a staff member reading the timeline will recognise. Can be overridden per update.updates: up to 100 per request. Each row succeeds or fails on its own; the batch never aborts halfway.help_request_id: comes from the outbound webhook payload (help_request_id).
Actions
| Action | Effect | When it changes nothing |
|---|---|---|
acknowledge | new → ack ("your team has seen it") | Any other starting status |
note | Adds a staff-visible note to the request's timeline | Never (but note text is required) |
resolve | Any open status → resolved, stamps the resolved time | Already resolved |
reopen | resolved → in_progress, clears the resolved time | Not currently resolved |
Any update may also carry external_ref (your own case id, up to 200
characters). It is stamped on the request, shown in the timeline, and echoed in
every subsequent webhook payload for that request, so you never need a mapping
table. Actions that change nothing still return ok: true with
changed: false, so retries are harmless.
The response
{
"results": [
{ "help_request_id": "6b1e0c2a-…", "ok": true, "status": "ack", "changed": true, "external_ref": "SN-CASE-0042" },
{ "help_request_id": "8c2f1d3b-…", "ok": false, "error": "not_found" }
]
}Row-level error values: not_found (no such request in your institution),
bad_action, bad_help_request_id, note_required, update_failed
(transient; retry). Request-level errors use HTTP status codes: 401 for
credential problems (the body says whether the secret was missing, wrong,
revoked, or never issued), 400/413 for structural problems, 429 when
rate-limited.
Avoiding echo loops
Every change this endpoint makes flows back out through the outbound webhooks,
exactly like a change made in the console. Those events carry
"origin": "integration"; everything else carries "origin": "platform".
If your consumer writes back through this endpoint, drop
origin: "integration" events. They are your own updates returning.
Putting the loop together
- Subscribe to the outbound webhooks
Configure an endpoint for
help_request.createdandhelp_request.crisisunder Settings → Integrations → Outbound webhooks, and open a case in your system when one arrives. See webhooks. - Stamp your case id
When you open the case, send one
acknowledgeupdate withexternal_refset to your ticket number. The inbox timeline shows the linkage and every later webhook payload carries it. - Mirror the close
When your team resolves the case, send
resolve(a closingnoteis worth including: it lands in the timeline for the staff who use the First Six inbox). If the case reopens, sendreopen. - Test with a real round trip
Raise a test help request as a preview student, watch it arrive in your system, resolve it there, and confirm the First Six inbox and the student's "My requests" show resolved.
Troubleshooting
401 with "No help-desk sync secret has been issued for this institution."
Nobody has minted the credential yet. An institution admin issues it in the console under Settings → Integrations → Two-way help-desk sync. There is no platform default secret for this surface, deliberately.
401 with "This institution's help-desk sync secret has been revoked."
An admin revoked the credential, which shuts inbound sync until a new one is issued. Rotating in the console produces a fresh secret; the old one stops working the moment it is replaced.
Every update returns "not_found" but the ids look right
Two common causes. The id being sent is your own ticket number rather than
the First Six help_request_id from the webhook payload; or the
institution slug in the body does not match the institution the secret
was issued for. A request belonging to a different institution returns the
same not_found as a request that does not exist, by design.
Your system keeps re-processing its own updates
Your webhook consumer is reacting to events your sync writes caused. Drop
events with "origin": "integration" before they reach your case logic;
they are echoes, not news.
A resolve worked but the student still sees the request as open
Check the response row: ok: true with changed: false means the request
was already in that state, and status tells you what the platform
currently holds. If a staff member reopened it in the console after your
resolve, the console wins; subscribe to help_request.updated to hear
about it.
Next steps
The fastest answer is usually one question away.