Workflows

Expanded FlexFactor Workflow Details

Credentials

A merchant integration is provisioned with the credentials below. MID, API Key, and API Secret are always required; Tokenization Key and Site ID are optional depending on your integration. All credentials are environment-specific — sandbox and production values differ and must be swapped at cutover (see §6).

CredentialTypeRequiredDescription
MIDGUIDRequiredFlexFactor Merchant Identification Number. Identifies the merchant account on every API call.
API KeyStringRequiredPublic client key (the appKey) sent with the API Secret to /oauth2/token to obtain a Bearer token.
API SecretStringRequiredClient secret (the appSecret) paired with the API Key to authenticate. Store securely server-side; never expose it to the client.
Tokenization KeyStringOptionalAuthenticates the /tokenize API, which does not use a Bearer token — the key is supplied as the environment query parameter (alongside mid) to vault card data. Not sent on any other call.
Site IDGUIDOptionalIdentifies a specific site/descriptor under the MID. Used when a merchant has multiple descriptors or brands on one MID.

Example credential set

{
  "mid": "3f8a1c42-9b07-4e5d-bc6a-1d2e3f4a5b6c",
  "apiKey": "Kp9XmR2vQ7sL4wN8tB6yH3jF5dG1aZ0cVxU4eW7n",
  "apiSecret": "AQAAAAEAACcQwL8xR2vQ7sN4wB6yH3jF5dG1aZ0cVxU4eW7nKp9XmR2vQ7sL4wN8tB6yH3jF5dG1aZ0cV==",
  "tokenizationKey": "9Hq4Lm2Rv7Ns8Wb6Yh3Jf5Dg1Kp",
  "siteId": "a1b2c3d4-e5f6-4789-9abc-def012345678"
}

These are illustrative values — replace every credential with the ones issued for your account, and use the matching set for each environment.


1. Authentication

Every API call requires a valid Bearer token. Tokens have a 10-minute TTL — cache and reuse them; do not request a new token per call.

%%{init: {'theme': 'base', 'themeVariables': {'actorBkg': '#3C3C42', 'actorTextColor': '#FFFFFF', 'actorBorder': '#56565E', 'noteBkgColor': '#F8F8F8', 'noteTextColor': '#3C3C42', 'activationBkgColor': '#FF5B5B', 'activationBorderColor': '#E7575B', 'signalColor': '#56565E', 'signalTextColor': '#3C3C42'}}}%%
sequenceDiagram
    participant C as Integrator
    participant FF as FlexFactor API

    C->>FF: POST /v1/oauth2/token<br/>{AppKey, AppSecret}
    FF-->>C: {accessToken, expires}
    Note over C: Cache token — reuse until 401

    C->>FF: Any API call<br/>Authorization: Bearer {accessToken}
    alt 401 Unauthorized
        FF-->>C: 401
        C->>FF: POST /v1/oauth2/token<br/>(re-authenticate with AppKey + AppSecret)
        FF-->>C: {new accessToken}
        C->>FF: Retry original request once
        FF-->>C: Response
    else Success
        FF-->>C: 200 / 201 response
    end

Environments

EnvironmentBase URL
Sandboxhttps://api-sandbox.flexfactor.io/v1/
Productionhttps://api.flexfactor.io/v1/

2. Primary flows

2.1 CIT purchase ("sale")

The default flow for customer-initiated transactions. Synchronous — response is immediate. No special account enablement required.

%%{init: {'theme': 'base', 'themeVariables': {'actorBkg': '#3C3C42', 'actorTextColor': '#FFFFFF', 'actorBorder': '#56565E', 'noteBkgColor': '#F8F8F8', 'noteTextColor': '#3C3C42', 'activationBkgColor': '#FF5B5B', 'activationBorderColor': '#E7575B', 'signalColor': '#56565E', 'signalTextColor': '#3C3C42'}}}%%
sequenceDiagram
    participant C as Integrator
    participant FF as FlexFactor API

    Note over C: Original decline occurs at primary processor
    C->>FF: POST /v1/evaluate
    Note over C,FF: isMIT: false, transactionType: "PURCHASE"

    alt APPROVED
        FF-->>C: {result: "Success", status: "APPROVED", orderId: "ff-order-guid", orderSessionKey: "ff-session-guid"}
        C->>C: Update order as approved — rescue succeeded
    else DECLINED
        FF-->>C: {result: "Success", status: "DECLINED"}
        C->>C: Return original decline to merchant
    else CHALLENGE
        FF-->>C: {result: "Success", status: "CHALLENGE"}
        C->>C: Treat as decline (gateway/orchestrator integrations)
    end

Minimal request (required fields)

POST /v1/evaluate
Authorization: Bearer {accessToken}
Content-Type: application/json

{
  "mid": "your-flexfactor-mid-guid",
  "isDeclined": true,
  "orderId": "your-ext-order-id",
  "siteId": "your-site-guid",
  "idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
  "isMIT": false,
  "transactionType": "PURCHASE",
  "customerIp": "203.0.113.45",
  "attemptCount": 2,
  "transaction": {
    "id": "txn-guid",
    "amount": 9900,
    "currency": "USD",
    "timestampUtc": "2026-05-22T14:30:00Z",
    "timezoneUtcOffset": -6,
    "responseCode": "05",
    "responseCodeSource": "processor",
    "avsResultCode": "Y",
    "cvvResultCode": "M",
    "cavvResultCode": "2",
    "cardNotPresent": true
  },
  "payer": {
    "email": "[email protected]"
  },
  "billingInformation": {
    "firstName": "Jane",
    "lastName": "Smith",
    "addressLine1": "123 Main St",
    "city": "Austin",
    "state": "TX",
    "zipcode": "78701",
    "country": "United States",
    "countryCode": "US"
  },
  "paymentMethod": {
    "cardNumber": "tok_sandbox_abc123",
    "token": true,
    "cardBinNumber": "424242",
    "cardLast4Digits": "4242",
    "expirationMonth": 12,
    "expirationYear": 2028,
    "verificationValue": "123",
    "holderName": "Jane Smith",
    "cardType": "credit",
    "cardBrand": "Visa",
    "cardCountry": "US"
  },
  "merchant": {
    "id": "merchant-guid",
    "mcc": 5999,
    "country": "US"
  }
}

