Worker Recipes

Recipe: app approval

A signing worker for a provider that sends a push notification to the signer's phone, where they approve the signature in the provider's app, using the poll channel

Private beta. Qualified signatures are available on request. Request access and we enable them for your account. SWP/1 is in beta and may change.

This recipe fits a provider that:

  • already knows the signer, who installed its app and enrolled earlier,
  • keeps a long-term certificate for them, and
  • asks for approval with a push notification: the signer opens the app, checks what they sign and confirms with a PIN or biometrics.

The signer never leaves the ceremony. Your worker answers authorize with poll_after instead of a URL, and SignatureAPI asks again until the signer has decided. Not sure this is your provider? See Choose your provider’s model.

The flow

sequenceDiagram
    participant B as Signer's browser
    participant P as SignatureAPI
    participant W as Your worker
    participant Q as Provider
    participant A as Signer's phone
    B->>P: Finish the ceremony
    P->>W: POST /v1/credential
    W->>Q: Look up the signer's certificate
    W-->>P: completed (certificates, key_algo_oids)
    Note over P: Compute the digest
    P->>W: POST /v1/authorize (digest, label)
    W->>Q: Start an approval request
    Q->>A: Push notification
    W-->>P: pending (poll_after: 3)
    P-->>B: Waiting for your confirmation
    loop Every poll_after seconds
        P->>W: POST /v1/authorize (same body)
        W->>Q: Check the request
        W-->>P: pending (poll_after: 3)
    end
    A->>Q: Signer confirms with a PIN
    P->>W: POST /v1/authorize (same body)
    W->>Q: Check the request
    Q-->>W: Approved
    W-->>P: completed (authorization reference)
    P->>W: POST /v1/sign
    W->>Q: Sign the hash
    W-->>P: completed (signatures)
    P-->>B: Finalizing your signature…

1. credential: look up the certificate

The provider already has the signer’s certificate. Find it, and answer completed with the chain and key_algo_oids, as in the CSC recipe. If your provider returns a complete CMS, add "signature_format": "cms" instead. The polling below works the same in both formats.

If the signer isn’t enrolled, answer failed with credential_unavailable, and tell them in error_description what to do, for example which app to install.

2. authorize: push once, then report

A poll repeats the call, not the act. SignatureAPI sends the same body, with the same Idempotency-Key, every time. Your worker must start the approval once, then report on it.

async function authorize(body) {
    const sessionId = body.session.id;
    let request = await store.get(`approval:${sessionId}`);

    if (!request) {
        // First call: start the approval, which sends the push
        request = await provider.startApproval({
            user: await userFor(body.signer),
            hash: body.digests[0].value,
            text: `Sign "${body.digests[0].label}"`,
        });
        await store.set(`approval:${sessionId}`, request);
    }

    const state = await provider.getApproval(request.id); // Every later call: check, never push again
    switch (state.status) {
        case "approved":
            return { status: "completed", authorization: { reference: state.sad } };
        case "declined":
            return { status: "failed", error: "access_denied", error_description: "You declined the signature in the app." };
        case "expired":
            return { status: "failed", error: "authorization_expired", error_description: "The request in the app expired." };
        default:
            return { status: "pending", poll_after: 3, expires_at: request.expiresAt };
    }
}

Run this inside your idempotency handler. It stores only final outcomes, so each poll reaches your code again while the answer is pending.

  • poll_after is the wait in seconds. SignatureAPI keeps it between 1 and 60. A few seconds keeps the ceremony responsive.
  • expires_at is when the provider gives up on the request. It can shorten the connection’s Authorization timeout, never extend it. When the deadline passes, the session fails with authorization window expired.
  • The label is the envelope title. Show it in the app, so the signer knows what they confirm.

In CSC terms, this is an out-of-band authorization: credentials/authorize answers 202 while the signer decides, and credentials/authorizeCheck reports the result. Map 202 to pending and the final result to completed or failed.

What the signer sees

With poll_after and no action_url, the ceremony shows Waiting for your confirmation and asks the signer to confirm in their provider’s app or device. It checks with SignatureAPI on its own, and moves on as soon as your worker answers completed.

Offer a page too

Some signers start on a laptop and need a QR code to find the request on their phone. Answer with both channels:

{
    "status": "pending",
    "action_url": "https://signer.example.com/qr?ticket=6b0e...",
    "poll_after": 3,
    "expires_at": "2026-08-24T16:45:00Z"
}

The ceremony then shows Authorize your signature and sends the browser to your page. SignatureAPI keeps polling at the same time. The first channel to finish wins:

  • The poll finishes first. Still redirect your page to callback_url with a code, so the signer gets back to the ceremony. SignatureAPI sees the step is already done and only takes them back.
  • The redirect finishes first. SignatureAPI calls authorize with callback.code, and stops polling.

3. sign

Spend the approval and sign, as in the CSC recipe, or return the CMS for cms. sign never answers pending.

Test it

  • Approve on the phone. The ceremony moves on by itself, without a click.
  • Decline on the phone. The ceremony shows your error_description and Try again.
  • Ignore the push. The session fails when the deadline passes.
  • Count the pushes. A slow signer gets one notification, however many polls arrive.