Buddy Punch

Code Examples

Copy-paste samples for the six most common flows, each in curl, Node.js (18+, built-in fetch), Python (requests), and C# (HttpClient).

Replace bp_live_xxxxxxxxxxxxxxxx with your API key. See the Getting Started guide for how to create one.

Convention: all samples assume the key is in an environment variable. Never hard-code keys in source files.


1. Authentication Ping

Check that your key works. GET /employee with limit=1 is a lightweight authenticated round-trip.

curl

curl -sS https://api2.buddypunch.com/employee?limit=1 \
  -H "Authorization: Bearer $BP_API_KEY"

Node.js

const res = await fetch('https://api2.buddypunch.com/employee?limit=1', {
  headers: { 'Authorization': `Bearer ${process.env.BP_API_KEY}` },
});
console.log(res.status, await res.json());

Python

import os, requests

res = requests.get(
    'https://api2.buddypunch.com/employee',
    headers={'Authorization': f'Bearer {os.environ["BP_API_KEY"]}'},
    params={'limit': 1},
)
print(res.status_code, res.json())

C#

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("BP_API_KEY"));

var res = await client.GetAsync("https://api2.buddypunch.com/employee?limit=1");
Console.WriteLine($"{(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");

2. List Employees (with Full Pagination)

Loop through every page until hasMore is false.

curl

cursor=""
while : ; do
  url="https://api2.buddypunch.com/employee?limit=100${cursor:+&startingAfter=$cursor}"
  page=$(curl -sS "$url" -H "Authorization: Bearer $BP_API_KEY")
  echo "$page" | jq '.data[]'
  [ "$(echo "$page" | jq -r '.hasMore')" = "true" ] || break
  cursor=$(echo "$page" | jq -r '.data[-1].id')
done

Node.js

async function listAllEmployees() {
  const all = [];
  let cursor = null;

  while (true) {
    const url = new URL('https://api2.buddypunch.com/employee');
    url.searchParams.set('limit', '100');
    if (cursor) url.searchParams.set('startingAfter', cursor);

    const res = await fetch(url, {
      headers: { 'Authorization': `Bearer ${process.env.BP_API_KEY}` },
    });
    const page = await res.json();
    all.push(...page.data);

    if (!page.hasMore) break;
    cursor = page.data[page.data.length - 1].id;
  }
  return all;
}

Python

import os
import requests

def list_all_employees():
    all_items = []
    cursor = None
    headers = {'Authorization': f'Bearer {os.environ["BP_API_KEY"]}'}

    while True:
        params = {'limit': 100}
        if cursor:
            params['startingAfter'] = cursor
        page = requests.get(
            'https://api2.buddypunch.com/employee',
            headers=headers, params=params,
        ).json()
        all_items.extend(page['data'])

        if not page['hasMore']:
            return all_items
        cursor = page['data'][-1]['id']

C#

using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;

async Task<List<JsonElement>> ListAllEmployeesAsync(HttpClient client)
{
    var all = new List<JsonElement>();
    int? cursor = null;

    while (true)
    {
        var url = $"https://api2.buddypunch.com/employee?limit=100" +
                  (cursor is int c ? $"&startingAfter={c}" : "");
        using var doc = JsonDocument.Parse(await client.GetStringAsync(url));
        var page = doc.RootElement;

        var data = page.GetProperty("data");
        foreach (var item in data.EnumerateArray())
            all.Add(item.Clone());

        if (!page.GetProperty("hasMore").GetBoolean()) return all;
        // [^1] = last element; avoids requiring System.Linq just for .Last()
        cursor = data[data.GetArrayLength() - 1].GetProperty("id").GetInt32();
    }
}

3. Create a Punch

Record a completed punch (start + end) with an idempotency key so a retry after a timeout never creates a duplicate.

curl

curl -X POST https://api2.buddypunch.com/punch \
  -H "Authorization: Bearer $BP_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "employeeId": 4521,
    "timeCardId": 5678,
    "punchInDatetime": "2026-03-17T08:00:00",
    "punchOutDatetime": "2026-03-17T17:00:00",
    "locationId": 301,
    "note": "Worked full shift"
  }'

Node.js

import { randomUUID } from 'node:crypto';