Field optimization ranks

FieldRankNote
paymentMethod.verificationValueCriticalDirectly drives rescue performance — include whenever available; absence has measurable negative impact
customerIpCritical (CIT)CIT only — directly drives rescue performance; omit for MIT (cardholder not present). Must be the end-user client IP, not a server or proxy IP
paymentMethod.cardBrand, cardType, cardCountryRequiredRequired fields; each contributes meaningful rescue signal
paymentMethod.holderNameRequiredRequired per field reference
transaction.responseCode, responseCodeSource, cardNotPresentRequiredDecline context from the original transaction — required on every request
transaction.avsResultCode, cvvResultCodeHighOptional decline context — significantly improves rescue performance; populate from the original decline response
transaction.cavvResultCodeMediumOptional decline context — populate from the original decline response when available
transaction.processorNameHighSignificantly improves rescue performance; not required for launch
deviceDetails (all child fields)HighSignificantly improve rescue performance when device context is available at decline time; not required for launch

High-value optional fields

{
  "transaction": {
    "responseDescription": "Do Not Honor",
    "responseStatus": "DECLINED",
    "processorName": "Stripe"
  },
  "payer": {
    "phone": "5125550123"
  },
  "deviceDetails": {
    "deviceType": "desktop",
    "deviceName": "Chrome 124",
    "deviceOS": "macOS 14",
    "browser": "Chrome",
    "userAgent": "Mozilla/5.0 ..."
  }
}

When deviceDetails is included, all five child fields (deviceType, deviceName, deviceOS, browser, userAgent) are required.

Response examples

// APPROVED
{"result": "Success", "status": "APPROVED", "orderId": "ff-3a7b2c1d-...", "orderSessionKey": "ff-9e8d7c6b-..."}

// DECLINED
{"result": "Success", "status": "DECLINED"}

// CHALLENGE (treat as decline in gateway integrations)
{"result": "Success", "status": "CHALLENGE"}

result reflects whether the API request succeeded (Success/Failed, mixed case). status reflects the transaction outcome. Always key your integration logic on status. Failed is returned only when the evaluation did not complete — an evaluation timeout, a concurrent evaluation of the same order, or an unexpected error — and status is then DECLINED as a fallback (see §3.5).

transaction.amount is in cents (9900 = $99.00). amountToRefund (for refunds) is in dollars.


2.2 CIT authorize + capture

Auth-only on evaluate, explicit capture step required. Requires FlexFactor to enable the authorize capability on the integrator account — not available by default.

%%{init: {'theme': 'base', 'themeVariables': {'actorBkg': '#3C3C42', 'actorTextColor': '#FFFFFF', 'actorBorder': '#56565E', 'noteBkgColor': '#F8F8F8', 'noteTextColor': '#3C3C42', 'activationBkgColor': '#FF5B5B', 'activationBorderColor': '#E7575B', 'signalColor': '#56565E', 'signalTextColor': '#3C3C42'}}}%%
sequenceDiagram
    participant C as Integrator
    participant FF as FlexFactor API

    Note over C: Original decline occurs at primary processor
    C->>FF: POST /v1/evaluate
    Note over C,FF: isMIT: false, transactionType: "AUTHORIZATION"

    alt CAPTUREREQUIRED
        FF-->>C: {result: "Success", status: "CAPTUREREQUIRED", orderId: "ff-order-guid", orderSessionKey: "ff-session-guid"}
        C->>FF: POST /v1/capture
        Note over C,FF: {orderId: "ff-order-guid", amount: 9900,<br/>currency: "USD", idempotencyKey: "new-guid"}
        FF-->>C: {status: "SUCCESS", success: true}
        C->>C: Update order as captured
    else DECLINED
        FF-->>C: {result: "Success", status: "DECLINED"}
        C->>C: Return original decline to merchant
    end

Evaluate request — same as CIT Purchase with one change:

{
  "transactionType": "AUTHORIZATION"
}

Capture request

POST /v1/capture
Authorization: Bearer {accessToken}
Content-Type: application/json

{
  "orderId": "ff-3a7b2c1d-...",
  "amount": 9900,
  "currency": "USD",
  "idempotencyKey": "661f9511-f30c-42e5-b827-557766551111"
}

Default capture timeout is 5 minutes but is adjustable per merchant; it is measured from the order's last update. If no capture arrives in time, the order moves to Cancelled (status 2) and order.expired fires. orderId in the capture request must be the FlexFactor-returned GUID, not your external orderId.

A successful capture returns status: SUCCESS, not APPROVED. A processor capture failure returns HTTP 200 with status: FAILED while success stays true — key on status. See /capture.


2.3 MIT async purchase

Core MIT use case — recurring billing, unscheduled card on file (COF), or any merchant-initiated retry where the cardholder is not present. Asynchronous — POST /v1/evaluate returns SUBMITTED immediately; the rescue resolves via webhooks.

%%{init: {'theme': 'base', 'themeVariables': {'actorBkg': '#3C3C42', 'actorTextColor': '#FFFFFF', 'actorBorder': '#56565E', 'noteBkgColor': '#F8F8F8', 'noteTextColor': '#3C3C42', 'activationBkgColor': '#FF5B5B', 'activationBorderColor': '#E7575B', 'signalColor': '#56565E', 'signalTextColor': '#3C3C42'}}}%%
sequenceDiagram
    participant C as Integrator
    participant FF as FlexFactor API

    Note over C: Original decline occurs at primary processor
    C->>FF: POST /v1/evaluate
    Note over C,FF: isMIT: true, isRecurring: true (expiryDateUtc optional)

    FF-->>C: {result: "Success", status: "SUBMITTED", orderId: "ff-order-guid", orderSessionKey: "ff-session-guid"}
    Note over C: Store the orderId — resolution is async

    FF--)C: webhook: order.completed (rescue approved)
    FF--)C: webhook: order.cancelled (rescue declined)
    FF--)C: webhook: order.expired (rescue window elapsed)

    Note over C,FF: Polling fallback if webhook missed
    C->>FF: GET /v1/orders/{orderId}
    FF-->>C: Current order status

