Skip to main content

Forwarding

note

Forwarding is in beta. Contact JustiFi Customer Success to have it enabled for your platform and to get a destination allow-listed.

Forwarding allows you to forward any card payment method stored with JustiFi to a third party. You supply the body and headers the destination requires and mark where the card details belong; JustiFi substitutes the real values and sends a request to the destination.

Forwarding is asynchronous. Creating a forwarding request returns immediately, before anything leaves JustiFi, and the outcome arrives by webhook or by retrieving the request.

How it works​

  1. You have a card payment method in JustiFi — any card payment method, however it was created, including one tokenized by the Tokenize Payment Method web component or the Checkout web component.
  2. You create a forwarding request naming the payment method, the destination, and the body and headers the destination expects. Card details are written as {{card_number}}, {{card_expiry_month}}, {{card_expiry_year}} and {{cardholder_name}} tags.
  3. JustiFi answers 201 with the request in pending status, then sends the request to the destination in the background; it swaps each tag for the real value, encodes the body in the format the destination expects, and relays your headers.
  4. You get the outcome from the forwarding_request.completed or forwarding_request.failed event, or by retrieving the forwarding request.

Prerequisites​

The destination must be allow-listed by JustiFi. Forwarding will only send to destinations JustiFi has approved, matched exactly as a full URL — no trailing slash, casing or query string normalization is applied. Contact JustiFi Customer Success to have a destination added, and use exactly the URL you are given.

You supply the destination's credentials. JustiFi holds no credentials of its own for the destination. Any authorization required by the destination needs to be passed to the forwarding_request.headers and is relayed as-is.

Only card payments can be forwarded. Bank accounts, card-present payment methods and digital wallets (Apple Pay and Google Pay) are rejected.

Creating a forwarding request​

Create the request with Create a Forwarding Request. forwarding_request.body is the shape the destination expects, not a JustiFi shape — JustiFi passes it through, substituting only the tags it finds.

curl --request POST \
--url https://api.justifi.ai/v1/forwarding/requests \
--header 'authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'content-type: application/json' \
--header 'idempotency-key: YOUR_IDEMPOTENCY_KEY' \
--data '{
"payment_method": "pm_123xyz",
"url": "https://api.stripe.com/v1/payment_methods",
"forwarding_request": {
"body": {
"type": "card",
"card": {
"number": "{{card_number}}",
"exp_month": "{{card_expiry_month}}",
"exp_year": "{{card_expiry_year}}"
},
"billing_details": { "name": "{{cardholder_name}}" },
"metadata": { "reference": "ord_9f21" }
},
"headers": {
"Authorization": "Bearer sk_live_destination_key"
}
}
}'

No Sub-Account header is needed — the account is retrieved from the payment method.

You always write forwarding_request.body as a JSON object. JustiFi encodes it into the format the destination requires — form-encoding it, for instance, when the destination expects form data — and sets Content-Type to match, so you do not need to send one. If you do send Content-Type, yours is relayed unchanged and you are responsible for it matching the body JustiFi produces.

The response is the forwarding request in pending status, with response still null:

{
"id": "fwd_123xyz",
"type": "forwarding_request",
"data": {
"id": "fwd_123xyz",
"account_id": "acc_123xyz",
"payment_method_id": "pm_123xyz",
"url": "https://api.stripe.com/v1/payment_methods",
"http_method": "POST",
"provider": "stripe",
"status": "pending",
"failure_reason": null,
"replacements": ["card_number", "card_expiry_month", "card_expiry_year", "cardholder_name"],
"request": {
"body": {
"type": "card",
"card": { "number": "4242", "exp_month": 5, "exp_year": 2042 },
"billing_details": { "name": "Lindsay Whalen" },
"metadata": { "reference": "ord_9f21" }
},
"headers": { "Authorization": "[FILTERED]" }
},
"response": null,
"attempted_at": null,
"created_at": "2024-01-01T12:00:00Z",
"updated_at": "2024-01-01T12:00:00Z"
},
"page_info": null
}

replacements lists the tags JustiFi found and will substitute, which is a useful confirmation that your tags were read the way you intended.

Card tags​

TagSubstituted with
{{card_number}}the full card number
{{card_expiry_month}}the expiration month, as a number (5, not "05")
{{card_expiry_year}}the expiration year, as a four digit number (2042)
{{cardholder_name}}the cardholder name on the payment method

