Shoninshonin

Overview

Markdown

Shonin is an open source, human-in-the-loop approval API. Send an approval request to any email address and wait for a human decision before your automation continues. Approvers need no account: they just click a link.

Base URL https://shonin.dev/api/v1

All requests must include an Authorization: Bearer <api_key> header, except the /v1/decide/:token links in approval emails, which approvers open and which need no key.

Shonin is MIT licensed. Try the hosted demo at shonin.dev, or self-host it.

Quickstart

Markdown

This quickstart uses the hosted demo, which is capped and meant for trying Shonin out. For real use, self-host Shonin.

  1. Sign in with your email at /login. Your API key is on your dashboard and is emailed to you.

  2. Send an approval request. Use your own email as the approver: the hosted demo only sends to your account email.

    bash
    curl -X POST https://shonin.dev/api/v1/approvals \
      -H "Authorization: Bearer sk_your_api_key" \
      -H "Content-Type: application/json" \
      -d '{
        "action": "Deploy to production",
        "approver_email": "you@example.com"
      }'
  3. Open the email and click Approve, then read the result with the id from the response:

    bash
    curl https://shonin.dev/api/v1/approvals/APPROVAL_ID \
      -H "Authorization: Bearer sk_your_api_key"

The status field changes from pending to approved or rejected.

Authentication

Markdown

Pass your API key as a Bearer token in the Authorization header on every request. Sign in at /login to see your key.

bash
curl -H "Authorization: Bearer sk_your_api_key" \
  https://shonin.dev/api/v1/approvals/APPROVAL_ID

Keep your API key secret. Do not expose it in client-side code or public repositories.

Create Approval

POST/v1/approvalsMarkdown

Creates a new approval request and sends an email to the approver with Approve and Reject buttons. Returns immediately. The approval stays pending until the approver decides.

Request body

NameTypeRequiredDescription
actionstringrequiredA short description of what needs approval. Shown prominently in the email.
approver_emailstringrequiredThe email address of the person who will approve or reject.
contextstringoptionalOptional extra context displayed in the email below the action.
webhook_urlstringoptionalPublic https URL to POST the decision to when the approver clicks Approve or Reject. Private and reserved addresses are rejected with a 400.
expires_in_hoursnumberoptionalHow many hours before the approval link expires. Defaults to 24.
command_typestringoptionalWhat kind of action this is, for example git_push_force or sql_drop. Drives the risk banner in the email. Inferred from action when omitted. See Risk Levels.
filesarrayoptionalFiles the action touches, for example [{ "path": "app/route.ts", "status": "modified" }]. Status is modified, added, deleted or renamed.
diffstringoptionalA diff to show the approver. Truncated at 50KB.

Example request

bash
curl -X POST https://shonin.dev/api/v1/approvals \
  -H "Authorization: Bearer sk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "Deploy to production",
    "approver_email": "cto@company.com",
    "context": "PR #247 merged, 3 files changed",
    "webhook_url": "https://yourapp.com/webhooks/shonin"
  }'

Example response

Status 201 Created:

json
{
  "id": "a1b2c3d4-...",
  "status": "pending",
  "created_at": "2026-03-21T18:00:00Z",
  "expires_at": "2026-03-22T18:00:00Z"
}

Get Approval

GET/v1/approvals/:idMarkdown

Returns the current state of an approval. Use this to poll for a decision if you are not using webhooks. Only approvals belonging to the authenticated account are returned.

Example request

bash
curl https://shonin.dev/api/v1/approvals/a1b2c3d4 \
  -H "Authorization: Bearer sk_your_api_key"

Example response

Status 200 OK:

json
{
  "id": "a1b2c3d4-...",
  "action": "git push origin main --force",
  "context": "Remote diverged after a rebase",
  "approver_email": "cto@company.com",
  "status": "approved",
  "webhook_url": null,
  "command_type": "git_push_force",
  "risk_level": "DESTRUCTIVE",
  "risk_bullets": [
    "Will overwrite upstream commits",
    "Bypasses branch protection rules"
  ],
  "files": [{ "path": "app/route.ts", "status": "modified" }],
  "expires_at": "2026-03-22T18:00:00Z",
  "decided_at": "2026-03-21T18:45:00Z",
  "created_at": "2026-03-21T18:00:00Z"
}

Risk Levels

Markdown

When an approval has a command_type, the email opens with a risk banner so the approver can see how reversible the action is. If you leave command_type out, Shonin infers it from action when it recognizes commands such as git push --force, git reset --hard, rm, drop table or a migration.