Minimal request (MIT-specific required fields)

POST /v1/evaluate
Authorization: Bearer {accessToken}
Content-Type: application/json

{
  "mid": "your-flexfactor-mid-guid",
  "isDeclined": true,
  "orderId": "your-ext-order-id",
  "siteId": "your-site-guid",
  "idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
  "isMIT": true,
  "isRecurring": true,
  "transactionType": "PURCHASE",
  "attemptCount": 3,
  "transaction": {
    "id": "txn-guid",
    "amount": 9900,
    "currency": "USD",
    "timestampUtc": "2026-06-12T14:30:00Z",
    "timezoneUtcOffset": -6,
    "responseCode": "51",
    "responseCodeSource": "processor",
    "avsResultCode": "Y",
    "cvvResultCode": "P",
    "cavvResultCode": "2",
    "cardNotPresent": true
  },
  "payer": { "email": "[email protected]" },
  "billingInformation": {
    "firstName": "Jane", "lastName": "Smith",
    "addressLine1": "123 Main St", "city": "Austin",
    "state": "TX", "zipcode": "78701",
    "country": "United States", "countryCode": "US"
  },
  "paymentMethod": {
    "cardNumber": "tok_sandbox_abc123", "token": true,
    "cardBinNumber": "424242", "cardLast4Digits": "4242",
    "expirationMonth": 12, "expirationYear": 2028,
    "holderName": "Jane Smith",
    "cardType": "credit", "cardBrand": "Visa", "cardCountry": "US"
  },
  "merchant": { "id": "merchant-guid", "mcc": 5999, "country": "US" }
}

expiryDateUtc is optional. When you send it, it is the deadline by which FlexFactor must complete the rescue — up to 21 days from the original decline. When you omit it, the default expiry configured for the merchant in FlexFactor applies. After the window closes the order is cancelled (status 2, Cancelled) and an order.expired webhook is emitted.

customerIp is not sent for MIT — the cardholder is not present and there is no client IP.

verificationValue (CVV) is generally not available for MIT. Omit it — sending a stale or empty CVV degrades rescue performance.

attemptCount is high-value for MIT retries — send the merchant's own attempt number for the order (1 = the first/original attempt, 2 = the first retry) so FlexFactor knows which retry it is receiving.

High-value MIT field — subscription object

{
  "subscription": {
    "subscriptionId": "sub-guid-or-merchant-id",
    "price": 9900,
    "currency": "USD",
    "interval": "monthly",
    "schemeTransactionId": "network-txn-id-from-original-cit"
  }
}

subscription.schemeTransactionId (the network transaction ID from the original CIT that established the credential) is the highest-impact field for rescue performance — include whenever available. The full subscription object is strongly recommended for all MIT flows.

Response

{"result": "Success", "status": "SUBMITTED", "orderId": "ff-3a7b2c1d-...", "orderSessionKey": "ff-9e8d7c6b-..."}

Webhook resolution

WebhookMeaningAction
order.completedRescue approved — funds capturedMark order as paid
order.cancelledRescue declined — no further attemptsReturn to dunning or write off
order.expiredRescue window passed without resolutionTreat as failed

2.4 MIT async authorize

Auth-only MIT variant — requires the authorize capability enabled on the FlexFactor account (not default). Resolves asynchronously via order.capturerequired webhook; explicit /capture call required.

%%{init: {'theme': 'base', 'themeVariables': {'actorBkg': '#3C3C42', 'actorTextColor': '#FFFFFF', 'actorBorder': '#56565E', 'noteBkgColor': '#F8F8F8', 'noteTextColor': '#3C3C42', 'activationBkgColor': '#FF5B5B', 'activationBorderColor': '#E7575B', 'signalColor': '#56565E', 'signalTextColor': '#3C3C42'}}}%%
sequenceDiagram
    participant C as Integrator
    participant FF as FlexFactor API

    C->>FF: POST /v1/evaluate
    Note over C,FF: isMIT: true, isRecurring: true,<br/>transactionType: "AUTHORIZATION" (expiryDateUtc optional)
    FF-->>C: {result: "Success", status: "SUBMITTED", orderId: "ff-order-guid", orderSessionKey: "ff-session-guid"}

    FF--)C: webhook: order.capturerequired (auth approved)
    C->>FF: POST /v1/capture
    Note over C,FF: {orderId, amount, currency, idempotencyKey}
    FF-->>C: {status: "SUCCESS", success: true}

Evaluate request — same as MIT async purchase with one change:

{
  "transactionType": "AUTHORIZATION"
}

Capture request — identical to the capture call in section 2.2.


2.5 Sync MIT

Retry-driven synchronous MIT variant — emulates a traditional retry workflow where the card is attempted multiple times over several days. Every response is immediate (like CIT), but resolution happens across multiple /evaluate calls, not one: the initial call always returns DECLINED, FlexFactor works the rescue in the background, and the existing merchant retry cadence re-calls /evaluate until the order is rescued (APPROVED) or the rescue window passes. A rescued order must still be captured by a further call — see Capture is conditional below.

Requires FlexFactor back-end configuration on the account — not available by default.

Rescue window. expiryDateUtc is optional on this flow and normally omitted. When it is omitted, the window comes from your FlexFactor portal configuration: a default expiration period expressed in hours from the timestamp the decline was received. A supplied expiryDateUtc is honored on a direct API integration, but that is not the typical setup here. If you reach FlexFactor through a gateway, the field is not conveyed and the portal-configured window always applies.

Note the unit. Async MIT uses a window of up to 21 days; the Sync MIT default is measured in hours. Compare the configured period against your retry schedule — a dunning cadence measured in days will routinely place your next /evaluate after the window has closed.

