Handle payment completion and 3-D Secure authentication

Complete a payment with Hosted fields

Once you have tokenised the customer's card details with the Hosted Fields SDK, use the token to complete the payment.

This guide continues the Configure Hosted fields SDK flow after elements.tokenize(). It explains how to:

  • Process the token.
  • Handle authentication when the issuer requires it.
  • Follow redirects.
  • Read the Payment Request state.
  • Retry a declined payment.
  • Confirm the payment result.

The SDK is not involved after tokenisation. It collects the card data and returns a token.

Your server then communicates with the Corefy API. The browser is only involved when it needs to submit an authentication or redirect action.

Understand the payment model

  1. Use the Payment Request snapshot as the source of truth.

  2. Every decision comes from the current Payment Request state, including:

    • status.
    • active_payment_commit_info.status.
    • active_payment_transaction_info.status.
  3. Do not maintain the payment state separately in your application.

  4. The payment model contains three levels:

Term Meaning
Payment Request The complete payment lifecycle from creation to a terminal status. It uses a pr_... ID.
Payment Commit One authorisation lifecycle inside the Payment Request. A declined transaction can finish the Commit or leave it open for another attempt.
Payment Transaction One Provider attempt inside a Payment Commit. A Payment Commit can contain multiple transactions.
Snapshot The Payment Request state returned by the API at a specific moment. active_payment_commit_info contains the current Commit, and active_payment_transaction_info contains the current transaction.
Directive The flow_action object on the current transaction. It describes a form that the browser must submit.
Terminal status A Payment Request status that does not change again.
Resolution The reason behind a status. The Payment Request, Commit, and Transaction use separate resolution values.
  1. Treat flow_action as a directive rather than as payment information.
  2. When the system requires the browser to continue the payment flow, the active transaction contains a flow_action with:
  • url.
  • method.
  • params.
  1. Submit the directive exactly as returned.

Do not parse, rename, reorder, or modify the Provider parameters.

Understand the return flow

  1. Set options.return_url when you create the Payment Request.

The parameter is mandatory.

  1. After authentication or a redirect, the system returns the customer to this URL and appends four query parameters.

  2. Treat these parameters as a Routing hint only.

  3. The customer can modify values in the browser address bar. Read the Payment Request from your server before you act on the result.

  4. Use Webhooks as the authoritative record of the final payment outcome.

A customer can close the browser tab during authentication and never return to your page. The payment can still resolve.

Configure the domain before loading Hosted fields

  1. If the Domain iFrame whitelist is enabled for Hosted fields, add the domain that serves your Checkout page to the whitelist.
  2. Hosted fields refuse to load on an unlisted domain.
  3. The SDK returns: forbidden_origin

For more information, go to the Manage Security settings for Hosted fields guide.

Use the API keys

  1. Hosted fields uses two separate credentials.
Credential Use Location
Hosted Fields API key (hf_...) Create a Payment Request and process the token. Hosted fields tab of the Merchant account.
Merchant API key Read the Payment Request state. API key settings.
  1. The keys are not interchangeable.
  2. The Hosted Fields API does not accept the Merchant API key.
  3. The Merchant API does not accept the Hosted Fields API key.

You need both keys to complete the payment flow.

Use the Hosted Fields API key

  1. Use the Hosted Fields API key to:

    • Create a Payment Request.
    • Process a Payment token.
  2. Also pass the Hosted Fields API key to:

    createElements({ apiKey })

  3. The key is scoped to collecting card data and can be included in the browser application.

Use the Merchant API key

  1. Use the Merchant API key to read the Payment Request state.
  2. Keep this key on your server.
  3. Do not expose it in the browser.

Send API requests from the server

  1. Send both payment API calls from your server.
  2. The API does not allow the required cross-origin requests from the browser.
  3. A browser fetch fails before the request reaches the API.
  4. The Merchant API key must also remain on the server because it is a full account credential.
  5. The Hosted Fields SDK does not call these payment API endpoints.

