Only present if getCapabilities().supportsManualCapture. Capture is the canonical double-charge operation — the idempotency key is REQUIRED, and under supportsMultiCapture each partial capture is its own charge with its own key.
Tokenize-first completion: create the payment from the clientToken
confirm() produced, the hostedTokenizationId plus the browser's device data
(see decodeWorldlineClientToken; a bare hostedTokenizationId is accepted
too). The signed context is the only trusted source of
amount/currency/capture-method. The session's metadata, when it has
entries, is sent JSON-encoded as order.references.merchantParameters; a
context signed before metadata was carried sends none.
Every payment carries the mandatory 3-D Secure properties the adapter can
supply: the return URL in both documented forms,
threeDSecure.skipAuthentication: false, the device data as
order.customer.device, and the use case Cartes Bancaires requires,
paymentProduct130SpecificInput.threeDSecure.usecase: "single-amount",
whatever the card's brand. acceptHeader and ipAddress are observed on
the customer's HTTP request and CompletePaymentInput carries neither, so
they are not sent. The cardholder name is entered in the Hosted
Tokenization iframe. sca.challenge: "force" requests
challengeIndicator: "challenge-required". sca.exemption: "moto" sends
transactionChannel: "MOTO", Worldline's mail order / telephone order
channel, with the 3-D Secure data unchanged; without it no channel is
sent, which Worldline reads as ECOMMERCE.
A REDIRECT merchantAction (3-D Secure challenge) surfaces as
requires_action with the redirect URL on raw; the customer completes it
and the host reconciles with retrievePayment. Its metadata is the
answer's merchantParameters echo when it carries one. The API contract
documents that echo on GET calls and webhooks only, so without one it is
the metadata the payment's request carried: this call's for the call's own
payment, and for an earlier attempt's, the metadata that payment reads
back with. A decline rejects with the
same error code whether Worldline answers it with a 402 or with a 2xx
carrying a REJECTED payment (see mapWorldlineError); raw is the error
body in the first case and the whole CreatePayment response in the second.
Worldline answers a key it has seen with that key's first outcome for at
least 24 hours, so on its own a key whose attempt failed would answer every
later card with that failure. When a key answers with a replay of a failed
attempt, the payment goes out again under a key derived from
idempotencyKey and that attempt, and so on until an attempt that did not
fail, past which nothing is sent: a completion repeated after a success
returns that payment. An attempt failed once its payment was declined or
cancelled, as its answer shows or, for a 3-D Secure challenge or a payment
still pending when answered, as it reads back now. A payment authorised or
captured when created never counts as failed, nor does one that reads
Authorised and cancelled (6); a challenge still open is returned as
requires_action, and a refusal whose payment may still go through is
thrown marked outcomeUnknown, as is a payment that cannot be read back. An
earlier attempt's payment that did not end unpaid is returned only when it
was made for the session's amount and currency; otherwise the completion
is refused with a non-retryable invalid_request marked outcomeUnknown
(raw.reason: "another_sessions_payment").
A replay is recognised by its X-GCS-Idempotence-Request-Timestamp header
on the key's first send in the call. On a re-send, after a timeout or a lost
connection, only a timestamp that reads as the documented milliseconds,
less than 23 hours old and more than 15 minutes before that first send,
names an earlier completion's attempt. Any other replayed answer may be
the call's own: it is read like a replay, but a failed attempt among them
is thrown marked outcomeUnknown instead of walked past. That margin assumes
this server's clock runs less than 15 minutes ahead of Worldline's. A 409,
a 429 and a 5xx never lead to a new attempt. The completion is refused
with a non-retryable invalid_request after 20 attempts under one key
(raw.reason: "attempt_limit"), and, marked outcomeUnknown, when Worldline
could forget the key's first attempt before the session expires
(raw.reason: "first_attempt_may_expire").
Creates the Hosted Tokenization session (POST /hostedtokenizations — no amount at this step) and encodes amount/currency/capture-method, the return URL, the SCA preference, the metadata and the returned hostedTokenizationId into a signed, self-contained context that completePayment later verifies and trusts. The client mounts the iframe from the returned hostedTokenizationUrl (the session's clientSecret).
metadata goes to Worldline JSON-encoded, as the payment's
order.references.merchantParameters, which Worldline echoes on reads and
in webhooks: retrievePayment reports it as PaymentInfo.metadata, and
readWorldlineWebhookMetadata reads it from a webhook. Worldline's API
contract says of that field: "This field must not contain any personal
data." Keep personal data out of a Worldline session's metadata; the
adapter sends what it is given. The context is signed, not encrypted:
whoever holds pspSessionId can read it, the metadata included.
Refused with invalid_request before anything reaches Worldline when the
session has no return URL (neither a non-empty returnUrl nor the
adapter's defaultReturnUrl) or one Worldline rejects (over 200
characters, or without a protocol such as https:// or an app scheme),
when id is longer than the 40 characters
order.references.merchantReference accepts, when
statementDescriptor is longer than the 256 characters
order.references.softDescriptor accepts, or when the JSON sent for
metadata is not an object of string values or is longer than the 1000
characters order.references.merchantParameters accepts (UTF-16 code
units, so an emoji counts two).
Throws PayFanoutError (code "invalid_request") on unparseable payloads; maps genuinely unknown-but-valid event types to type "unknown" rather than throwing.
Reads the payment, its captures and its refunds. metadata is the
session's as Worldline echoes it in merchantParameters (see
createPaymentSession). createdAt is the payment's
paymentOutput.transactionDate, which Worldline describes as "the
server-side processing date and time of the transaction" without saying
whether a later capture or refund moves it. It is read only with a time
zone: Z, ±HH:MM or ±HH. Without a readable one, a value without a zone
included, it is 1970-01-01T00:00:00.000Z.
OptionalpayfanoutId: stringPolls a refund. Worldline Direct exposes no refund-by-id endpoint (that is
a Connect-era surface) — the only read is GET /payments/{id}/refunds — so
refundId is the composite "{paymentId}:{refundId}" that refundPayment
returned, resolved here through the payment's refund list.
"Test connection" probe: one side-effect-free GET against the account's
test-connection service, /v2/{merchantId}/services/testconnection. It
reads the RAW HTTP status, so an auth rejection (401/403; Worldline answers
a key, secret or PSPID it cannot match, or keys of the other environment,
with a 403) is told apart from an outage (429/5xx) from the status line
alone. Only a 2xx carrying the service's OK answer, "result": "OK",
passes: "If you receive an OK result you know that your connection with us
is working correctly" (Connect S2S API reference, Test connection). Any
other answer that is neither an auth rejection nor an outage means the
probe did not get that answer: a baseUrl override is wrong (Worldline
answers a wrong endpoint with an empty body), names a host that answers 2xx
with something else, or the service answered a result other than OK, which
no Worldline page documents. It reports internal, naming the HTTP status
and, on a non-2xx, the first error's id when it reads as one. A single
call, never retried; never mutates PSP state, never puts a credential in
the result.
Webhook handling — same adapter, separate concerns. Both async: future PSPs may need remote key retrieval; the contract stays uniform. MUST operate on the RAW request body bytes/string — re-serializing a parsed body breaks signatures (the conformance suite tests this).
Resolves "canceled" only when the PSP confirms it. Under getCapabilities().modificationOutcome "asynchronous" the PSP merely acknowledges the request and the outcome arrives by webhook, so the returned status is "processing" — never an unconfirmed terminal state.