The cutoff is hard. There is no grace period. A call arriving after the window closes returns DECLINED even when the order was rescued and awaiting capture, so an uncaptured rescue cannot be recovered by capturing late.

Critical requirement: FlexFactor must be the only rescue mechanism in play for this flow. No other cascades, retry engines, or authorization attempts may run concurrently — doing so risks duplicate charges. Confirm all other recovery/cascade logic is disabled for merchants using Sync MIT.

%%{init: {'theme': 'base', 'themeVariables': {'actorBkg': '#3C3C42', 'actorTextColor': '#FFFFFF', 'actorBorder': '#56565E', 'noteBkgColor': '#F8F8F8', 'noteTextColor': '#3C3C42', 'activationBkgColor': '#FF5B5B', 'activationBorderColor': '#E7575B', 'signalColor': '#56565E', 'signalTextColor': '#3C3C42'}}}%%
sequenceDiagram
    participant C as Integrator
    participant FF as FlexFactor API

    Note over C: Original decline occurs at primary processor
    C->>FF: POST /v1/evaluate (initial attempt)
    Note over C,FF: isMIT: true, isRecurring: true<br/>(expiryDateUtc optional — normally omitted.<br/>The window is portal-configured in hours from the decline)
    FF-->>C: {result: "Success", status: "DECLINED"}
    Note over C,FF: The initial call ALWAYS returns DECLINED — expected behavior.<br/>FlexFactor continues working the rescue in the background.
    C->>C: Return original decline — continue normal retry cadence

    loop Merchant retry schedule — same orderId on every retry
        C->>FF: POST /v1/evaluate (retry)
        Note over C,FF: Matched to the original order via orderId,<br/>amount, payer, and payment instrument —<br/>no new order is created
        alt Still processing
            FF-->>C: {result: "Success", status: "DECLINED"}
            C->>C: Continue retry cadence
        else Rescue window passed without a rescue
            FF-->>C: {result: "Success", status: "DECLINED"}
            FF--)C: webhook: order.expired (order → Cancelled)
            C->>C: Terminal — stop retries
        else Rescued (order is now CAPTUREREQUIRED)
            Note over FF: Nothing is captured until a call completes the order.
            FF-->>C: {result: "Success", status: "APPROVED", orderId: "ff-order-guid", orderSessionKey: "ff-session-guid"}
            Note over C,FF: This call performed the capture.
            C->>C: Update order as approved — stop retries
        end
    end

    Note over C,FF: If the rescue lands in CAPTUREREQUIRED after your final retry,<br/>no call ever completes the order — it is never captured, and every<br/>response you saw was DECLINED. Consume order.capturerequired to prevent this.
    FF--)C: order.capturerequired
    C->>FF: POST /v1/capture (or one further /evaluate)
    FF-->>C: Order completed

A retry /evaluate against an order in CAPTUREREQUIRED state acts as the /capture call and returns APPROVED — this evaluate-as-capture behavior is unique to Sync MIT. An explicit POST /v1/capture also completes the order and is equally valid on this flow.

Capture is conditional — plan for three outcomes, not two

A rescued order rests in CAPTUREREQUIRED until one of those two calls completes it. FlexFactor does not initiate the capture itself, which means a successful rescue has three possible endings:

OutcomeWhat you observeFinal order state
Rescued and capturedA retry returns APPROVED; that call performed the captureCompleted (status 7)
Never rescued — the window elapsedEvery retry returns DECLINEDCancelled (status 2), order.expired fires
Rescued but never capturedEvery retry returns DECLINED — identical to the row aboveCancelled (status 2), order.expired fires — also identical

The third outcome occurs when no /evaluate or /capture call reaches the order before the rescue window elapses — most commonly when the rescue lands after your dunning cadence has made its final attempt, or when the schedule is sparse enough that the next call falls outside the window. The rescue succeeded, but the funds are never collected.

The last two rows are indistinguishable after the fact. Same responses, same final state, same webhook. The only signal that separates a rescue lost at the capture step from a rescue that never happened is whether order.capturerequired fired for that order earlier — which is a second reason to consume it.

Recommended: subscribe to order.capturerequired and complete the order on receipt — via POST /v1/capture or one further /evaluate — rather than relying solely on the next scheduled retry. Your retry cadence is not visible to FlexFactor, so it cannot close this gap for you. Confirm during onboarding that your retry schedule outlasts the configured rescue window.

If you reach FlexFactor through a gateway rather than directly, the gateway standard capture serves the same purpose — it is relayed to FlexFactor as the capture for the order.

Key differences from async MIT

Async MITSync MIT
expiryDateUtcOptional — merchant default expiry applies when omitted (up to 21 days)Optional — normally omitted; window comes from portal configuration as a period in hours from the decline. Not conveyed on gateway-routed integrations
Initial responseAlways SUBMITTEDAlways DECLINED — expected; FlexFactor keeps working the rescue in the background
ResolutionWebhooks (order.completed / cancelled / expired)Merchant retry cadence — each retry /evaluate returns DECLINED until rescued, then APPROVED
Retry matchingn/a — single submission per orderSame orderId on every retry — matched to the original order; no new order is created
Pending state / webhooksRequiredRetry logic drives the flow, but consume order.capturerequired as a completion backstop
CaptureExplicit /capture (authorize variant)Conditional — a retry /evaluate acts as the capture, or call /capture explicitly. Absent either, the order is never captured
transactionType effectRespectedAuth/capture always, regardless of transactionType

2.6 Cancel

Cancel (void) a rescue order that has not yet been captured. Behaves like a VOID — it stops an in-flight rescue rather than reversing a settled charge. Applicable to:

  • MIT orders in Draft, Onhold, Processing, or Capturerequired state — any state that is not Completed or Cancelled
  • CIT orders in Capturerequired or Onhold state

To reverse a charge that has already been captured/settled, use the Refund flow (§2.7) instead.

Two endpoints are available, differing only in which identifier they key on:

