Skip to content
No hardwareTurn the phones your people already carry into card machines. No terminal to buy or rent.Get started →
TapProofGet started

Developers

Integrate in a day.
Verify in an hour.

Signed requests in, signed events out. No SDK required on the server side — it is HTTP and an HMAC you already know how to check.

Event contract

What lands on your endpoint

Your LMS is the system of record; our stream is the narrative. When the two disagree, fetch the case — the snapshot is authoritative.

case.received
We accepted a case you pushed.
case.assigned
An eligible agent was assigned. Ineligible agents never reach this.
visit.notified
The customer was told who is coming, with the delivery receipt.
contact.blocked
An attempt was refused. Carries the reason code.
visit.completed
Outcome, geo-stamp and proof.
payment.collected
Absolute totalCollected and outstandingAfter — never deltas.
case.closed
Terminal, collected or unrecovered.

Append only

A record, not a report

Sequenced per account, signed over a timestamped body, replayable after an outage. A gap is visible rather than silent.

Verification

Check the signature

The timestamp is inside the MAC, so a captured payload cannot be replayed later. Tolerance is five minutes.

// headers
//   X-TapProof-Timestamp: 1804393800
//   X-TapProof-Signature: v1=<hex>
//   X-TapProof-Sequence:  42

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

// constant-time, always
const ok = crypto.timingSafeEqual(
  Buffer.from(signature),
  Buffer.from(expected),
);
Android

Take a payment

An ActivityResultContract, so you get process-death and configuration-change safety for free — which matters on the handsets a field fleet actually carries.

TapProof.initialize(this, TapProofConfig(
    baseUrl       = "https://api.tapproof.in",
    environment   = PRODUCTION,
    tokenProvider = { authStore.freshToken() },
    certificatePins = listOf("…"),   // required in prod
))

val collect = rememberLauncherForActivityResult(
    TapProofContract()
) { result -> handle(result) }

collect.launch(CollectRequest(amountPaise = 250_000))

Refusals are events too

You are told what did not happen, and why

Every blocked contact arrives with a reason code. Most platforms drop these on the floor; they are the half that proves your operation was run properly.

Guarantees

Four properties you can build against

01

Idempotent

Re-sending a case or re-launching a collect with the same reference never double-charges and never duplicates.
02

Sequenced

Per-licensee sequence numbers, so a gap in your ingest is visible rather than silent.
03

Absolute

Payment events carry totalCollected and outstandingAfter. Never accumulate deltas.
04

Replayable

At-least-once delivery with de-duplication, and replay by sequence after an outage.

Verification

The timestamp is inside the signature

Which means a payload captured off the wire cannot be replayed at you later. Tolerance is five minutes; anything older is rejected before it reaches your handler.

Compare in constant time. We publish working verification code rather than a description of it, because a description is where most webhook integrations go wrong.

HMAC-SHA256 over timestamp + raw body

Run it yourself

A demo that needs no infrastructure

The whole flow — refusals included — runs on a laptop with no database, no Redis and no network. Point your integration engineer at it before a single call is scheduled.

$ node scripts/demo.js

02  Assignment — eligibility is checked before a name reaches the customer
    [--] FP-DEL-0912 refused — agent_certificate_expired
    [ok] FP-DEL-0417 assigned — IIBF valid, antecedents clear

04  The contact window is enforced, not reported on
    [--] 19:40 IST refused — outside_contact_window
    [--] one hour's notice refused — notice_less_than_one_day
    [ok] 12:00 IST, two days' notice — visit opens

08  Outcome returns as a signed, sequenced, verifiable event
    [ok] your endpoint verifies the signature
    [ok] tampered payload rejected — signature_mismatch
    [ok] replay an hour later rejected — timestamp_outside_tolerance