Three rules apply:

  • A tag must be the entire value of its field. "number": "{{card_number}}" works; "number": "card-{{card_number}}" is rejected. There is no string interpolation.
  • Unknown tags are rejected. Only the four tags above are supported.
  • {{card_cvc}} is not supported. JustiFi does not retain the CVV after the request that collected it. Destinations that require a CVV cannot be reached by forwarding.

Braces that are not a whole-value tag are left alone, so a value like "ref: {{legacy}}" passes through untouched. A body with no tags at all is accepted, and replacements comes back empty.

Getting the outcome​

Forwarding requests move through four statuses:

StatusMeaning
pendingaccepted and queued; nothing has been sent
processingthe request is being sent and the outcome is not yet known
completedthe destination answered — see response.status_code for the outcome
failedthe destination could not be reached — see failure_reason
note

completed means the destination answered, not that it accepted the request. A 402 from the destination is a completed forwarding request with response.status_code of 402. Always check response.status_code.

When a request failed, response stays null and failure_reason says why:

Failure ReasonMeaning
timeoutthe destination did not answer in time
connection_errorthe connection or TLS handshake to the destination failed
internal_errorJustiFi failed to send the request

Webhooks​

Subscribe to forwarding_request.completed and forwarding_request.failed. The event payload is the same object the API returns. Exactly one of the two is published per forwarding request. See Forwarding Request events.

Polling via API​

If you prefer polling, you can use the Get a Forwarding Request API and wait for the status to change from pending or processing to completed or failed.

curl --request GET \
--url https://api.justifi.ai/v1/forwarding/requests/fwd_123xyz \
--header 'authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'sub-account: acc_123xyz'

Once the destination has answered, response is populated:

{
"status": "completed",
"failure_reason": null,
"response": {
"id": "fwdr_123xyz",
"status_code": 200,
"body": {
"id": "pm_1QabcStripeExample",
"object": "payment_method",
"card": { "last4": "4242" }
},
"headers": { "content-type": "application/json" },
"response_time_ms": 512
},
"attempted_at": "2024-01-01T12:00:01Z"
}

List Forwarding Requests returns a sub account's forwarding requests newest first, and can be filtered by payment_method_id, created_before and created_after.

What JustiFi stores, and what you can read back​

Forwarding is designed so that reading a forwarding request back can never expose card data, no matter what you sent or what the destination returned:

  • Request body — stored with the tags still in it, never with the card details. When you read it back, the card number renders as its last four digits; the expiration date and cardholder name render in the clear. This is true wherever you put a tag, including somewhere unexpected like a metadata field.
  • Request headers — names are kept so you can confirm what was relayed, but every value renders as [FILTERED]. The destination credentials you send are never readable again, so keep your own copy.
  • Response body and headers — anything that looks like a card number is reduced to its last four digits before being stored, so a destination that echoes the card back cannot leak it through JustiFi.

Idempotency​

Idempotency-Key is required, and works as it does everywhere else in the JustiFi API. Retrying with the same key and the same parameters returns the same forwarding request and never sends a second request to the destination — which matters more here than usual, since the destination may create a record of its own. Reusing a key with different parameters returns 409.

Limits​

  • One attempt. A forwarding request that fails is not retried. To try again, create a new forwarding request with a new Idempotency-Key.
  • Timeouts. JustiFi waits up to 10 seconds to connect and up to 90 seconds for the destination to answer, then records timeout.
  • JSON request bodies only. forwarding_request.body must be a JSON object; a string or an array is rejected. forwarding_request.headers, if present, must be a flat object of string values. JustiFi handles encoding the body into the form the destination expects.
  • Cards only. Bank accounts, card-present payment methods and digital wallet cards cannot be forwarded.

Errors​

Every rejection happens before anything is created or sent, so a 400 means nothing reached the destination.

CodeMeaning
forwarding_destination_not_allowedthe url is not on the allow list, or does not match an allow-listed destination exactly
payment_method_type_not_supportedthe payment method is not a card
digital_wallet_not_supportedthe card is an Apple Pay or Google Pay card
invalid_parametera card tag is unknown, or is not the entire value of its field
forwarding_request_requiredforwarding_request is missing
forwarding_request_body_invalidforwarding_request.body is not a JSON object
forwarding_request_headers_invalidforwarding_request.headers is not an object of string values
payment_method_not_foundno payment method with that id exists (404)
forwarding_request_not_foundno forwarding request with that id exists on this sub account (404)
invalid_id_formatthe payment_method id is malformed
not_authorizedyour credentials have no admin access to the account that owns the payment method (403)