The Corefy host that serves the Hosted fields iFrames communicates with the API server to server.

Configure the Hosted fields host

If you use:

createElements({ baseUrl })

set baseUrl to the Corefy host that serves the field iFrames.

This host is separate from both backend APIs.

Understand the payment flow

  1. The flow involves three components:
Component Responsibility
Browser Loads Hosted fields, collects card details, tokenises them, and submits authentication or redirect directives.
Your server Creates Payment Requests, processes tokens, and reads the result.
Corefy Provides the API and the pages used during authentication or redirect flows.
  1. Anything that creates or changes payment state or reads the result is a server-side operation.

  2. The browser only:

    • Collects the card details.
    • Tokenises the card details.
    • Sends the customer to the authentication or Provider page.
    • Returns the customer to your Checkout.
  3. Corefy can create another transaction after a failed Provider attempt.

  4. There is no separate retrying state.

  5. Read the Payment Request again. If the replacement transaction contains a new directive, submit it in the same way as the previous directive.

Create a Payment Request

  1. Create the Payment Request before you process the token.

  2. Use:

    POST /api/hosted-fields/v1/payment-requests

  3. Authenticate the request with the Merchant account ID and Hosted Fields API key.

Example:

curl -X POST 'https://processing.eu.corefy.com/api/hosted-fields/v1/payment-requests' \
  -u '<merchant_account_id>:<hosted_fields_api_key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "data": {
      "type": "payment-requests",
      "attributes": {
        "test_mode": true,
        "origin_reference_id": "order-4821",
        "reference_id": "order-4821",
        "currency": "USD",
        "strategy": "direct",
        "amount": "50",
        "options": {
          "return_url": "https://merchant.example/checkout/return",
          "commits_limit": 2,
          "transactions_limit": 2
        }
      }
    }
  }'

Set Payment Attempt limits

  1. Use commits_limit to define how many Commits the Payment Request accepts.
  2. This value also defines how many process calls the Payment Request can serve.
  3. Use transactions_limit to define how many transactions can exist inside the Payment Request.

Both values:

  • Must be integers.
  • Can be up to 10.

The transactions_limit value can also be null.

  1. When all available Commits are used, the Payment Request moves to:

    failed

    with:

    attempt_limit_reached

Tokenise the card details

  1. Collect the customer's card details with the Hosted Fields SDK.

  2. Then, call:

    elements.tokenize()

  3. The SDK returns a Payment token.

  4. Use the token to process the Payment Request.

Process the Payment token

  1. Send the token to the Payment Request from your server.

  2. Use:

    POST /api/hosted-fields/v1/payment-requests/{id}/process

  3. Include:

    • payment_method_scheme_id.
    • The token returned by Hosted fields.

The payment_method_scheme_id is required for every process request.

Example:

{
  "data": {
    "type": "payment-requests",
    "id": "pr_...",
    "attributes": {
      "payment_method_scheme_id": "card",
      "token": "..."
    }
  }
}
  1. The id in the request body must match the Payment Request ID in the URL.

The API rejects the request if IDs do not match.

  1. Authenticate the request with the Hosted Fields API key.

Handle the Payment Request state

  1. Terminal Payment Request statuses include:
paid
over_paid
partially_paid
returned
partially_returned
verified
failed
expired
cancelled

Example:

const TERMINAL_STATUSES = new Set([
  'paid',
  'over_paid',
  'partially_paid',
  'returned',
  'partially_returned',
  'verified',
  'failed',
  'expired',
  'cancelled',
]);
  1. If the Payment Request has a terminal status, use:

    • The Payment Request resolution.
    • The Transaction resolution.
    • The Commit resolution when transaction information is not available.

Keep the Payment Request and card-level resolutions separate because they use different value sets.

Handle moderation holds

  1. Check:

    moderation_status

  2. If the value is: blocked, treat the payment as failed for the customer-facing flow.

  3. Keep the payment open for investigation by your operations team.

  4. A moderation hold can leave the Payment Request in pending indefinitely.

  5. Do not wait for a terminal Payment Request status before finishing the customer-facing flow.