EndpointID usedWhen to use
POST /v1/orders/{orderId}/cancelFlexFactor order GUIDStandard cancellation once the FlexFactor orderId is known
POST /v1/orders/{external-order-id}/external-cancelMerchant's own order IDStandard cancellation keyed by your external order ID — usable at any time; especially useful when the FlexFactor orderId was never received (e.g., an /evaluate timeout)
%%{init: {'theme': 'base', 'themeVariables': {'actorBkg': '#3C3C42', 'actorTextColor': '#FFFFFF', 'actorBorder': '#56565E', 'noteBkgColor': '#F8F8F8', 'noteTextColor': '#3C3C42', 'activationBkgColor': '#FF5B5B', 'activationBorderColor': '#E7575B', 'signalColor': '#56565E', 'signalTextColor': '#3C3C42'}}}%%
sequenceDiagram
    participant C as Integrator
    participant FF as FlexFactor API

    alt Cancel by FlexFactor orderId
        C->>FF: POST /v1/orders/{orderId}/cancel
        Note over C,FF: Body: {} (empty)
        FF-->>C: 200 OK — {success: true, responseMessage: null}
    else Cancel by external order ID (e.g., FlexFactor orderId never received)
        C->>FF: POST /v1/orders/{external-order-id}/external-cancel
        Note over C,FF: Empty request body
        FF-->>C: 200 OK — {success: true, responseMessage: null}
    end
    C->>C: Update order as cancelled

Option A — cancel by FlexFactor order ID

POST /v1/orders/{orderId}/cancel
Authorization: Bearer {accessToken}
Content-Type: application/json

{}
HTTP 200 OK
{ "success": true, "responseMessage": null }

Option B — external-cancel (by external order ID)

Identifies the order by the merchant's own external order ID and can be used at any time — especially useful when the FlexFactor orderId was never received (e.g., the /evaluate response timed out).

POST /v1/orders/{external-order-id}/external-cancel
Authorization: Bearer {accessToken}

{}
HTTP 200 OK
{ "success": true, "responseMessage": null }

A 200 does not by itself prove the order was cancelled. A cancel against an order outside the cancellable states also returns 200 with success: true and leaves the order unchanged. Confirm with GET /v1/orders/{id} (status 2) or the order.cancelled webhook — see /cancel.


2.7 Refund

Refund a rescued transaction. FlexFactor refunds the cardholder directly (as MoR) and adjusts merchant settlement accordingly.

%%{init: {'theme': 'base', 'themeVariables': {'actorBkg': '#3C3C42', 'actorTextColor': '#FFFFFF', 'actorBorder': '#56565E', 'noteBkgColor': '#F8F8F8', 'noteTextColor': '#3C3C42', 'activationBkgColor': '#FF5B5B', 'activationBorderColor': '#E7575B', 'signalColor': '#56565E', 'signalTextColor': '#3C3C42'}}}%%
sequenceDiagram
    participant C as Integrator
    participant FF as FlexFactor API
    participant CH as Cardholder (bank)

    C->>FF: POST /v1/orders/{orderId}/refund
    Note over C,FF: {amountToRefund: 99.00, refundMessage: "..."}
    FF-->>C: 200 OK
    FF-->>CH: Refund issued to card
    FF--)C: webhook: order.refunded (async)
    C->>C: Update order as refunded

Request

POST /v1/orders/{orderId}/refund
Authorization: Bearer {accessToken}
Content-Type: application/json

{
  "amountToRefund": 99.00,
  "refundMessage": "Customer requested refund"
}

amountToRefund is in dollars, not cents. Partial refunds are supported. A failed refund normally returns HTTP 200 with status: FAILED and success: false — see /refund. Full refunds are de-duplicated by order ID at the platform level; partial refunds are not — always send an idempotencyKey with partial refunds.

refundMessage is optional.


2.8 Order inquiry

Retrieve the current status of a rescue order by its FlexFactor-assigned orderId.

%%{init: {'theme': 'base', 'themeVariables': {'actorBkg': '#3C3C42', 'actorTextColor': '#FFFFFF', 'actorBorder': '#56565E', 'noteBkgColor': '#F8F8F8', 'noteTextColor': '#3C3C42', 'activationBkgColor': '#FF5B5B', 'activationBorderColor': '#E7575B', 'signalColor': '#56565E', 'signalTextColor': '#3C3C42'}}}%%
sequenceDiagram
    participant C as Integrator
    participant FF as FlexFactor API

    C->>FF: GET /v1/orders/{orderId}
    FF-->>C: Order record with current status

    Note over C,FF: Also available: GET /v1/orders<br/>with search filters

Request

GET /v1/orders/{orderId}
Authorization: Bearer {accessToken}

3. Order inquiry & timeout handling

3.1 What happens when the orderId is not returned due to a timeout

If a POST /v1/evaluate call times out before the response arrives, the integrator has not received the FlexFactor-assigned orderId. The recovery path:

%%{init: {'theme': 'base', 'themeVariables': {'actorBkg': '#3C3C42', 'actorTextColor': '#FFFFFF', 'actorBorder': '#56565E', 'noteBkgColor': '#F8F8F8', 'noteTextColor': '#3C3C42', 'activationBkgColor': '#FF5B5B', 'activationBorderColor': '#E7575B', 'signalColor': '#56565E', 'signalTextColor': '#3C3C42'}}}%%
sequenceDiagram
    participant C as Integrator
    participant FF as FlexFactor API

    C->>FF: POST /v1/evaluate (idempotencyKey: "abc-guid-123")
    Note over C,FF: Network timeout — no response received

    C->>C: Record: idempotencyKey = "abc-guid-123"<br/>and your external orderId

    alt Recovery: retry with the same idempotencyKey
        C->>FF: POST /v1/evaluate (idempotencyKey: "abc-guid-123")
        FF-->>C: Identical response to original<br/>(no duplicate order created)
        C->>C: Process response normally — capture orderId
    end

    alt Recovery: search by external orderId
        C->>FF: GET /v1/orders (filter by external orderId)
        FF-->>C: Matching order record with status
        C->>C: Determine final state from order status
    end

    alt Ongoing monitoring: poll by FlexFactor orderId (once known)
        C->>FF: GET /v1/orders/{ff-orderId}
        FF-->>C: Current order status
    end