const res = await fetch('https://api2.buddypunch.com/punch', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.BP_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': randomUUID(),
  },
  body: JSON.stringify({
    employeeId: 4521,
    timeCardId: 5678,
    punchInDatetime: '2026-03-17T08:00:00',
    punchOutDatetime: '2026-03-17T17:00:00',
    locationId: 301,
    note: 'Worked full shift',
  }),
});
console.log(res.status, await res.json());

Python

import os
import uuid
import requests

res = requests.post(
    'https://api2.buddypunch.com/punch',
    headers={
        'Authorization': f'Bearer {os.environ["BP_API_KEY"]}',
        'Content-Type': 'application/json',
        'Idempotency-Key': str(uuid.uuid4()),
    },
    json={
        'employeeId': 4521,
        'timeCardId': 5678,
        'punchInDatetime': '2026-03-17T08:00:00',
        'punchOutDatetime': '2026-03-17T17:00:00',
        'locationId': 301,
        'note': 'Worked full shift',
    },
)

C#

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Threading.Tasks;

using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("BP_API_KEY"));

var req = new HttpRequestMessage(HttpMethod.Post, "https://api2.buddypunch.com/punch")
{
    Content = JsonContent.Create(new {
        employeeId = 4521,
        timeCardId = 5678,
        punchInDatetime = "2026-03-17T08:00:00",
        punchOutDatetime = "2026-03-17T17:00:00",
        locationId = 301,
        note = "Worked full shift",
    }),
};
req.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
var res = await client.SendAsync(req);

Omit punchOutDatetime to create an open punch that can be closed later via PUT /punch/{id}/out.


4. Create a Time Off Request

curl

curl -X POST https://api2.buddypunch.com/timeOff \
  -H "Authorization: Bearer $BP_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "employeeId": 4521,
    "ptoHoursTuples": [
      {
        "date": "2026-04-10",
        "endDate": "2026-04-10",
        "ptoEarningCodeId": 12,
        "hours": 8.0,
        "note": "Family trip"
      }
    ],
    "isDateRange": false
  }'

Node.js

import { randomUUID } from 'node:crypto';

const res = await fetch('https://api2.buddypunch.com/timeOff', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.BP_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': randomUUID(),
  },
  body: JSON.stringify({
    employeeId: 4521,
    ptoHoursTuples: [{
      date: '2026-04-10',
      endDate: '2026-04-10',
      ptoEarningCodeId: 12,
      hours: 8.0,
      note: 'Family trip',
    }],
    isDateRange: false,
  }),
});

Python

import os
import uuid
import requests

res = requests.post(
    'https://api2.buddypunch.com/timeOff',
    headers={
        'Authorization': f'Bearer {os.environ["BP_API_KEY"]}',
        'Content-Type': 'application/json',
        'Idempotency-Key': str(uuid.uuid4()),
    },
    json={
        'employeeId': 4521,
        'ptoHoursTuples': [{
            'date': '2026-04-10',
            'endDate': '2026-04-10',
            'ptoEarningCodeId': 12,
            'hours': 8.0,
            'note': 'Family trip',
        }],
        'isDateRange': False,
    },
)

C#

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Threading.Tasks;

using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("BP_API_KEY"));

var req = new HttpRequestMessage(HttpMethod.Post, "https://api2.buddypunch.com/timeOff")
{
    Content = JsonContent.Create(new {
        employeeId = 4521,
        ptoHoursTuples = new[] {
            new {
                date = "2026-04-10",
                endDate = "2026-04-10",
                ptoEarningCodeId = 12,
                hours = 8.0,
                note = "Family trip",
            }
        },
        isDateRange = false,
    }),
};
req.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
var res = await client.SendAsync(req);

5. List Webhook Endpoints

curl

curl -sS https://api2.buddypunch.com/webhookEndpoint \
  -H "Authorization: Bearer $BP_API_KEY"

Node.js

const res = await fetch('https://api2.buddypunch.com/webhookEndpoint', {
  headers: { 'Authorization': `Bearer ${process.env.BP_API_KEY}` },
});
const { data } = await res.json();
console.log(`${data.length} endpoints registered`);

Python

import os
import requests

res = requests.get(
    'https://api2.buddypunch.com/webhookEndpoint',
    headers={'Authorization': f'Bearer {os.environ["BP_API_KEY"]}'},
)
for endpoint in res.json()['data']:
    print(endpoint['id'], endpoint['url'], endpoint['status'])

