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).
| Credential | Type | Required | Description |
|---|---|---|---|
| MID | GUID | Required | FlexFactor Merchant Identification Number. Identifies the merchant account on every API call. |
| API Key | String | Required | Public client key (the AppKey) sent with the API Secret to /oauth2/token to obtain a Bearer token. |
| API Secret | String | Required | Client secret (the AppSecret) paired with the API Key to authenticate. Store securely server-side; never expose it to the client. |
| Tokenization Key | String | Optional | Used only by the /tokenize API — passed as the environment query parameter to vault card data. Not sent on any other call. |
| Site ID | GUID | Optional | Identifies 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
| Environment | Base URL |
|---|---|
| Sandbox | https://api-sandbox.flexfactor.io/v1/ |
| Production | https://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"
},
"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
| Field | Rank | Note |
|---|---|---|
paymentMethod.verificationValue | Critical | Directly drives rescue performance — include whenever available; absence has measurable negative impact |
customerIp | Critical (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, cardCountry | Required | Required fields; each contributes meaningful rescue signal |
paymentMethod.holderName | Required | Required per field reference |
transaction.responseCode, responseCodeSource, processorName | High | Significantly improve rescue performance; not required for launch |
deviceDetails (all child fields) | High | Significantly improve rescue performance when device context is available at decline time; not required for launch |
High-value optional fields
{
"transaction": {
"responseCode": "05",
"responseDescription": "Do Not Honor",
"responseCodeSource": "processor",
"processorName": "Stripe",
"avsResultCode": "Y",
"cvvResultCode": "M"
},
"payer": {
"phone": "5125550123"
},
"deviceDetails": {
"deviceType": "desktop",
"deviceName": "Chrome 124",
"deviceOS": "macOS 14",
"browser": "Chrome",
"userAgent": "Mozilla/5.0 ..."
}
}When
deviceDetailsis 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"}
resultreflects whether the API request succeeded (SUCCESS/FAILED).statusreflects the transaction outcome. Always key your integration logic onstatus.
transaction.amountis 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: {result: "SUCCESS", status: "APPROVED"}
C->>C: Update order as approved
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.
orderIdin the capture request must be the FlexFactor-returned GUID, not your external orderId.
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 set
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 (not rescued before expiryDateUtc)
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,
"expiryDateUtc": "2026-06-16T00:00:00Z",
"transactionType": "PURCHASE",
"attemptCount": 3,
"transaction": {
"id": "txn-guid",
"amount": 9900,
"currency": "USD"
},
"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" }
}
expiryDateUtcis the deadline by which FlexFactor must complete the rescue — recommended up to 21 days. After this date the order is cancelled (status 2, Cancelled) and anorder.expiredwebhook is emitted.
customerIpis 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.
attemptCountis 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 fullsubscriptionobject is strongly recommended for all MIT flows.
Response
{"result": "SUCCESS", "status": "SUBMITTED", "orderId": "ff-3a7b2c1d-...", "orderSessionKey": "ff-9e8d7c6b-..."}Webhook resolution
| Webhook | Meaning | Action |
|---|---|---|
order.completed | Rescue approved — funds captured | Mark order as paid |
order.cancelled | Rescue declined — no further attempts | Return to dunning or write off |
order.expired | expiryDateUtc passed without resolution | Treat 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 set
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: {result: "SUCCESS", status: "APPROVED"}
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 merchant's existing retry cadence re-calls /evaluate until the order is rescued (APPROVED) or the rescue window passes.
Requires FlexFactor back-end configuration on the account — not available by default. No expiryDateUtc is sent; the rescue window is controlled by the FlexFactor back-end configuration for this flow.
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/>(no expiryDateUtc)
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 (or rescue window passed)
FF-->>C: {result: "SUCCESS", status: "DECLINED"}
C->>C: Continue retry cadence
else Rescued
FF-->>C: {result: "SUCCESS", status: "APPROVED", orderId: "ff-order-guid", orderSessionKey: "ff-session-guid"}
C->>C: Update order as approved — stop retries
end
end
A retry
/evaluateagainst an order inCAPTUREREQUIREDstate automatically acts as the/capturecall and returnsAPPROVED— this auto-capture behavior is unique to Sync MIT. All other authorize flows require a separate, explicitPOST /v1/capture.
Key differences from async MIT
| Async MIT | Sync MIT | |
|---|---|---|
expiryDateUtc | Required | Omit — rescue window is controlled by FlexFactor back-end configuration |
| Initial response | Always SUBMITTED | Always DECLINED — expected; FlexFactor keeps working the rescue in the background |
| Resolution | Webhooks (order.completed / cancelled / expired) | Merchant retry cadence — each retry /evaluate returns DECLINED until rescued, then APPROVED |
| Retry matching | n/a — single submission per order | Same orderId on every retry — matched to the original order; no new order is created |
| Pending state / webhooks | Required | Not required — the existing retry logic drives the flow |
| Capture | Explicit /capture (authorize variant) | Automatic — a retry /evaluate against a CAPTUREREQUIRED order acts as the capture |
transactionType effect | Respected | Auth/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,Processing, orCAPTUREREQUIREDstate - CIT orders in
CAPTUREREQUIREDstate
To reverse a charge that has already been captured/settled, use the Refund flow (§2.7) instead.
Two endpoints are available depending on whether the FlexFactor orderId has been received:
| Endpoint | ID used | When to use |
|---|---|---|
POST /v1/orders/{orderId}/cancel | FlexFactor order GUID | Primary — standard cancellation once orderId is known |
POST /v1/orders/{external-order-id}/external-cancel | Merchant's own order ID | Timeout fallback — use when a cancel must be issued before the FlexFactor orderId has been received (e.g., evaluate response timed out) |
%%{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 FlexFactor orderId known (standard)
C->>FF: POST /v1/orders/{orderId}/cancel
Note over C,FF: Body: {} (empty)
FF-->>C: 200 OK — {}
else Timeout — orderId not yet received
C->>FF: POST /v1/orders/{external-order-id}/external-cancel
Note over C,FF: Empty request body
FF-->>C: 200 OK
end
C->>C: Update order as cancelled
Option A — cancel by FlexFactor order ID (primary)
POST /v1/orders/{orderId}/cancel
Authorization: Bearer {accessToken}
Content-Type: application/json
{}HTTP 200 OK
{}
Option B — external-cancel (timeout fallback)
Use when a cancel must be issued before the FlexFactor orderId has been received — identified by the merchant's own external order ID.
POST /v1/orders/{external-order-id}/external-cancel
Authorization: Bearer {accessToken}
{}
HTTP 200 OK
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"
}
amountToRefundis in dollars, not cents. Partial refunds are supported. Full refunds are de-duplicated by order ID at the platform level; partial refunds are not — always send anidempotencyKeywith partial refunds.
refundMessageis 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
| Scenario | Behavior |
|---|---|
Retry with same idempotencyKey | FlexFactor returns the identical response — no duplicate order is created |
New idempotencyKey on retry | A 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 filters | Search by your external orderId to find orders when the FF orderId was never received |
The
idempotencyKeyis 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 Code | Recommended action |
|---|---|
200 / 201 | Process response normally — no retry needed |
401 | Refresh access token; retry the request once |
409 in-flight | First request still processing — retry the same idempotencyKey after a short backoff (see §3.4) |
409 conflict | Reused key with a different payload — do not retry; use a unique key per operation (see §3.4) |
500 / 502 / 504 | Retry with exponential backoff — max 3 attempts |
503 | Do 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.) |
400 | Do not retry — fix the request payload |
| Timeout / no response | Retry 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:
status | statusName | Meaning | Action |
|---|---|---|---|
| 0 | Draft | MIT order received, not yet processing | Pending — await webhook / keep polling |
| 1 | Onhold | Order temporarily on hold | Pending — keep polling |
| 2 | Cancelled | Order was cancelled (voided) | Treat as not rescued |
| 3 | Problem | A processing problem occurred | Treat as not rescued; investigate |
| 4 | Processing | Rescue in progress | Pending — await webhook / keep polling |
| 5 | Capturerequired | Auth succeeded; capture required | Call /capture to complete |
| 6 | Returned | Order was refunded/returned | Treat as refunded |
| 7 | Completed | Rescue succeeded — funds captured | Mark as paid |
An MIT order that exceeds its
expiryDateUtcresolves toCancelled(status2); theorder.expiredwebhook still fires so you can distinguish expiry from a merchant/system cancellation.
The
/evaluateresponsestatus(APPROVED,DECLINED,CHALLENGE,SUBMITTED,CAPTUREREQUIRED) is a separate field from the orderstatusNameabove —GET /ordersdoes 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
409conflict.
Per-endpoint behavior
| Endpoint | Key | Behavior |
|---|---|---|
POST /v1/evaluate | Required | 24-hour replay of the original response plus conflict detection. |
POST /v1/capture | Required | A double capture is always prevented on (orderId, amount, currency). The key adds same-key replay and different-key conflict detection. |
POST /v1/orders/{id}/refund | Optional — recommended for partial refunds | Full 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. |
POST /v1/orders/{id}/cancel | Optional | Cancel is guarded on the order. The key adds replay and conflict detection. |
POST /v1/orders/{external-order-id}/external-cancel | Optional | Keyed 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 anidempotencyKeyon a partial refund. On/evaluateand/capturethe 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
| Situation | Status | What you receive |
|---|---|---|
| Fresh request | 200 | Processed normally; returns its result body. |
| Duplicate within 24h | 200 | The original response is replayed exactly; the operation does not run again. |
| Concurrent duplicate still in progress | 409 | "request already in progress, retry shortly" — the first request is not yet finished. |
| Same key + materially different payload | 409 | The key was reused for a different operation (client bug). |
| Prior attempt crashed with unknown outcome | 500 | Will not auto-retry until the key expires (24h). Investigate before retrying. |
| Idempotency store temporarily unavailable | 503 | Includes a Retry-After header; the request is safe to retry. |
| Resource not found | 404 | The target resource does not exist. |
| Deterministic validation error | 400 | Validation failures are replayed consistently as 400. |
Retry guidance
409in-flight ("request already in progress") — your first request is still processing. Retry the same request (same key) after a short backoff.503idempotency store unavailable — honor theRetry-Afterheader and retry the same request; it is safe to do so.500crashed, unknown outcome — the same key keeps returning500until it expires (24h). Investigate before retrying; do not auto-retry.409conflict (reused key) — not retryable. Fix the integration to use a unique key per logical operation.
The
503described here is specific to the idempotency store and carries aRetry-Afterheader — it is distinct from the rescue-availability503on/evaluate(see §5 and Error Handling), which means FlexFactor cannot rescue the transaction right now and should not be retried.
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.
| Path | Who it applies to | paymentMethod.token | cardNumber |
|---|---|---|---|
| Tokenized (default) | Any integrator — no PCI scope expansion | true | FlexFactor token (GUID from POST /v1/tokenize) |
| Direct card data | PCI-DSS certified integrators with valid AOC | false (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 Code | Meaning | Action |
|---|---|---|
| 200, 201, 204 | Success | Process response normally |
| 400 | Validation error | Parse errors object for field-level details — do not retry |
| 401 | Invalid / expired token | Refresh accessToken; retry once |
| 403 | Access denied | Check permissions — do not retry |
| 405 | Method not allowed | Check HTTP method |
| 500 | Server error | Retry with exponential backoff (max 3 attempts) |
| 502 | Bad gateway | Retry with exponential backoff |
| 503 | Service unavailable | Fall back to original decline — do not retry |
| 504 | Gateway timeout | Upstream 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
| Item | Sandbox | Production |
|---|---|---|
| Base URL | api-sandbox.flexfactor.io | api.flexfactor.io |
| AppKey | Sandbox-issued AppKey | Production-issued AppKey |
| AppSecret | Sandbox-issued AppSecret | Production-issued AppSecret |
| MID | Sandbox MID (GUID) | Production MID (GUID) |
| SiteID | Sandbox SiteID(s) scoped to sandbox MID | Production SiteID(s) scoped to production MID |
| Webhook source IPs | 35.172.5.145, 3.211.16.180 | 18.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]
Updated about 1 hour ago

