Continuations
Continuations are library-owned resume artifacts for operations that paused
with GLN_BACKEND_OUTCOME_ACTION_REQUIRED. They are most commonly produced by
FinTS TAN, decoupled approval, and Verification of Payee flows.
How a Continuation Is Produced
Backend operations return a gln_backend_result_t envelope. When the envelope
outcome is GLN_BACKEND_OUTCOME_ACTION_REQUIRED, inspect the interrupt and take
the continuation before destroying the envelope:
gln_backend_result_t* result = NULL;
gln_status_t s = gln_submit_transfer(backend, &request, &result);
if (s != GLN_OK) {
return 1;
}
if (gln_get_backend_result_outcome(result) == GLN_BACKEND_OUTCOME_ACTION_REQUIRED) {
const gln_interrupt_info_t* interrupt = gln_get_backend_result_interrupt_info(result);
gln_continuation_t* continuation = gln_take_backend_result_continuation(result);
if (interrupt != NULL && interrupt->kind == GLN_INTERRUPT_TAN_REQUIRED) {
printf("%s\n", interrupt->tan.challenge ? interrupt->tan.challenge : "");
}
/* Save or resume continuation. */
gln_destroy_continuation(continuation);
}
gln_destroy_backend_result(result);
gln_take_backend_result_continuation transfers ownership. If you do not take
the continuation, gln_destroy_backend_result destroys it with the envelope.
Calling take a second time returns NULL.
Storage
Continuation stores let a process survive restart between the paused operation and the follow-up.
GLN_API gln_status_t GLN_CALL gln_save_continuation(
gln_continuation_store_t* in_store,
const char* in_id,
gln_continuation_t* in_continuation,
gln_error_t* out_error);
GLN_API gln_status_t GLN_CALL gln_load_continuation(
gln_continuation_store_t* in_store,
const char* in_id,
gln_continuation_t** out_continuation,
gln_error_t* out_error);
GLN_API void GLN_CALL gln_destroy_continuation(gln_continuation_t* in_continuation);
gln_save_continuation copies the artifact into the supplied store. It does
not destroy the in-memory handle; destroy the handle after a successful save
unless you are going to resume it immediately.
gln_load_continuation creates a new caller-owned handle. Destroy loaded
handles with gln_destroy_continuation.
in_id is an application-chosen key. Use IDs that are scoped by profile,
account, operation, and user where needed; the library treats the ID as an
opaque string.
Resume
Resume uses the same backend handle shape as other operations and returns a fresh backend result envelope:
GLN_API gln_status_t GLN_CALL gln_resume_continuation(
gln_backend_t* in_backend,
gln_continuation_t* in_continuation,
gln_secret_t* in_interactive_secret_or_null,
const gln_continuation_input_t* in_input,
gln_backend_result_t** out_result);
The backend, continuation, secret, and input are borrowed for the duration of the call. The old continuation remains caller-owned; destroy it after the resume attempt. If the resumed operation pauses again, take the next continuation from the returned envelope and replace the old persisted artifact.
gln_continuation_input_t selects the non-secret resume payload:
typedef enum {
GLN_CONTINUATION_INPUT_KIND_NONE = 0,
GLN_CONTINUATION_INPUT_KIND_DECOUPLED_POLL = 1,
GLN_CONTINUATION_INPUT_KIND_HHDUC_RESPONSE = 2,
GLN_CONTINUATION_INPUT_KIND_VOP_CONFIRM = 3
} gln_continuation_input_kind_t;
typedef struct {
uint32_t struct_size;
gln_continuation_input_kind_t kind;
const char* hhduc_response_or_null;
} gln_continuation_input_t;
#define gln_default_continuation_input \
{ sizeof(gln_continuation_input_t), GLN_CONTINUATION_INPUT_KIND_NONE, NULL }
Use these combinations:
| Pause | Resume input | Secret | Result status before resume |
|---|---|---|---|
| TAN challenge | GLN_CONTINUATION_INPUT_KIND_NONE, or GLN_CONTINUATION_INPUT_KIND_HHDUC_RESPONSE when an HHD_UC response is supplied | gln_secret_t* containing the TAN | GLN_ERR_TAN_REQUIRED |
| Decoupled approval poll | GLN_CONTINUATION_INPUT_KIND_DECOUPLED_POLL | NULL | GLN_ERR_DECOUPLED_PENDING |
| Decoupled-pending HHDuc echo/status variant | GLN_CONTINUATION_INPUT_KIND_HHDUC_RESPONSE | NULL | GLN_ERR_DECOUPLED_PENDING |
| Verification of Payee confirmation | GLN_CONTINUATION_INPUT_KIND_VOP_CONFIRM | NULL | GLN_ERR_VOP_CONFIRMATION_REQUIRED |
A TAN is passed through gln_secret_t so secret bytes do not cross the ABI as a
plain borrowed string. HHD_UC response text is not a secret and travels through
hhduc_response_or_null. in_input itself is required:
gln_default_continuation_input before a NULL interactive secret returns an error
envelope with GLN_ERR_INVALID_ARG.
For a VoP confirmation pause, the backend result has outcome
GLN_BACKEND_OUTCOME_ACTION_REQUIRED, status
GLN_ERR_VOP_CONFIRMATION_REQUIRED, result kind
GLN_BACKEND_RESULT_KIND_UNKNOWN, and interrupt kind
GLN_INTERRUPT_VOP_CONFIRMATION_REQUIRED. The active interrupt fields are
vop_confirm.vop_id_or_null, vop_confirm.result_code_or_null,
vop_confirm.alternate_name_or_null, and
vop_confirm.explanatory_text_or_null; they are borrowed from the result
envelope and remain valid until that envelope is destroyed. For a VoP
confirmation resume, set kind to GLN_CONTINUATION_INPUT_KIND_VOP_CONFIRM;
supported VoP confirmation resume paths treat that as confirmation of the VoP
id stored in the continuation.
Unsupported VoP confirmation resume paths return an error envelope with status
GLN_ERR_NOT_SUPPORTED and issue type unsupported_vop_confirmation_backend.
Resume Example
gln_error_t error = {0};
gln_default_error(&error);
gln_continuation_t* continuation = NULL;
gln_status_t s = gln_load_continuation(
continuation_store,
"profiles/alice/transfer.resume",
&continuation,
&error);
if (s != GLN_OK) {
fprintf(stderr, "%s\n", error.message ? error.message : "");
gln_release_error(&error);
return 1;
}
gln_secret_t* tan = NULL;
const char tan_text[] = "123456";
s = gln_create_secret((const uint8_t*)tan_text, strlen(tan_text), &tan);
if (s != GLN_OK) {
gln_destroy_continuation(continuation);
return 1;
}
gln_continuation_input_t input = {0};
gln_default_continuation_input(&input);
gln_backend_result_t* resumed = NULL;
s = gln_resume_continuation(backend, continuation, tan, &input, &resumed);
gln_destroy_secret(tan);
gln_destroy_continuation(continuation);
if (s != GLN_OK) {
return 1;
}
if (gln_get_backend_result_outcome(resumed) == GLN_BACKEND_OUTCOME_ACTION_REQUIRED) {
gln_continuation_t* next = gln_take_backend_result_continuation(resumed);
if (next != NULL) {
s = gln_save_continuation(continuation_store, "profiles/alice/transfer.resume", next, &error);
if (s != GLN_OK) {
fprintf(stderr, "%s\n", error.message ? error.message : "");
gln_release_error(&error);
}
gln_destroy_continuation(next);
}
}
gln_destroy_backend_result(resumed);
Inspection and Description
Use inspection for diagnostics, resume routing, and validation before showing a saved artifact to an operator:
GLN_API gln_status_t GLN_CALL gln_inspect_continuation(
gln_continuation_t* in_continuation,
gln_continuation_info_t* out_info,
gln_error_t* out_error);
GLN_API gln_status_t GLN_CALL gln_describe_continuation(
gln_continuation_t* in_continuation,
char** out_describe_json,
gln_error_t* out_error);
Initialize gln_continuation_info_t with continuation-info output slot with struct_size set to sizeof(gln_continuation_info_t), inspect
the returned metadata, and keep any copied strings needed after destroying the
continuation. Description JSON returned by gln_describe_continuation is a
caller-owned char*; release it with gln_release_string.
gln_inspect_continuation fills the C-visible gln_continuation_info_t
fields:
| Field | Populated value |
|---|---|
backend | GLN_CONTINUATION_BACKEND_FINTS for current client continuations. |
operation_kind | Borrowed string naming the original operation, such as submit_transfer or list_transactions. |
profile_hint | Borrowed bank_code:user_id hint when both pieces are present, the single present value when only one is present, otherwise NULL. |
requires_pin | Non-zero for current FinTS continuations because resume needs the FinTS PIN to open the next dialog. |
requires_tan | Non-zero for non-decoupled FinTS continuations. Use gln_describe_continuation to distinguish a TAN prompt from progress.phase == "vop_confirmation". |
cached_bpd_present | Non-zero when the continuation carries cached bank parameter data. |
cached_upd_present | Non-zero when the continuation carries cached user parameter data. |
retry_after_seconds_present | Non-zero when retry_after_seconds carries a saved decoupled polling interval. |
retry_after_seconds | Saved decoupled polling interval in seconds when retry_after_seconds_present is non-zero; otherwise 0 and absent. |
gln_describe_continuation returns JSON for operator-facing display,
diagnostics, and phase-specific routing. For FinTS continuations,
progress.phase is tan_required, decoupled_pending, or
vop_confirmation. progress.follow_up_kind is null for the continuation
producers implemented by this C API. progress.retry_after_seconds is the
saved decoupled polling interval when the continuation carries one, otherwise
0. Description also includes operation_kind,
original_request_json, created_at_unix_ms, and cached BPD/UPD presence
flags.
Invalid and Stale Artifacts
GLN_ERR_RESUME_ARTIFACT_INVALID means a persisted continuation could not be
parsed or accepted by the current resume parser. Treat it as a terminal
artifact failure: discard that artifact and re-issue the original workflow.
Banks also expire TAN and decoupled approval windows. Applications should reject old saved continuations before asking the user for a TAN or polling approval.
See Also
- Conventions - result-envelope status and ownership.
- Stores - continuation store vtable and file-backed storage.
- Secrets - wrapping TAN values.
- Troubleshooting - stale and invalid resume artifacts.