Handle failed transactions

  1. Check the active transaction status before you submit a directive.

  2. A failed authorisation uses: authorize_failed

  3. A failed transaction can still contain the flow_action that failed.

Corefy does not clear the directive after assigning it to the transaction.

  1. Submitting the directive again can send the customer to an expired authentication page and create a loop.
  2. Check whether the transaction has already failed before you read and submit its directive.

Handle Cascading

  1. If:

    active_payment_transaction_info.status = authorize_failed

    and:

    active_payment_commit_info.status = authorize_pending

    wait for the state to update.

  2. Corefy can continue the cascade automatically and create a replacement transaction.

  3. The replacement transaction can use another Provider.

  4. It can also contain its own flow_action.

  5. Do not:

    • Retry the Payment Request yourself.
    • Submit the failed transaction directive again.
  6. There is no separate retrying status.

  7. Read the Payment Request again and react to the updated snapshot.

Handle a completed Payment Commit

  1. A Commit is complete when its status is:

    authorize_failed

    or:

    failed

  2. If the Commit is complete and:

    • The active transaction has failed.
    • Or the active transaction is not available.
  3. You can start another Payment Attempt if the Payment Request limits allow it.

  4. Use the transaction resolution when it is available.

  5. Otherwise, use the Payment Commit resolution.

Handle directives

  1. Look for supported directives in the active transaction.

  2. The supported types are:

    acs_redirect authenticate_3ds_v2 redirect

  3. Only submit a directive when it contains both:

    • url.
    • method.
  4. If one of these values is missing, wait for the Payment Request state to update.

  5. If flow_action contains an unrecognised key, wait instead of treating it as an error.

  6. An unrecognised directive usually indicates a payment method that is not enabled.

Submit the flow action

  1. Submit the directive as a normal HTML form in the customer's browser.
  2. The server cannot submit this request on behalf of the browser.

Example:

function submitDirective(directive, { target = '_self' } = {}) {
  const form = document.createElement('form');
  form.method = directive.method;
  form.action = directive.url;

  const params = directive.params?.params ?? directive.params ?? {};

  for (const [name, value] of Object.entries(params)) {
    if (value === null || value === undefined || value === 'undefined') continue;

    const input = document.createElement('input');
    input.type = 'hidden';
    input.name = name;
    input.value = String(value);
    form.appendChild(input);
  }

  if (target !== '_self') form.target = target;

  document.body.appendChild(form);
  form.submit();
  form.remove();
}

Handle nested parameters

  1. Corefy can return parameters in a nested structure:
{
  "type": "pareq",
  "params": {
    "PaReq": "...",
    "MD": "...",
    "TermUrl": "..."
  }
}
  1. Older or custom Providers can return the parameters directly.
  2. Read the nested structure first and fall back to the direct value:
directive.params?.params ?? directive.params ?? {}
  1. The params value can also be null. For example, a redirect can contain all required data in the URL.

Skip empty parameters

  1. Skip parameter values that are: null undefined "undefined"

  2. Some Providers reject "undefined" as a malformed parameter instead of ignoring it.

Preserve Provider parameters

  1. Submit every parameter exactly as returned.

  2. Do not:

    • Rename parameters.
    • Reorder parameters.
    • Rewrite values.
    • Add your own parameters.
  3. If TermUrl is present, use the value returned by Corefy. Do not replace it.

  4. TermUrl points back to Corefy and is how the authentication result returns to the platform. Not every Provider sends TermUrl.

Choose the redirect target

  1. Use _self by default. This causes a full-page navigation.
  2. The current Checkout page is destroyed and rebuilt when the customer returns. This option works without additional handling.
  3. You can also submit the directive to a named iFrame. This keeps your Checkout page and its local state available during authentication.

