Developer API and Webhooks
Connect Sparko to Your Own Systems
Sparko gives your engineering team two building blocks for custom integrations: a read-only public API for pulling people, time-and-leave, and recruiting data on demand, and webhooks that push a signed notification to your endpoint the moment key events happen. Both are managed from Admin Settings and are designed to be safe by default: keys and secrets are shown once, every request is tenant-isolated, and outbound webhooks are cryptographically signed.
Overview
What you can build
- Pull data on a schedule — sync employees into a data warehouse, feed a BI dashboard, or reconcile a downstream system nightly using the public API.
- React to events in real time — kick off a provisioning workflow when someone is hired, notify payroll when a departure is recorded, or update an applicant-tracking mirror when a candidate changes stage.
Two surfaces, two auth models
| Surface | Direction | How it authenticates |
|---|---|---|
| Public API | Your system pulls from Sparko | API key in an X-API-Key header |
| Webhooks | Sparko pushes to your system | HMAC-SHA256 signature you verify with a signing secret |
Availability
The developer API and webhooks are a paid capability. They are available on paid plans; on the free plan the endpoints return 403. If you downgrade, existing keys stop authenticating, but you can still sign in to audit and revoke them.
Note: The public API is intentionally read-only. There is no write scope — the API can never create, edit, or delete records in your Sparko tenant.
Part 1 — The Public API
Base URL
https://api.sparko.app/api/integrate/v1
Creating an API key
API keys are created and managed under Admin Settings → Security & Compliance → API Keys.
- Go to Admin Settings → Security & Compliance → API Keys
- Click Create API Key
- Give the key a descriptive name (for example, "Data warehouse sync" or "Zapier integration")
- Choose the scopes the key should carry (see below) — pick only what the integration needs
- Click Create key
- Copy the key immediately. It starts with
spk_live_and is shown exactly once. Sparko stores only a one-way hash, so if you lose the key you must create a new one.
Security: Treat the key like a password. Store it in your secrets manager, never in source control or a browser. Only the key's short prefix (for example
spk_live_ab12cd…) is ever shown again, so you can recognize a key in the list without exposing the secret.
Authenticating requests
Send the key in an X-API-Key header:
curl https://api.sparko.app/api/integrate/v1/employees \
-H "X-API-Key: spk_live_your_key_here"
An Authorization: Bearer <key> header works too. Every request is resolved to your company only — the tenant is derived from the key itself, never from anything in the request, so a key can only ever read your own data.
Any authentication problem (missing key, revoked key, expired key) returns a generic 401 Unauthorized with no detail about which key or why. A valid key that lacks the scope an endpoint requires returns 403 Forbidden.
Scopes
A key carries one or more read-only scopes. Each endpoint requires a specific scope, so a key scoped to people.read can list employees but cannot touch recruiting data.
| Scope | Grants read access to |
|---|---|
people.read |
Employees and departments |
timeleave.read |
Leave requests and leave balances |
recruiting.read |
Jobs, applications, and candidate names |
Endpoints
All endpoints are GET only. List endpoints accept skip and limit query parameters for pagination (limit defaults to 50, maximum 200).
| Method | Path | Scope | Returns |
|---|---|---|---|
GET |
/employees |
people.read |
A page of employees |
GET |
/employees/{employee_id} |
people.read |
One employee |
GET |
/leave-requests |
timeleave.read |
A page of leave requests |
GET |
/leave-balances |
timeleave.read |
A page of leave balances |
GET |
/jobs |
recruiting.read |
A page of job postings |
GET |
/jobs/{job_id} |
recruiting.read |
One job posting (with full description) |
GET |
/applications |
recruiting.read |
A page of applications (candidate name + stage) |
What the API returns (and what it does not)
Responses are deliberately trimmed to non-sensitive fields. The API surfaces things like job title, department, employment status, dates, leave balances, and application stage. It never returns deep personal data — no email, phone number, date of birth, national ID, home address, salary, or demographic information. Employee and candidate names are the only decrypted personal fields, and only on the endpoints whose scope you granted.
Example — list employees:
curl "https://api.sparko.app/api/integrate/v1/employees?limit=2" \
-H "X-API-Key: spk_live_your_key_here"
{
"items": [
{
"employee_id": "emp_01H...",
"employee_number": "E-1042",
"first_name": "Emily",
"last_name": "Thomas",
"job_title": "Senior Engineer",
"department": "Engineering",
"employment_status": "active",
"manager_id": "emp_01G...",
"hire_date": "2023-04-01",
"termination_date": null
}
],
"skip": 0,
"limit": 2
}
Rate limits
The public API is limited to 300 requests per minute per key. Space out bulk syncs and use pagination rather than hammering an endpoint.
Rotating and revoking keys
- Rotate: Create a new key, update your integration to use it, then revoke the old one. There is no in-place edit — rotation is always "create new, retire old."
- Revoke: Click Revoke next to a key in the list. Revocation is immediate — the next request with that key gets a
401.
Part 2 — Webhooks
Webhooks let Sparko notify your systems the instant something happens, so you do not have to poll the API. When a subscribed event occurs, Sparko sends a signed POST to the URL you configured.
The events you can subscribe to
A subscription listens for one or more of these five events:
| Event | Fires when |
|---|---|
employee.hired |
A new employee is hired |
employee.terminated |
An employee is terminated / offboarded |
leave.approved |
A leave request is approved |
application.stage_changed |
A candidate moves to a new pipeline stage |
offer.accepted |
A candidate accepts an offer |
Creating a subscription
Webhooks are managed under Admin Settings → Security & Compliance → Webhooks.
- Go to Admin Settings → Security & Compliance → Webhooks
- Click Add webhook
- Enter your endpoint URL (must be
https://and publicly reachable) - Optionally add a short description
- Choose which events to receive
- Click Create webhook
- Copy the signing secret immediately. It starts with
whsec_and is shown exactly once. You will use it to verify that incoming webhooks genuinely came from Sparko.
URL safety: Sparko validates your endpoint URL when you save it and re-checks the resolved address again at delivery time. Endpoints that point at private, internal, or loopback addresses are rejected — webhooks can only be delivered to genuinely public destinations.
The webhook payload
Sparko sends a JSON envelope with a stable shape:
{
"id": "del_01H...",
"event": "leave.approved",
"created_at": "2026-07-27T14:03:11+00:00",
"data": {
"request_id": "req_01H...",
"employee_id": "emp_01H..."
}
}
Each delivery also carries these headers:
| Header | Purpose |
|---|---|
X-Sparko-Event |
The event name (for example leave.approved) |
X-Sparko-Delivery |
A unique delivery ID, useful for de-duplication |
X-Sparko-Signature |
The signature you verify (see below) |
Your endpoint should respond with any 2xx status code to acknowledge receipt. Anything else is treated as a failed delivery and retried.
Verifying the signature
Every webhook is signed so you can confirm it came from Sparko and was not tampered with or replayed. The X-Sparko-Signature header looks like this:
X-Sparko-Signature: t=1753624991,v1=4f2c...9ab
tis the Unix timestamp when we signed the request.v1is an HMAC-SHA256 hex digest computed over the string"{t}." + raw_request_body, keyed with your subscription's signing secret.
To verify, recompute the HMAC yourself and compare:
import hashlib
import hmac
import time
def verify(secret: str, header: str, raw_body: bytes, tolerance_seconds: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
timestamp, provided = parts.get("t"), parts.get("v1")
if not timestamp or not provided:
return False
# Reject stale deliveries to defend against replays.
if abs(int(time.time()) - int(timestamp)) > tolerance_seconds:
return False
signed = f"{timestamp}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, provided)
Two rules for a correct implementation:
- Sign the raw bytes. Compute the HMAC over the exact request body as received, before any JSON re-serialization. Re-encoding can change bytes and break the signature.
- Enforce the timestamp window. Reject signatures whose
tis more than a few minutes old (Sparko's own tolerance is 5 minutes) so an intercepted request cannot be replayed later.
Use a constant-time comparison (like hmac.compare_digest above) rather than ==.
Testing an endpoint
After you add a webhook, click Test on the subscription. Sparko sends a one-off signed test delivery so you can confirm your endpoint receives the request and verifies the signature correctly — before any real event depends on it.
Delivery, retries, and the dead-letter state
Sparko attempts each delivery immediately when the event fires. If your endpoint does not return a 2xx, the delivery is retried with exponential backoff:
| Attempt | Waits before retrying |
|---|---|
| 2nd | ~1 minute |
| 3rd | ~5 minutes |
| 4th | ~15 minutes |
| 5th | ~1 hour |
| (final) | ~6 hours |
After 5 failed attempts, the delivery is marked dead-letter and no longer retried automatically.
To inspect what happened, click Deliveries on a subscription. Each row shows the event, its status (delivered, failed, or dead_letter), the last HTTP status code your endpoint returned, the attempt count, and when it was sent. You can Redeliver any past delivery to send it again as a fresh attempt — handy after you fix an outage on your side.
Pausing and deleting
- Pause / Resume a subscription to temporarily stop deliveries without losing its configuration or signing secret.
- Delete a subscription to remove it and its delivery history. This cannot be undone.
Who can manage the API and webhooks
Access is controlled by permissions, so you decide which admins can see and change these settings:
| Permission | Allows |
|---|---|
admin.api_keys.view |
See the list of API keys (never the secrets) |
admin.api_keys.manage |
Create and revoke API keys |
admin.webhooks.view |
See webhook subscriptions and their delivery history |
admin.webhooks.manage |
Create, edit, test, redeliver, pause, and delete webhooks |
These are typically held by IT Admin and Super Admin roles. See Roles and Permissions for how to assign them.
Best Practices
✅ Least privilege: Scope each API key to only the data its integration needs
✅ One key per integration: So you can revoke one without breaking the others
✅ Store secrets safely: Keep API keys and signing secrets in a secrets manager, never in code or config committed to source control
✅ Always verify signatures: Reject any webhook whose signature or timestamp does not check out
✅ Make handlers idempotent: Use X-Sparko-Delivery to de-duplicate, since a retried delivery may arrive more than once
✅ Respond fast: Acknowledge with a 2xx quickly and do heavy work asynchronously, so a slow handler is not treated as a failure
✅ Rotate keys periodically: Create a new key, migrate, then revoke the old one
Troubleshooting
Q: My API request returns 401 Unauthorized
- The key is missing, revoked, expired, or mistyped. Confirm you are sending it in the
X-API-Keyheader and that the key is still Active in the list. If in doubt, create a fresh key.
Q: My API request returns 403 Forbidden
- Either your plan does not include API access, or the key is missing the scope the endpoint requires. Check the key's scopes in the list and create a new key with the right scope if needed.
Q: My API request returns 429 Too Many Requests
- You have exceeded 300 requests per minute. Slow down and paginate with
skip/limitinstead of many rapid calls.
Q: My webhook signature check keeps failing
- Verify you are computing the HMAC over the raw request body and the string
"{timestamp}." + body, using the exact signing secret shown when you created the subscription. Re-serializing the JSON before hashing is the most common cause.
Q: A webhook shows as dead_letter
- Your endpoint failed all retry attempts. Fix the endpoint, then use Redeliver to resend that delivery.
Q: I lost my API key or signing secret
- Secrets are shown only once and cannot be recovered. Create a new key (or a new webhook subscription) and update your integration.
Need Help?
- Email: [email protected]
- Documentation: https://sparko.app/docs
- Status: https://status.sparko.app
Applies To: Admins on paid Sparko plans Feature Tier: Developer API and webhooks are a paid capability