Saplic applications: email and webhook

Jobs that reach Saplic through a feed receive their applications on Saplic: the candidate never leaves the site. Each application is forwarded to the source:

  • By email, to the application address of the job (if the feed provides one) or, otherwise, to the application address registered for the source.
  • By webhook, if the source registered an https:// URL.

Both carry the same data. Only candidates who applied to a job from that source are sent.

The webhook

A POST to the registered URL, with Content-Type: application/json; charset=utf-8.

Header Content
X-Saplic-Event application.created (an application) or test.ping (a test).
X-Saplic-Delivery Delivery identifier. It is the same on every retry of an application.
X-Saplic-Timestamp Time of sending, in Unix seconds.
X-Saplic-Signature HMAC-SHA256(secret, timestamp + "." + body), in lowercase hexadecimal.
  • https only. Saplic does not follow redirects: a 3xx response counts as a failure.
  • Maximum response time: 10 seconds.
  • Any 2xx response counts as delivered. The response body is ignored.

Body of application.created

{
  "event": "application.created",
  "delivery_id": "4f6c1d0a9b7e4c2f8a1d3e5b7c9f0a2b",
  "created_at": "2026-10-07T15:04:05+00:00",
  "source": "saplic",
  "job": {
    "reference": "ACME-2026-0412",
    "title": "Marketing Coordinator",
    "url": "https://acmefoods.example/careers/marketing-coordinator",
    "saplic_url": "https://saplic.com/oferta.php?id=41234"
  },
  "application": {
    "applied_at": "2026-10-07T15:03:58+00:00",
    "answers": [
      { "question": "Do you have a valid work permit?", "answer": "Yes" }
    ]
  },
  "candidate": {
    "name": "Ana López",
    "email": "[email protected]",
    "phone": "+503 70001234",
    "country": "El Salvador",
    "city": "San Salvador",
    "experience": [
      { "title": "Marketing Analyst", "company": "Globex", "from": "2022-03", "to": "", "current": true }
    ],
    "education": [
      { "title": "Marketing", "school": "Universidad Centroamericana", "level": "Universitario" }
    ],
    "cv_url": "https://saplic.com/feed-cv.php?t=…",
    "cv_url_expires_in_hours": 72
  }
}

job.reference is the referencenumber the source sent in its feed. cv_url is null if the candidate did not upload a CV file.

Body of test.ping

{
  "event": "test.ping",
  "delivery_id": "…",
  "created_at": "2026-10-07T15:04:05+00:00",
  "source": "saplic",
  "message": "Evento de prueba de Saplic. No corresponde a ninguna postulación."
}

It is sent from the dashboard to check the connection and the signature. It carries no candidate data.

How to verify the signature

The secret is a string of 64 hexadecimal characters that Saplic gives the source. It is used as is, as text (do not convert it to bytes).

  1. Read the raw body, byte for byte, before parsing it as JSON.
  2. Compute HMAC-SHA256(secret, X-Saplic-Timestamp + "." + body) in hexadecimal.
  3. Compare it with X-Saplic-Signature using a constant-time comparison.
  4. Reject the request if X-Saplic-Timestamp differs from the server time by more than 5 minutes.
  5. Reject (or answer 200 without processing) if X-Saplic-Delivery was already received: it is a retry of something already processed.

Steps 4 and 5 are what prevent someone who captures a delivery from sending it again.

PHP:

$body      = file_get_contents('php://input');
$ts        = $_SERVER['HTTP_X_SAPLIC_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_SAPLIC_SIGNATURE'] ?? '';
$ok = ctype_digit($ts)
   && abs(time() - (int)$ts) <= 300
   && hash_equals(hash_hmac('sha256', $ts . '.' . $body, $secret), $signature);
if (!$ok) { http_response_code(401); exit; }
// … discard if $_SERVER['HTTP_X_SAPLIC_DELIVERY'] was already processed …
http_response_code(200);

Python:

import hmac, hashlib, time

def valid_signature(secret: str, timestamp: str, body: bytes, signature: str) -> bool:
    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
        return False
    expected = hmac.new(secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

Node.js:

const crypto = require('crypto');

function validSignature(secret, timestamp, body /* Buffer */, signature) {
  if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const expected = crypto.createHmac('sha256', secret).update(timestamp + '.').update(body).digest('hex');
  return expected.length === signature.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

Retries

If the email does not go out or the webhook does not answer 2xx within 10 seconds, Saplic retries after 5 minutes, 30 minutes and 2 hours. If the last retry also fails, the delivery is marked as failed and the Saplic team is alerted; the application stays stored and can be resent by hand.

Each webhook retry carries a new X-Saplic-Timestamp and signature, and the same X-Saplic-Delivery.

The CV link

The CV is not attached: it is sent as a link (cv_url in the webhook, a button in the email).

  • It expires after 72 hours.
  • It is signed and shows no identifiers: it cannot be modified, and another one cannot be guessed.
  • It stops working if the candidate withdraws the application or deletes the account.
  • Once expired, the page explains how to request a new one.

Download the file when you receive the application rather than storing the link.

The email

Subject: New application: {job title} — via Saplic, in the language the source was registered with (Spanish, English, Portuguese or French). It carries the job reference, the candidate’s details (name, email, phone, country, city, experience, education and the answers to the job’s questions) and the CV link.

Changing the secret

The secret can be regenerated from the source dashboard on Saplic at any time. The previous one stops working immediately, so it must be updated on the source side at the same moment.