Docs / Getting started
Quickstart
Create a project, schedule a delayed job and a nightly cron schedule, and receive a signed webhook, in roughly ten minutes.
Last updated March 4, 2026
#Before you start
You need three things: a Tend account, an HTTPS endpoint that can receive a POST request, and a terminal. If you do not yet have a public endpoint, a tunneling tool pointed at a local server works fine for development. Tend will not deliver to plain http:// URLs in production projects, and it will not follow redirects, so give it the final address.
Create a project in the dashboard at tendcomputer.com. Every project comes with a development key beginning tnd_dev_ and a production key beginning tnd_live_. This guide uses the development key throughout, so nothing you do here can touch production data.
# Keep the key out of your shell history and source files
export TEND_API_KEY="tnd_dev_8fQ2mLx0aVt3"
# Install one SDK (all optional; curl works everywhere)
pip install tend
npm install @tend/sdk
go get github.com/tendcomputer/tend-go#Create a delayed job
A delayed job runs once. You provide a target URL, a payload, and either an absolute run_at timestamp or a relative delay_seconds. The example below asks Tend to call your endpoint 60 seconds from now with a small JSON payload. The Idempotency-Key header makes the request safe to retry: if your network drops after we accept the job, sending the same key and body within 24 hours returns the original job instead of creating a duplicate.
curl -X POST https://api.tendcomputer.com/v2/jobs \
-H "Authorization: Bearer $TEND_API_KEY" \
-H "Idempotency-Key: welcome-email-user-4821" \
-H "Content-Type: application/json" \
-d '{
"url": "https://app.example-shop.dev/hooks/send-welcome",
"delay_seconds": 60,
"payload": { "user_id": 4821, "template": "welcome_v3" },
"retry": { "max_attempts": 5 }
}'import os
from tend import Tend
client = Tend(api_key=os.environ["TEND_API_KEY"])
job = client.jobs.create(
url="https://app.example-shop.dev/hooks/send-welcome",
delay_seconds=60,
payload={"user_id": 4821, "template": "welcome_v3"},
retry={"max_attempts": 5},
idempotency_key="welcome-email-user-4821",
)
print(job.id, job.next_run_at)import { Tend } from "@tend/sdk";
const client = new Tend({ apiKey: process.env.TEND_API_KEY });
const job = await client.jobs.create(
{
url: "https://app.example-shop.dev/hooks/send-welcome",
delay_seconds: 60,
payload: { user_id: 4821, template: "welcome_v3" },
retry: { max_attempts: 5 },
},
{ idempotencyKey: "welcome-email-user-4821" }
);
console.log(job.id, job.next_run_at);job, err := client.Jobs.Create(ctx, &tend.JobParams{
URL: "https://app.example-shop.dev/hooks/send-welcome",
DelaySeconds: 60,
Payload: map[string]any{"user_id": 4821, "template": "welcome_v3"},
Retry: &tend.RetryParams{MaxAttempts: 5},
IdempotencyKey: "welcome-email-user-4821",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(job.ID, job.NextRunAt)A successful call returns 201 Created with the stored job. Note the status of scheduled and the computed next_run_at.
{
"id": "job_01J8Q4ZC7N3XWD5B9E2KHRT6VA",
"status": "scheduled",
"url": "https://app.example-shop.dev/hooks/send-welcome",
"region": "us-east",
"payload": { "user_id": 4821, "template": "welcome_v3" },
"retry": { "max_attempts": 5, "base_seconds": 10, "cap_seconds": 3600 },
"next_run_at": "2026-03-04T15:22:41Z",
"created_at": "2026-03-04T15:21:41Z"
}#Receive the result
When the job fires, Tend sends a POST to your URL. Your handler should verify the Tend-Signature header, do the work, and return a 2xx status within 15 seconds. That timeout is the webhook timeout: a slower response counts as a failed attempt and the run is retried. If the work takes longer, accept the request, respond 202, and finish in the background.
Signature verification is one line in each SDK. The signing secret for your project is shown once in the dashboard under Webhooks, and always begins with whsec_.
from flask import Flask, request
from tend import Tend, SignatureError
app = Flask(__name__)
@app.post("/hooks/send-welcome")
def send_welcome():
try:
event = Tend.webhooks.verify(
payload=request.get_data(),
signature=request.headers["Tend-Signature"],
secret="whsec_9d1f3c7a2b",
)
except SignatureError:
return "bad signature", 400
user_id = event.payload["user_id"]
# ...send the email, keyed on event.run_id to stay idempotent...
return "", 204import express from "express";
import { Tend } from "@tend/sdk";
const app = express();
app.post("/hooks/send-welcome", express.raw({ type: "*/*" }), (req, res) => {
let event;
try {
event = Tend.webhooks.verify({
payload: req.body,
signature: req.get("Tend-Signature"),
secret: "whsec_9d1f3c7a2b",
});
} catch {
return res.status(400).send("bad signature");
}
// ...send the email, keyed on event.run_id to stay idempotent...
res.status(204).end();
});#Add a cron schedule
A schedule is a recurring job definition. It needs a unique name, a cron expression, an IANA timezone, and the same url and payload fields a job takes. The schedule below runs a report at 02:00 every day in Denver time. Cron expressions use five fields (minute, hour, day of month, month, day of week), and daylight-saving transitions are handled by the timezone, not by you.
curl -X POST https://api.tendcomputer.com/v2/schedules \
-H "Authorization: Bearer $TEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "nightly-usage-report",
"cron": "0 2 * * *",
"timezone": "America/Denver",
"url": "https://app.example-shop.dev/hooks/usage-report",
"payload": { "window": "24h" }
}'schedule = client.schedules.create(
name="nightly-usage-report",
cron="0 2 * * *",
timezone="America/Denver",
url="https://app.example-shop.dev/hooks/usage-report",
payload={"window": "24h"},
)
print(schedule.id, schedule.next_run_at)const schedule = await client.schedules.create({
name: "nightly-usage-report",
cron: "0 2 * * *",
timezone: "America/Denver",
url: "https://app.example-shop.dev/hooks/usage-report",
payload: { window: "24h" },
});
console.log(schedule.id, schedule.next_run_at);If the expression is malformed or the timezone is not a real IANA zone, you receive 400 invalid_cron_expression. If it would fire more often than every 30 seconds, you receive 422 interval_too_short. A second schedule with the same name in the same environment returns 409 schedule_name_taken.
#Verify and clean up
Confirm that your job ran by fetching its runs. A healthy result has one run in the succeeded state and one attempt.
curl "https://api.tendcomputer.com/v2/jobs/job_01J8Q4ZC7N3XWD5B9E2KHRT6VA/runs?limit=10" \
-H "Authorization: Bearer $TEND_API_KEY"{
"data": [
{
"id": "run_01J8Q52M0HB8T4YQF7C3ZPX9DE",
"job_id": "job_01J8Q4ZC7N3XWD5B9E2KHRT6VA",
"status": "succeeded",
"attempts": 1,
"started_at": "2026-03-04T15:22:41Z",
"finished_at": "2026-03-04T15:22:42Z",
"response_status": 204
}
],
"next_cursor": null
}To remove the sample schedule, delete it by ID. Deleting a schedule stops future runs but leaves existing run history in place until it ages out of your plan's log retention window.
curl -X DELETE https://api.tendcomputer.com/v2/schedules/sch_01J8Q5BN2VK6M1RHW3D0AYTC8F \
-H "Authorization: Bearer $TEND_API_KEY"