HyphaBase

Deleting and undeleting

Deleting a customer is reversible by default. Making it permanent is a different request, on purpose.

Someone in Sarah's office deletes the wrong Henderson.

That's fine. Deleting a customer hides them, it doesn't destroy them, and putting them back is one request. Ten years of visit history isn't riding on a mis-tap in a busy office.

Delete

DELETE /api/modules/clients/{id}
curl -X DELETE https://api.hyphabase.com/api/modules/clients/$CUSTOMER_ID \
  -u sk_live_xxx: -H "X-Location-Id: $HYPHABASE_LOCATION_ID"

Afterwards they're gone from lists and a direct read is a 404. Their addresses, notes and tags are untouched, waiting.

Find deleted customers

Two query parameters:

# Deleted people alongside current ones.
curl -G https://api.hyphabase.com/api/modules/clients \
  -u sk_live_xxx: -H "X-Location-Id: $HYPHABASE_LOCATION_ID" \
  --data-urlencode "include_deleted=1"

# Only the deleted ones — a recycle bin.
curl -G https://api.hyphabase.com/api/modules/clients \
  -u sk_live_xxx: -H "X-Location-Id: $HYPHABASE_LOCATION_ID" \
  --data-urlencode "only_deleted=1"

only_deleted=1 is what you build a "recently deleted" screen from, which is the fastest way for Sarah's office to fix its own mistake without ringing anyone.

Undelete

POST /api/modules/clients/{id}/restore
curl -X POST https://api.hyphabase.com/api/modules/clients/$CUSTOMER_ID/restore \
  -u sk_live_xxx: -H "X-Location-Id: $HYPHABASE_LOCATION_ID"

They come back exactly as they were, with everything attached.

This route takes {id}, not {client}

Same id — but restore and force are the two routes in this module that use {id}, because the usual lookup can't find a deleted record. Only matters if you generate URLs from a template.

Delete for good

DELETE /api/modules/clients/{id}/force
curl -X DELETE https://api.hyphabase.com/api/modules/clients/$CUSTOMER_ID/force \
  -u sk_live_xxx: -H "X-Location-Id: $HYPHABASE_LOCATION_ID"

Gone. No undelete, no recovery through the API.

This one is not reversible

Reach for it when you have an actual reason — somebody exercising their right to be erased, or clearing up a test import. Not for tidiness: deleted customers are already invisible to normal reads, so "the list looks messy" isn't a reason.

If you're unsure, don't. A site's database can be restored from backup, but that's a support conversation rather than an endpoint.

Building a proper undo

Delete plus restore is enough to do this the good way:

async function deleteWithUndo(customer) {
  await fetch(`${BASE}/api/modules/clients/${customer.id}`, {
    method: 'DELETE',
    credentials: 'include',
    headers: {
      'X-XSRF-TOKEN': readCookie('XSRF-TOKEN'),
      'X-Location-Id': locationId,
    },
  });

  removeFromList(customer.id);

  showToast(`${customer.name} deleted`, {
    action: {
      label: 'Undo',
      onClick: async () => {
        await fetch(`${BASE}/api/modules/clients/${customer.id}/restore`, {
          method: 'POST',
          credentials: 'include',
          headers: {
            'X-XSRF-TOKEN': readCookie('XSRF-TOKEN'),
            'X-Location-Id': locationId,
          },
        });
        restoreToList(customer);
      },
    },
  });
}

Delete straight away and offer undo, rather than an "are you sure?" dialogue. The reversibility is what makes that safe, and it's a much better experience — one tap instead of two, every time, for the 99% of deletions that were intended.

What gets sent out

Deleting fires clients.client.deleted. Restoring fires clients.client.updated, not a dedicated restore event — so a system mirroring your customers should treat "updated" as "re-check this record" rather than assuming it still exists in the state it last saw.

What to grant

All three need clients.delete. There's no separate scope for permanent deletion, so if you want to withhold that, withhold clients.delete entirely and do deletions through a credential you control more tightly.

Next

On this page