Key points

ScenarioBehavior
Retry with same idempotencyKeyFlexFactor returns the identical response — no duplicate order is created
New idempotencyKey on retryA new order evaluation is created — use only if you are certain the first was never processed
GET /v1/orders/{id}Authoritative status lookup by FlexFactor orderId — use once orderId is known
GET /v1/orders with filtersSearch by your external orderId to find orders when the FF orderId was never received

The idempotencyKey is the primary safety net for retries. Always persist it before making the API call so it survives a crash or timeout.

3.2 Retry patterns

HTTP CodeRecommended action
200 / 201Process response normally — no retry needed
401Refresh access token; retry the request once
409 in-flightFirst request still processing — retry the same idempotencyKey after a short backoff (see §3.4)
409 conflictReused key with a different payload — do not retry; use a unique key per operation (see §3.4)
500 / 502 / 504Retry with exponential backoff — max 3 attempts
503Do not retry — fall back to original decline. (A 503 from the idempotency store is the exception — it carries Retry-After and is safe to retry; see §3.4.)
400Do not retry — fix the request payload
Timeout / no responseRetry with the same idempotencyKey

3.3 Determining final transaction status after a timeout or uncertain state

Use GET /v1/orders/{id} as the authoritative source of truth for any order after a timeout or uncertain state.

What GET /v1/orders/{id} returns

The response carries a numeric status and a matching statusName:

statusstatusNameMeaningAction
0DraftMIT order received, not yet processingPending — await webhook / keep polling
1OnholdOrder temporarily on holdPending — keep polling
2CancelledOrder was cancelled (voided)Treat as not rescued
3ProblemA processing problem occurredTreat as not rescued; investigate
4ProcessingRescue in progressPending — await webhook / keep polling
5CapturerequiredAuth succeeded; capture requiredCall /capture to complete. On Sync MIT a further /evaluate also completes it — but one of the two must arrive, or the order is never captured
6ReturnedDefined in the enum; a refund does not assign itNot returned today — a refunded order stays 7; read refundsAmount
7CompletedRescue succeeded — funds captured. Stays 7 after a full or partial refundMark as paid; reconcile refunds from refundsAmount and order.refunded

An MIT order that exceeds its rescue window resolves to Cancelled (status 2); the order.expired webhook still fires so you can distinguish expiry from a merchant/system cancellation. This applies whether the window came from expiryDateUtc (async MIT) or from portal configuration (Sync MIT — see section 2.5). An authorization that is not captured within its capture window resolves the same way: Cancelled with order.expired.

On Sync MIT, note that a rescued order that was never captured also lands here. Cancelled + order.expired therefore does not by itself mean the rescue failed — see Capture is conditional in section 2.5.

The /evaluate response status (APPROVED, DECLINED, CHALLENGE, SUBMITTED, CAPTUREREQUIRED) is a separate field from the order statusName above — GET /orders does not return those words.

For CIT flows, the final state is always synchronous (APPROVED, DECLINED, or CHALLENGE) — a timeout on a CIT evaluate call means the response was lost in transit; retry with the same idempotencyKey to recover the deterministic result.

3.4 Idempotency reference

Idempotency lets you retry a request after a timeout or network error without performing the operation twice. When you attach the same key to a retry, FlexFactor recognizes the retry and returns the original result instead of executing the operation a second time.

Sending the key. The idempotency key is a GUID sent in the JSON request body as idempotencyKey — it is not an HTTP header.

{ "idempotencyKey": "3f9a2c14-8b6d-4e2a-9f17-2c5d8e0a1b34" }
  • Generate one key per logical operation (e.g., one key for a specific capture of a specific order).
  • Reuse the same key on every retry of that operation — this is what makes the retry safe.
  • Do not reuse a key for a different operation. A reused key carrying a materially different payload is rejected as a 409 conflict.

Per-endpoint behavior

EndpointKeyBehavior
POST /v1/evaluateRequired24-hour replay of the original response plus conflict detection.
POST /v1/captureRequiredA double capture is always prevented on (orderId, amount, currency). The key adds same-key replay and different-key conflict detection.
POST /v1/orders/{id}/refundOptional — recommended for partial refundsFull refunds are de-duplicated by order ID even with no key. Partial refunds are not, so send a key to make a partial-refund retry safe. The key replays the original response and detects conflicts. A refund the processor declined is not stored against the key, so a same-key retry runs the refund again; an unexpected failure (REFUND_FAILED) is stored and replayed without re-running the refund.
POST /v1/orders/{id}/cancelOptionalCancel is guarded on the order. The key adds replay and conflict detection.
POST /v1/orders/{external-order-id}/external-cancelOptionalKeyed by the same underlying order as cancel — issuing both a cancel and an external-cancel for one order collapses into a single operation.

Even without a key, several operations are protected by natural keys: capture on (orderId, amount, currency), and full refunds and cancel on the order itself. Partial refunds are the exception — they are not auto-deduplicated, so always send an idempotencyKey on a partial refund. On /evaluate and /capture the key is required; on cancel and external-cancel it is optional but recommended — it lets you replay the exact original response and surfaces a reused-key conflict.

Replay window and scope

  • 24-hour replay window. A completed operation's result is retained for 24 hours. A duplicate received within that window returns the stored original response. After 24 hours the key expires and the same key is treated as fresh.
  • Per-merchant scope. Keys and duplicate detection are scoped to your authenticated merchant account — your keys never collide with another merchant's, and you only ever replay your own responses.

Response reference

SituationStatusWhat you receive
Fresh request200Processed normally; returns its result body.
Duplicate within 24h200The original response is replayed exactly; the operation does not run again.
Concurrent duplicate still in progress409"request already in progress, retry shortly" — the first request is not yet finished.
Same key + materially different payload409The key was reused for a different operation (client bug).
Prior attempt crashed with unknown outcome500Will not auto-retry until the key expires (24h). Investigate before retrying.
Idempotency store temporarily unavailable503Includes a Retry-After header; the request is safe to retry.
Resource not found404The target resource does not exist.
Deterministic validation error400Validation failures are replayed consistently as 400.