Handle the return

  1. After authentication or redirect, Corefy sends the customer to options.return_url.

  2. Corefy appends the following parameters to the existing query string:

    status resolution operation_id reference_id

    Example:

    https://merchant.example/checkout/return   
    ?status=attempted   
    &resolution=ok   
    &operation_id=pr_Xxr2ZwTT5vNNUEhc   
    &reference_id=order-4821
  3. The operation_id value is the Payment Request ID.

  4. The status and resolution values contain the Payment Request state at the moment of the redirect.

Verify the return

  1. Use operation_id to identify the Payment Request.
  2. Do not use the other query parameters to determine or verify the payment state. A customer can edit these values in the browser.
  3. Read the Payment Request from the server again.

Example:

async function handleReturn(req, res) {
  const paymentRequestId = req.query.operation_id;
  const order = await orders.findByPaymentRequestId(paymentRequestId);

  const attributes = await readPaymentRequest(paymentRequestId);
  const next = decide(attributes);

  switch (next.action) {
    case 'result':
      return res.redirect(`/orders/${order.id}`);

    case 'wait': {
      const settled = await pollUntilSettled(paymentRequestId);

      if (settled.action === 'result') {
        return res.redirect(`/orders/${order.id}`);
      }

      if (settled.action === 'submit') {
        return res.render('submitting', {
          directive: settled.directive
        });
      }

      if (settled.action === 'retry') {
        return res.render('retry', {
          reason: settled.reason
        });
      }

      return res.render('pending', {
        paymentRequestId
      });
    }

    case 'submit':
      return res.render('submitting', {
        directive: next.directive
      });

    case 'retry':
      return res.render('retry', {
        reason: next.reason
      });

    default:
      return res.render('pending', {
        paymentRequestId
      });
  }
}
  1. The orders implementation belongs to your application.

Handle an early return

  1. The customer can return to your page before the Provider callback reaches Corefy. A non-terminal Payment Request at this point is valid.

  2. Read the Payment Request again.

  3. Then:

    • Display the result if the payment is finished.
    • Submit a new directive if one is available.
    • Offer another Payment Attempt if the Commit is complete.
    • Wait or poll if the payment is still moving.
  4. The second authentication step can occur after the first one. For example, Corefy can cascade the payment to another Provider. The new transaction then contains an ordinary new directive.

  5. Submit it using the same logic.

Distinguish Payment Request resolutions

  1. The resolution query parameter in return_url belongs to the Payment Request.

  2. Payment Request resolutions include:

ok 
expired 
attempt_limit_reached 
merchant_account_disabled 
organization_disabled 
blocked_by_firewall_rules 
blocked_due_to_firewall_failure
  1. Card-level reasons do not appear here.
  2. For example: insufficient_funds belongs to the transaction.
  3. Read card-level reasons from: active_payment_transaction_info.resolution
  4. Do not treat Payment Request and transaction resolutions as the same value set.

Confirm the payment outcome

  1. The payment flow does not always follow a strict sequence.

  2. A wait state can occur:

    • Immediately after process.
    • After the customer returns from authentication.
    • During a cascade.
  3. When the payment still requires processing and no browser action is available, show a pending state.

  4. Use the Payment Request Webhook to update the actual order state.

  5. The Webhook arrives even if the customer never returns to your page.

  6. Use polling only if you want to provide the customer with an updated result sooner.

Handle pending payments

  1. A pending status does not always mean that the payment is temporarily waiting to become paid.

  2. The Payment Request can remain pending when:

    • The Commit covers only part of the Payment Request and the Payment Request has not expired.
    • A delayed charge captured the amount and waits for you to capture or cancel it.
    • A verify strategy captured the amount and waits for a return or confirmation.
  3. A standard direct card payment reaches pending only in the first case.

  4. If you use delayed charges or verify, do not build a result page that waits for a terminal status.

  5. The Payment Request can remain pending until you act.

For more information, go to the Payment Statuses & Resolutions guide.

Retry a declined payment