C#

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;

using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("BP_API_KEY"));

var res = await client.GetAsync("https://api2.buddypunch.com/webhookEndpoint");
using var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
foreach (var endpoint in doc.RootElement.GetProperty("data").EnumerateArray())
    Console.WriteLine($"{endpoint.GetProperty("id")} {endpoint.GetProperty("url")}");

6. Verify a Webhook Signature

Every webhook POST includes an X-BuddyPunch-Signature header of the form t=<unix_timestamp>,v1=<hmac_hex>. The signed payload is "<timestamp>.<raw_body>".

Reject requests where the timestamp is older than five minutes (replay protection). Details and event payload shapes are in the Webhooks guide.

Node.js

import crypto from 'node:crypto';

function verifyWebhook(rawBody, headerSignature, secret) {
  if (!headerSignature) return false;

  const parts = Object.fromEntries(
    headerSignature.split(',').map(p => {
      const [key, value] = p.split('=', 2);
      return [key?.trim(), value?.trim()];
    })
  );
  const timestamp = parts.t;
  const provided  = parts.v1;
  if (!timestamp || !provided) return false;

  const ts = Number(timestamp);
  if (!Number.isFinite(ts)) return false;

  // Replay window: reject if > 5 minutes old (or far in the future).
  if (Math.abs(Math.floor(Date.now() / 1000) - ts) > 300) return false;

  const signed   = `${timestamp}.${rawBody}`;
  const expected = crypto.createHmac('sha256', secret).update(signed).digest('hex');

  const expectedBuf = Buffer.from(expected);
  const providedBuf = Buffer.from(provided);
  // timingSafeEqual throws on length mismatch — check first so a malformed
  // header returns false instead of a 500.
  if (expectedBuf.length !== providedBuf.length) return false;

  return crypto.timingSafeEqual(expectedBuf, providedBuf);
}

Python

import hmac, hashlib, time

def verify_webhook(raw_body: bytes, header_signature: str, secret: str) -> bool:
    if not header_signature:
        return False

    # Tolerant parser: trim every key/value, ignore malformed segments with no '='.
    parts = {}
    for segment in header_signature.split(','):
        if '=' not in segment:
            continue
        key, value = segment.split('=', 1)
        parts[key.strip()] = value.strip()

    timestamp = parts.get('t')
    provided  = parts.get('v1')
    if not timestamp or not provided:
        return False

    try:
        ts = int(timestamp)
    except ValueError:
        return False

    # Replay window: reject if > 5 minutes off in either direction.
    if abs(int(time.time()) - ts) > 300:
        return False

    signed   = f"{timestamp}.{raw_body.decode()}"
    expected = hmac.new(secret.encode(), signed.encode(), hashlib.sha256).hexdigest()
    # compare_digest is length-safe: returns False on length mismatch, no exception.
    return hmac.compare_digest(expected, provided)

C#

using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;

bool VerifyWebhook(string rawBody, string headerSignature, string secret)
{
    if (string.IsNullOrWhiteSpace(headerSignature)) return false;

    // Tolerant parser: trim keys/values, skip malformed segments (missing '=',
    // more than one '=' in a value is fine because Split has count=2).
    var parts = new Dictionary<string, string>(StringComparer.Ordinal);
    foreach (var segment in headerSignature.Split(','))
    {
        var kv = segment.Split('=', 2);
        if (kv.Length != 2) continue;
        parts[kv[0].Trim()] = kv[1].Trim();
    }

    if (!parts.TryGetValue("t", out var timestampStr) ||
        !parts.TryGetValue("v1", out var provided) ||
        !long.TryParse(timestampStr, out var timestamp))
    {
        return false;
    }

    // Replay window: reject if > 5 minutes off in either direction.
    if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - timestamp) > 300) return false;

    var signed = $"{timestamp}.{rawBody}";
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    // ToLowerInvariant (not ToLower) — lowercase-of-hex-digits must be
    // culture-independent, otherwise Turkish-locale hosts compute a different
    // string for the same bytes and every signature fails to verify.
    var expected = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(signed))).ToLowerInvariant();

    // FixedTimeEquals is length-safe: returns false on length mismatch, no exception.
    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(expected),
        Encoding.UTF8.GetBytes(provided));
}