Retry guidance

  • 409 in-flight ("request already in progress") — your first request is still processing. Retry the same request (same key) after a short backoff.
  • 503 idempotency store unavailable — honor the Retry-After header and retry the same request; it is safe to do so.
  • 500 crashed, unknown outcome — the same key keeps returning 500 until it expires (24h). Investigate before retrying; do not auto-retry.
  • 409 conflict (reused key) — not retryable. Fix the integration to use a unique key per logical operation.

The 503 described here is specific to the idempotency store and carries a Retry-After header — it is distinct from the rescue-availability 503 on /evaluate (see §5 and Error Handling), which means FlexFactor cannot rescue the transaction right now and should not be retried.


3.5 Response envelope and transaction state model

This section defines the fields every FlexFactor response shares, what each HTTP status code means, and how the status values returned by /evaluate, /capture, GET /orders, and webhooks map to one another. The endpoint pages link here rather than restating it.

Response envelope. Every POST endpoint that returns a body uses the same envelope, but not every endpoint populates every field. status describes the outcome of the operation you asked for, and its vocabulary differs by endpoint (see the next table). Key your integration logic on status.

FieldTypeMeaning
successbooleantrue when errors is empty. On /evaluate it is false only when the evaluation did not complete. On /capture it stays true when the processor declines the capture, so it does not signal the capture outcome.
resultstringPopulated on /evaluate only: Success when the evaluation completed, Failed when it did not (mixed case). Always null on /capture, /refund, and /cancel. Never the transaction outcome.
statusstringOutcome of the requested operation. See the per-endpoint vocabulary below.
statusCodestringFree-text field that no payment or order endpoint populates. Always null. It is not the HTTP status code.
errorsarray of objectsError details. Empty on success. Each item is serialized positionally: item1 message, item2 key, item3 error code, item4 friendly flag (boolean).
customPropertiesobjectAdditional properties. Empty in the documented flows.

status vocabulary by endpoint

Endpointstatus valuesOther body fields
POST /v1/evaluateAPPROVED, DECLINED, CHALLENGE, CAPTUREREQUIRED (synchronous flows); SUBMITTED (asynchronous MIT). DECLINED is also the fallback when result is Failed.orderId, orderSessionKey, senseKey, result
POST /v1/captureSUCCESS, FAILED — the capture outcome. A processor capture failure returns HTTP 200 with status: FAILED and success: true — see /capture.— (result is always null)
POST /v1/orders/{id}/refundSUCCESS, FAILED — the refund outcome. A failed refund normally returns HTTP 200 with status: FAILED — see /refund.responseCode, responseMessage, transactionId (result is always null)
POST /v1/orders/{id}/cancel, POST /v1/orders/{external-order-id}/external-cancelNo status. The body is { "success": true, "responseMessage": null } on HTTP 200 — also when the order was not in a cancellable state and nothing changed. See /cancel.success, responseMessage
GET /v1/orders, GET /v1/orders/{id}Not used on the envelope (null). The order record carries a numeric status (0 to 7) with a matching statusName — see §3.3.Order record

HTTP status code versus success, result, and status

You receiveMeaningWhat to do
HTTP 200 on /evaluate with result: SuccessFlexFactor completed the evaluation. The transaction outcome is in status. A declined rescue is HTTP 200 with status: DECLINED.Key your integration logic on status.
HTTP 200 on /evaluate with result: FailedThe evaluation did not complete: an evaluation timeout, a concurrent evaluation of the same order, or an unexpected error. status is DECLINED as a fallback and success is false.Return the original decline to the merchant. Do not treat it as a FlexFactor decision. To learn whether an order record was created, search GET /v1/orders by your external order ID (§3.1).
HTTP 200 on /capture with status: FAILEDThe processor did not capture the funds. success is still true.Treat the capture as failed. The order stays in Capturerequired until the capture window elapses (§2.2).
HTTP 200 on /refund with status: FAILEDThe refund was not applied. responseCode and responseMessage carry the processor result; an unexpected failure carries REFUND_FAILED in errors.Treat it as the refund outcome and retry if appropriate — see /refund.
HTTP 200 on /cancelThe cancel request was accepted. The order is now cancelled, was already cancelled, or was not in a cancellable state and is unchanged.Confirm with GET /v1/orders/{id} (status 2) or the order.cancelled webhook.
HTTP 4xxFlexFactor rejected the request. 400 carries validation details (problem details, or on /evaluate request validation a field-to-messages map with traceId); 404 is an unknown order (empty body on /capture and /cancel); 409 is an idempotency conflict. See Error Handling.Fix the request. Do not retry the same payload on 400. Follow §3.2 for 409.
HTTP 5xxFlexFactor did not process the request. 500 is problem details — or, on /capture, the plain text Capture call failed, and on /cancel, { "success": false, "responseMessage": "Failed to cancel order" }. 503 with Retry-After is the idempotency store.Follow the retry rules in §3.2. Quote traceId when contacting support.

HTTP 200 means the request reached FlexFactor, not that the transaction was approved, captured, refunded, or cancelled. Read status on /evaluate, /capture, and /refund, and confirm a cancel with GET /v1/orders/{id}.

Transaction state model

The table maps each state across the three places it can surface: the synchronous /evaluate or /capture response, the order record returned by GET /v1/orders/{id}, and webhooks. GET /v1/orders/{id} is authoritative when the sources disagree or a response was lost (§3.3). Webhooks notify you of a transition; deduplicate them on IdempotencyKey and treat IsResent: true as a redelivery.

