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_afteris the wait in seconds. SignatureAPI keeps it between 1 and 60. A few seconds keeps the ceremony responsive.expires_atis 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 withauthorization window expired.- The
labelis 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_urlwith acode, 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
authorizewithcallback.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_descriptionand 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.