GuidesRun payroll with no code

Run payroll with no code

For a business that runs payroll from a spreadsheet, or wants Zapier/Make/n8n to trigger it, without writing an integration or minting an API key.

This is not the merchant API-key surface the rest of these docs cover — there’s no Authorization: Bearer sxm_... header here, no scopes. A payroll connector is its own thing: a webhook URL and a signing secret, both created from the business dashboard, that your own no-code automation POSTs signed rows to.

Why this is separate from the API

The Payroll API (/v1/api/payroll/*) is for a business with its own engineers — VoxHR and My-Watchman both integrate against it directly. A connector exists for everyone else: a business owner who has a Google Sheet, or a Zapier account, and no developer. Creating a connector is the one interactive step (PIN-approved, funding wallet + spending caps set once); every trigger after that runs headlessly, bounded by those caps — nobody has to click “approve” each payday.

1. Create a connector

From the business dashboard: Settings → Developers → Payroll connectors → Add connector. Name it, pick USD or LRD, set a per-run cap and a daily cap, and confirm with your PIN.

You’ll see, once:

  • A trigger URL — https://api.soxara.com/v1/connectors/payroll/{connector_id}/trigger
  • A signing secret — store it in whatever automation tool you’re using (Zapier’s a secret storage step, Google Apps Script’s PropertiesService, an n8n credential)

Neither is shown again. If you lose the secret, revoke the connector and create a new one.

2. Sign and send a trigger

Every trigger is a POST to your connector’s URL with a JSON body and a signature header. The signature scheme is exactly Soxara’s own outbound webhook scheme (Signature verification), just reversed — you’re the one signing this time:

X-Connector-Signature: t=<unix seconds>,v1=<hex hmac-sha256>
signed payload = "{timestamp}." + raw_json_body

Body shape:

{
  "idempotency_key": "payroll-2026-09-30",   // your own key — a retry with the same one is a no-op
  "note": "September payroll",                // optional
  "payments": [
    { "payee_phone": "+231770000001", "payee_label": "Adama Smith", "amount_cents": 150000 },
    { "payee_phone": "+231770000002", "payee_label": "John Boakai", "amount_cents": 120000, "external_ref": "row-2" }
  ]
}

Node example (works as-is inside a Google Apps Script or a small Zapier “Code by Zapier” step):

const crypto = require('crypto');
 
function triggerPayroll(secret, connectorUrl, payload) {
  const body = JSON.stringify(payload);
  const t = Math.floor(Date.now() / 1000);
  const sig = crypto.createHmac('sha256', secret).update(`${t}.${body}`).digest('hex');
 
  return fetch(connectorUrl, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Connector-Signature': `t=${t},v1=${sig}`,
    },
    body,
  });
}

What happens

The response comes back synchronously with the run’s status:

{
  "success": true,
  "data": {
    "id": "…",
    "status": "completed",       // completed | partial | failed | rejected
    "payroll_run_id": "…",
    "total_cents": 270000,
    "received_at": "…"
  }
}
  • completed — every payee was paid.
  • partial — some payees were paid; others need attention (usually a phone number that isn’t on Soxara yet) — same per-line detail as the Payroll API.
  • failed — nothing was paid.
  • rejected — nothing ran at all: bad signature, a paused/revoked connector, or the trigger’s total exceeded the connector’s per-run or daily cap. Fix the trigger (or the connector’s caps from the dashboard) and resend with a fresh idempotency_key.

A payee_phone that doesn’t match an existing Soxara user is never sent an invite — it’s just marked needs_attention, the same posture as every other phone-resolution path in Soxara.

Pausing and revoking

Pause from the dashboard stops triggers from running without deleting anything — resume later and it picks back up. Revoke is permanent; the URL stops accepting requests immediately. Neither needs a PIN — only creating a connector (raising its authority) does.

Retrying safely

Reuse the same idempotency_key for a genuine retry (your automation crashed before it saw the response, a network blip, etc.) — a replay of one already processed returns the original result without running payroll twice. Change the key only when the payment rows actually changed.