StateSynchronous responseOrder status / statusName on GET /ordersWebhookTerminal?Your action
Received for asynchronous processing (async MIT)/evaluate status: SUBMITTED0 Draft, then 4 Processing (1 Onhold is possible — see §3.3)None until the order resolvesNoHold the order in a pending state. Pause your own retries.
Customer action required (CIT)/evaluate status: CHALLENGE—challenge.presented, then challenge.attempted, challenge.passed, or challenge.failedNoPresent the challenge if you embed the UI Widget. Otherwise treat it as a decline (§2.1).
Authorized, capture pending/evaluate status: CAPTUREREQUIRED (CIT authorize). Async MIT authorize and Sync MIT reach this state without a synchronous CAPTUREREQUIRED response.5 Capturerequiredorder.capturerequiredNoCall POST /v1/capture within the capture window — 5 minutes by default from the order's last update, adjustable per merchant (§2.2). On Sync MIT a further /evaluate also completes the order (§2.5).
Rescued and captured/evaluate status: APPROVED (CIT purchase; a Sync MIT retry that completed the order), or /capture status: SUCCESS7 Completedorder.completed — on every flow, delivered asynchronously after the API responseYesMark the order as paid.
Not rescued (synchronous)/evaluate status: DECLINED with result: Success— (an order record can exist in a not-eligible or cancelled state)—Yes for CIT. On Sync MIT, DECLINED is also the response while the rescue is still in progress (§2.5).Return the original decline to the merchant.
Evaluation did not complete/evaluate status: DECLINED with result: Failed, success: false———Return the original decline. See the HTTP table above.
Not rescued (asynchronous)—2 Cancelledorder.cancelledYesReturn to dunning or write off.
Rescue window or capture window elapsed—2 Cancelledorder.expiredYesTreat as not rescued. Covers an MIT order whose rescue window closed and any authorization (CIT authorize, MIT authorize, Sync MIT) not captured in time. On Sync MIT this also covers a rescued order that was never captured (§2.5).
Cancelled by you (/cancel, /external-cancel)HTTP 200, { "success": true, "responseMessage": null }2 Cancelledorder.cancelled (only when the order status actually changed)YesConfirm with GET /v1/orders/{id}, then mark the order as cancelled.
Processing problem—3 ProblemNone documented—Treat as not rescued. Contact [email protected] with the orderId.
Refunded (full or partial)/refund status: SUCCESS (FAILED with HTTP 200 when the refund did not go through)Stays 7 Completed; refundsAmount (dollars) increases. Status 6 Returned is defined but not assigned by a refund.order.refunded (EventData.Amount in cents)—Record the refund. Never issue a second refund in response to the webhook.
Chargeback received or reversed—Order status unchanged; the dispute appears in activityItems with type: chargebackpayment.chargeback.received, payment.chargeback.reversed—Handle the dispute in your merchant process.
Request not processedHTTP 4xx / 5xxUnchangedNone—Follow §3.2 and the HTTP table above.

Notes:

  • There is no Expired order status. Expiry resolves to 2 Cancelled, and order.expired is the signal that distinguishes it from a cancellation.
  • order.completed, order.cancelled, and order.expired are sent only when the order status actually changes and your endpoint subscribes to the event.
  • Refund and cancel transitions are in §2.6 and §2.7.

4. Tokenization confirmation

4.1 Tokenization is not required for PCI-scoped integrators

Integrators that are PCI-DSS certified and provide a valid PCI AOC to FlexFactor can send raw card data (full PAN and CVV) directly in the /evaluate payload. Tokenization is the default recommended path, but it is not required when the integrator has PCI scope and an AOC on file.

PathWho it applies topaymentMethod.tokencardNumber
Tokenized (default)Any integrator — no PCI scope expansiontrueFlexFactor token (GUID from POST /v1/tokenize)
Direct card dataPCI-DSS certified integrators with valid AOCfalse (or omitted)Raw PAN

Evaluate payload difference — direct card data path

{
  "paymentMethod": {
    "cardNumber": "xxxxxxxxxxxx1111",
    "token": false,
    "verificationValue": "123",
    "cardBinNumber": "411111",
    "cardLast4Digits": "1111",
    "expirationMonth": 12,
    "expirationYear": 2028
  }
}

All other payload fields remain identical to the tokenized path.

FlexFactor must enable direct card data on the integrator account before it is active. Provide your PCI AOC to the FlexFactor integration team at [email protected] to request activation.


5. Error reference

HTTP CodeMeaningAction
200, 201, 204SuccessProcess response normally
400Validation errorParse errors object for field-level details — do not retry
401Invalid / expired tokenRefresh accessToken; retry once
403Access deniedCheck permissions — do not retry
405Method not allowedCheck HTTP method
500Server errorRetry with exponential backoff (max 3 attempts)
502Bad gatewayRetry with exponential backoff
503Service unavailableFall back to original decline — do not retry
504Gateway timeoutUpstream did not respond in time — retry with exponential backoff (max 3)

Error response shape

{
  "status": 400,
  "title": "One or more validation errors occurred.",
  "traceId": "00-64a5cf4492a4f17b...",
  "errors": {
    "transaction.amount": ["The field 'amount' must be greater than 0."]
  }
}

6. Sandbox vs production — known differences

ItemSandboxProduction
Base URLapi-sandbox.flexfactor.ioapi.flexfactor.io
AppKeySandbox-issued AppKeyProduction-issued AppKey
AppSecretSandbox-issued AppSecretProduction-issued AppSecret
MIDSandbox MID (GUID)Production MID (GUID)
SiteIDSandbox SiteID(s) scoped to sandbox MIDProduction SiteID(s) scoped to production MID
Webhook source IPs35.172.5.145, 3.211.16.18018.233.184.33, 52.4.241.101

AppKey, AppSecret, MID, and SiteID are all environment-specific — none carry over from sandbox to production. SiteIDs are scoped to their parent MID; a merchant with multiple brands or descriptors will have one SiteID per descriptor, and each must be provisioned separately in production. Ensure all four are swapped as part of the production cutover checklist.

For any behavioral differences between sandbox and production not covered in the published documentation, raise them directly with the FlexFactor integration team at [email protected].


7. Integration support

All technical integration questions: [email protected]


Did this page help you?