command_typeLevelShown to the approver
git_push_forceDESTRUCTIVEWill overwrite upstream commits. Bypasses branch protection rules.
git_reset_hardDESTRUCTIVELocal changes will be permanently lost.
rmDESTRUCTIVEFiles cannot be recovered from trash.
sql_dropDESTRUCTIVETable data is permanently deleted.
sql_migrationHIGHSchema changes may be irreversible.
git_pushLOWReversible via git revert.
git_commitLOWReversible via git reset.
anything elseLOWNo banner text.

DESTRUCTIVE approvals add a 3 second countdown on the confirm page before the Approve button unlocks.

Create Decision

POST/v1/decisionsMarkdown

Ask a multiple-choice question instead of a yes or no. The respondent gets an email with one button per option and picks one.

Request body

NameTypeRequiredDescription
questionstringrequiredThe question to ask.
optionsarrayrequired2 to 10 options, each { "key": "prod", "label": "Production" }. A key is up to 16 characters and a label up to 200.
respondent_emailstringrequiredThe email address of the person who will answer.
contextstringoptionalOptional extra context shown in the email.
webhook_urlstringoptionalPublic https URL to POST the answer to.
expires_in_hoursnumberoptionalHow many hours before the links expire. Defaults to 24.

Example request

bash
curl -X POST https://shonin.dev/api/v1/decisions \
  -H "Authorization: Bearer sk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "question": "Which environment should we deploy to?",
    "options": [
      { "key": "staging", "label": "Staging" },
      { "key": "prod", "label": "Production" }
    ],
    "respondent_email": "cto@company.com"
  }'

Example response

Status 201 Created:

json
{
  "id": "e5f6a7b8-...",
  "status": "pending",
  "created_at": "2026-03-21T18:00:00Z",
  "expires_at": "2026-03-22T18:00:00Z"
}

Get Decision

GET/v1/decisions/:idMarkdown

Returns the current state of a decision. status is pending or decided, and chosen_key holds the key of the option that was picked.

json
{
  "id": "e5f6a7b8-...",
  "status": "decided",
  "chosen_key": "prod",
  "decided_at": "2026-03-21T18:45:00Z",
  "expires_at": "2026-03-22T18:00:00Z",
  "created_at": "2026-03-21T18:00:00Z"
}

Webhooks

Markdown

Set webhook_url on an approval or a decision and Shonin sends a POST with a JSON body once the decision is recorded.

Approval payload

json
{
  "id": "a1b2c3d4-...",
  "status": "approved",
  "decided_at": "2026-03-21T18:45:00Z"
}

Decision payload

json
{
  "id": "e5f6a7b8-...",
  "status": "decided",
  "chosen_key": "prod",
  "decided_at": "2026-03-21T18:45:00Z"
}

Rules

  • The URL must be a public https address. Private and reserved addresses are rejected with a 400 when you create the request.
  • Shonin waits 5 seconds for a response, does not follow redirects, and sends each webhook once with no retries.

Webhook payloads are not signed. Treat one as a signal and confirm the result with a GET request before you act on it.

How Decisions Work

Markdown

You never call the decide endpoint yourself. It sits behind the links in the email, and it is built so the code that asks for approval cannot give it.

  • The approve and reject links exist only in the email to the approver. The API never returns them, so your code cannot approve its own request.
  • Opening a link never decides. It leads to a confirm page, and the decision is recorded only when the approver confirms, which sends a POST. Mail scanners and link previews that open every link cannot decide.
  • Links are single-use and expire after 24 hours unless you set expires_in_hours.

Errors

Markdown

All errors return a JSON object with an error field containing a human-readable message.

json
{ "error": "Invalid API key" }

Error codes

StatusMeaningCommon cause
400Validation errorA required field is missing, a value has the wrong type, or webhook_url is not a public https address.
401Invalid API keyMissing or incorrect Authorization header.
404Not foundThe ID does not exist or belongs to a different account.

Hosted demo only

The hosted demo is capped to protect a shared email allowance. Self-hosted instances do not apply these limits.

StatusMeaningCommon cause
403Recipient not allowedapprover_email or respondent_email is not the email on your account.
429Daily limit reachedYour key has used its requests for the day.
503Demo budget usedThe shared email budget for today is used up. Try again tomorrow.

Self-Hosting

Markdown

Shonin is MIT licensed and runs on Next.js, Supabase and Resend. A self-hosted instance has no request limits, and it serves this same documentation at /docs.

The full setup guide is in the README on GitHub: Self-hosting guide.