Skip to content

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:

PauseResume inputSecretResult status before resume
TAN challengeGLN_CONTINUATION_INPUT_KIND_NONE, or GLN_CONTINUATION_INPUT_KIND_HHDUC_RESPONSE when an HHD_UC response is suppliedgln_secret_t* containing the TANGLN_ERR_TAN_REQUIRED
Decoupled approval pollGLN_CONTINUATION_INPUT_KIND_DECOUPLED_POLLNULLGLN_ERR_DECOUPLED_PENDING
Decoupled-pending HHDuc echo/status variantGLN_CONTINUATION_INPUT_KIND_HHDUC_RESPONSENULLGLN_ERR_DECOUPLED_PENDING
Verification of Payee confirmationGLN_CONTINUATION_INPUT_KIND_VOP_CONFIRMNULLGLN_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:

FieldPopulated value
backendGLN_CONTINUATION_BACKEND_FINTS for current client continuations.
operation_kindBorrowed string naming the original operation, such as submit_transfer or list_transactions.
profile_hintBorrowed bank_code:user_id hint when both pieces are present, the single present value when only one is present, otherwise NULL.
requires_pinNon-zero for current FinTS continuations because resume needs the FinTS PIN to open the next dialog.
requires_tanNon-zero for non-decoupled FinTS continuations. Use gln_describe_continuation to distinguish a TAN prompt from progress.phase == "vop_confirmation".
cached_bpd_presentNon-zero when the continuation carries cached bank parameter data.
cached_upd_presentNon-zero when the continuation carries cached user parameter data.
retry_after_seconds_presentNon-zero when retry_after_seconds carries a saved decoupled polling interval.
retry_after_secondsSaved 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.