USE CASE

Large payments,
released by agreement.

Dual authorisation on money movement is an old control, and it works. What is usually missing is evidence that survives scrutiny — a database row saying two people approved is only as trustworthy as the database.

Where single approval still slips through

Most finance systems have dual authorisation for transfers initiated by a person. The gaps are usually elsewhere: the automated payout run that executes on a schedule with no human in the loop, the treasury operation performed directly through a provider's API by engineering, the emergency supplier payment that is handled over chat because the normal path is too slow.

Business email compromise attacks target exactly these gaps. The attacker does not defeat your approval workflow; they find the path that never had one.

Approvals you can prove afterwards

Approvals recorded as rows in an application database rely entirely on that application being correct and uncompromised. Anyone with write access to the table can create an approval that never happened, and the record cannot distinguish the two cases.

A TeamSigner approval is a secp256k1 signature over a canonical string committing to the amount, the recipient, the reason, and the expiry — made on the approver's phone by a key that exists nowhere else. Verification needs only the signature, the pre-image, and the public key. It does not need to trust our servers or yours, which is what makes it useful evidence in a dispute or an investigation.

// Release a payout batch only on a verified, in-date approval.
func handleRelease(w http.ResponseWriter, r *http.Request) {
    msg, err := teamsigner.Verify(r, secret, trustedPubkeys, 2)
    if err != nil {
        http.Error(w, "unauthorised", http.StatusUnauthorized)
        return
    }
    batch, err := parseBatchRef(msg.Message)
    if err != nil || batch.Total > maxAutoRelease {
        http.Error(w, "refused", http.StatusBadRequest)
        return
    }
    // Store the signed approval alongside the batch — this is the audit record.
    if err := payouts.Release(r.Context(), batch.ID, msg.Raw); err != nil {
        http.Error(w, "internal", http.StatusInternalServerError)
        return
    }
    w.WriteHeader(http.StatusOK)
}

Keep the raw delivery. It contains the exact signed bytes and every signature, so the approval stays verifiable years later.

Setting the threshold against the amount

Define separate actions by band rather than one action for all payments. A routine supplier run might need two approvals; anything above a material threshold needs three including a director. The bands are yours to choose, but keeping them explicit as separate actions means the control is visible in configuration rather than buried in application logic.

Common questions

Is TeamSigner a payments product?

No. It collects approvals and delivers a signed webhook. Your own systems move the money; TeamSigner never touches funds or payment credentials.

Can approvers see what they are approving?

They see the action name and the reason the initiator entered, which is where the amount and recipient reference belong. Both are covered by the signature.

How long is an approval valid?

Every request carries an expiry that is part of the signed message. Once it passes, the approval is no longer valid and your verifier should reject it.

Put a threshold on it.

Define the action, pick how many approvals it needs, point it at your webhook. Free while we're getting started.

Log in & get started

Integrating? The full webhook contract is in the docs.

Related

The four-eyes principle
Guide
Separation of duties
Guide
Break-glass access
Use case