A declined payment can leave the Payment Request open until the configured limits are exhausted.

To retry:

  1. Read the transaction resolution.
  2. Use the Commit resolution if transaction information is not available.
  3. Tokenise the card details again.
  4. Process the new token.
  5. Read the updated Payment Request state.

Do not reuse the previous Hosted fields token. The previous token no longer exists after the first process request.

Remount Hosted fields

  • Mounting a Hosted field replaces the target container with an iFrame.
  • Calling destroy() removes the iFrame. It does not restore the original container.
  1. If you try to mount the field to the same selector again, the selector no longer matches an element.
  2. Keep a permanent wrapper around each Hosted field.

Example:

<div id="card-number-wrapper">
  <div class="field"></div>
</div>
  1. Before another Payment Attempt, create a new mount target.

Example:

function resetField(wrapperId) {
  const wrapper = document.querySelector(`#${wrapperId}`);
  wrapper.replaceChildren(document.createElement('div'));
  return wrapper.firstElementChild;
}
await cardNumber.mount(resetField('card-number-wrapper'));
  1. The mount() method accepts an HTMLElement. You do not need to use a selector for the new target.
  2. If you destroy the complete Hosted fields session, call: createElements() again before you create the fields.

Handle exhausted Payment Attempt limits

  1. When commits_limit is exhausted, the Payment Request moves to: failed with: attempt_limit_reached

  2. You cannot process another attempt on this Payment Request.

  3. Create a new Payment Request.

Poll for the payment result

Polling is optional.

  1. Use it when you want to give the customer an updated result before the Webhook arrives.

Example:

async function pollUntilSettled(
  paymentRequestId,
  {
    intervalMs = 3000,
    maxPasses = 20
  } = {}
) {
  for (let pass = 0; pass < maxPasses; pass++) {
    const next = decide(
      await readPaymentRequest(paymentRequestId)
    );

    if (next.action !== 'wait') {
      return next;
    }

    await sleep(intervalMs);
  }

  return { action: 'gave_up' };
}
  1. The sleep implementation belongs to your application.

The example uses:

  • A 3000 ms interval.
  • 20 polling passes.
  1. Choose the interval and number of passes for your integration. The platform does not guarantee a settlement time.

  2. Stop polling when the state returns an action other than wait.

    For example:

    • result.
    • submit.
    • retry.
  3. If the polling limit is reached:

    • Show a pending state.
    • Let the Webhook settle the order.
    • Do not mark the payment as failed.
  4. The gave_up action belongs to the polling helper in the example. It is not a Payment Request state returned by Corefy.

  5. Use polling only to improve the customer experience.

  6. Use Webhooks as the authoritative record.

Run the challenge in an iFrame

  1. Submitting the directive to a named iFrame keeps your Checkout page alive.
  2. Your local page state and polling can remain active while the customer completes authentication. This is a supported integration pattern.
  3. Corefy Checkout exposes a related security setting called Submit flow action in iFrame.
  4. Hosted fields does not provide a separate setting for this behaviour because the Checkout page belongs to your integration.

Example:

function submitInFrame(directive, frameName) {
  submitDirective(directive, {
    target: frameName
  });
}

Handle cross-origin restrictions

  1. The authentication document is cross-origin.

    You cannot:

    • Read its contents.
    • Measure it.
    • Detect completion from inside the iFrame.
  2. Use the Payment Request snapshot as the reliable signal.

  3. Continue polling the Payment Request while the challenge runs.

Handle issuers that block iFrames

  1. Some issuers refuse to load authentication pages in an iFrame. The iFrame can remain blank without producing an event.
  2. Provide a visible control that lets the customer continue in the current browser window.
  3. Submit the same directive again with: _self.

Avoid authentication timeouts

  1. Do not impose your own authentication timeout.

  2. A customer can take several minutes to complete authentication.

    For example, the customer can:

    • Read an SMS.
    • Switch to a banking application.
  3. Close the iFrame when the Payment Request snapshot no longer contains the active directive.

Review flow action types

Three directive types can appear during a card payment. Submit all of them using the same logic.

acs_redirect

Example:

{
  "acs_redirect": {
    "url": "https://acs.issuer.example/pareq",
    "type": "acs_redirect",
    "method": "POST",
    "params": {
      "type": "pareq",
      "params": {
        "PaReq": "...",
        "MD": "...",
        "TermUrl": "..."
      }
    }
  }
}

authenticate_3ds_v2

Example:

{
  "authenticate_3ds_v2": {
    "url": "https://acs.issuer.example/challenge",
    "type": "authenticate_3ds_v2",
    "method": "POST",
    "params": {
      "type": "creq",
      "params": {
        "creq": "..."
      }
    }
  }
}

redirect

Example:

{
  "redirect": {
    "url": "https://provider.example/pay/session-123",
    "type": "redirect",
    "method": "GET",
    "params": null
  }
}
  1. Parameter names differ between Providers and authentication versions.
  2. Do not branch on individual parameter names. Branch only on the available directive type.
  3. Submit the returned parameters without modifying them.

Check the integration before going live

Make sure that your integration:

  • Creates the Payment Request before processing the token.
  • Includes options.return_url.
  • Adds the Checkout domain to the Domain iFrame whitelist when the whitelist is enabled.
  • Uses the Hosted Fields API key to create the Payment Request.
  • Uses the Hosted Fields API key to process the token.
  • Keeps the Merchant API key on the server.
  • Does not send payment API requests from the browser.
  • Provides payment_method_scheme_id for every process request.
  • Uses the same Payment Request ID in the process URL and request body.
  • Treats Hosted fields tokens as single-use.
  • Tokenises the card details again before every new Payment Attempt.
  • Reads the Payment Request state from the server.
  • Uses the Payment Request snapshot as the source of truth.
  • Distinguishes Payment Request, Commit, and Transaction states.
  • Checks whether the active transaction has failed before submitting its directive.
  • Does not submit a stale directive from an authorize_failed transaction.
  • Waits when an authorize_failed transaction belongs to an authorize_pending Commit.
  • Allows Corefy to continue cascading automatically.
  • Expects a replacement transaction to contain another authentication directive.
  • Does not look for a separate retrying state.
  • Starts another Payment Attempt only after the current Commit is complete.
  • Handles a completed Commit when the active transaction is unavailable.
  • Uses the transaction resolution when it is available.
  • Uses the Commit resolution when the transaction does not provide one.
  • Submits flow_action parameters exactly as returned.
  • Supports nested and direct params.
  • Handles params: null.
  • Skips null, undefined, and "undefined" values.
  • Does not replace TermUrl.
  • Waits when a directive is unknown.
  • Waits when a directive does not contain both url and method.
  • Re-reads the Payment Request after the customer returns.
  • Uses only operation_id from the return URL to identify the Payment Request.
  • Does not use return URL parameters as proof of payment.
  • Handles the case when the customer returns before the Provider callback arrives.
  • Distinguishes Payment Request resolutions from transaction resolutions.
  • Uses Webhooks to record the final payment outcome.
  • Does not depend on the customer returning to the Checkout page.
  • Handles moderation holds separately from ordinary pending payments.
  • Handles Payment Requests that can intentionally remain pending.
  • Does not wait for a terminal status for delayed charges or verify flows.
  • Creates a fresh mount target before remounting Hosted fields.
  • Calls createElements() again after destroying the complete Hosted fields session.
  • Creates a new Payment Request after attempt_limit_reached.
  • Stops polling when the state returns an action other than wait.
  • Shows a pending state when polling reaches its limit.
  • Does not mark the payment as failed because polling ends.
  • Uses the Payment Request snapshot to track an authentication flow inside an iFrame.
  • Provides a _self fallback when an issuer refuses to load inside an iFrame.
  • Does not impose its own authentication timeout.

Did this page help you?