# Authentication Source: https://integration.wpay.com.au/guides/api/authentication API authentication basics and best practices. The API uses **Bearer (Token) Authentication** to authenticate any request. These tokens are **JSON Web Tokens (JWT)** which need to be created server side by the app. ## Create an API key To sign a JWT, you'll need to create a new API key. Please contact a Wpay representative to request API key generation. You can store the API key with your code or store it in a secure environment accessible to your app. Keys can also be provisioned and managed over the API. See [Managing API keys](./api-keys) to automate key provisioning at scale. ## Server-side SDKs > **[C# SDK](https://github.com/gr4vy/gr4vy-csharp)** > > Make server-side calls with the C# SDK. > **[Go SDK](https://github.com/gr4vy/gr4vy-go)** > > Make server-side calls with the Go SDK. > **[Java SDK](https://github.com/gr4vy/gr4vy-java)** > > Make server-side calls with the Java SDK. > **[PHP SDK](https://github.com/gr4vy/gr4vy-php)** > > Make server-side calls with the PHP SDK. > **[Python SDK](https://github.com/gr4vy/gr4vy-python)** > > Make server-side calls with the Python SDK. > **[TypeScript SDK](https://github.com/gr4vy/gr4vy-typescript)** > > Make server and client-side API calls with the TypeScript SDK. ## Mobile SDKs > **[Swift SDK](https://github.com/gr4vy/gr4vy-kotlin)** > > A low-level, client-side Swift SDK for those looking to build their own > custom user experiences on Apple devices. > **[Kotlin SDK](https://github.com/gr4vy/gr4vy-swift)** > > A low-level, client-side Kotlin SDK for those looking to build their own > custom user experiences on Android > **[Embed for iOS SDK](https://github.com/gr4vy/gr4vy-ios)** > > A fully embedded drop-in UI for Apple devices with support for card, > Apple Pay, and other payment methods. > **[Embed for Android SDK](https://github.com/gr4vy/gr4vy-android)** > > A fully embedded drop-in UI for Android devices with support for card, > Google Pay, and other payment methods. ## Development tools > **[CLI](/guides/tools/cli)** > > Create API tokens and checkout sessions with the command line interface tool. > **[Postman](/guides/tools/postman)** > > Start exploring the API using the official Postman collection. ## Without SDKs It is also [possible to generate the JWT](./jwts) without the SDKs using any number of open source JWT libraries. ## Troubleshoot a 401 response If the API returns a `401 Unauthorized` response for a token signed with a valid API key, check that you send the request to the host for the environment the key belongs to. The `sandbox` and `production` environments have separate infrastructure, so a key created in one environment doesn't work in the other. See [environments](/guides/api/environments) for the hostname of each environment. # API Compatibility Source: https://integration.wpay.com.au/guides/api/compatibility Learn how Gr4vy keeps the API backward compatible and how to keep your integration forward compatible. At Gr4vy, there is a commitment to providing a stable, reliable, and predictable API for merchants. The goal is to empower you to build robust integrations with confidence, knowing that your code won't break unexpectedly. This guide outlines the approach to API compatibility and how changes are managed. Industry best practices for REST APIs are followed to ensure a seamless experience. The strategy is centered on two key principles: backward compatibility for the API and forward compatibility for your client integration. ## Commitment: Backward compatibility Backward compatibility means that your existing code continues to work as the API is updated. This is ensured by strictly adhering to the following rules for any API updates: * **Additive Changes Only:** Only non-breaking, additive changes are introduced to the existing API. This includes: * Adding new API endpoints. * Adding new, optional request parameters to existing endpoints. * Adding new properties to API response bodies. * Adding new values to existing enum fields. * **No Breaking Changes (Without Notice):** Existing fields or endpoints are never removed or renamed in a backward-compatible update. Your current integration continues to function as expected. ## Your responsibility: Forward compatibility Forward compatibility is the responsibility of the client, and the easiest way to achieve this is through the SDKs. Forward compatibility means that client code should be resilient to the additive changes mentioned in the preceding section. The app should be able to handle unexpected properties in API responses without crashing or throwing errors. ### Using Gr4vy SDKs The easiest way to ensure forward compatibility is to use the official SDKs. They are designed to handle new response fields gracefully, allowing you to adopt new features at your own pace without being forced to update your code. ### Building your own integration If not using one of the SDKs, ensure that the HTTP client and JSON parser are configured to ignore unknown properties in the response body. Most standard libraries do this by default, but it is crucial to verify this behavior to prevent the integration from breaking when the API is enhanced. ### Deprecation signals Deprecated parameters and fields are marked as deprecated in the API reference. Some deprecations are also signaled in API responses. For example, when a [list transactions](/reference/transactions/list-transactions) request uses the deprecated `buyer_email_address` or `gift_card_last4` filter, the request still works and the response includes these headers. ```http theme={"system"} Deprecation: @1790640000 Link: ; rel="deprecation" ``` The `Deprecation` header holds the date of the deprecation as a Unix timestamp, as defined in [RFC 9745](https://www.rfc-editor.org/rfc/rfc9745). The `Link` header points to the documentation that describes the replacement. Log these headers to find and update code that relies on deprecated features. ### How breaking changes are handled While there is a strive to avoid them, there may be rare occasions where a breaking change is necessary to improve services, enhance security, or deprecate a feature. A breaking change is any modification that is not backward-compatible, such as removing a field or changing a data type. In these situations, the following is provided: * **Advance Notice:** Any planned breaking changes are communicated at least 30 days in advance. This notice is sent via email and Slack, and is posted on [the developer changelog](/updates). * **Clear Documentation:** Clear documentation and a migration guide are provided to help you adapt your code to the new update. * **Collaborative Support:** It is understood that every team's development cycle is different. If the 30-day window presents a significant challenge for your organization, there is collaboration to find a viable solution. The support team is here to help you through the transition. By following these principles, the aim is to provide a powerful and stable platform to build on for years to come. # Connectivity and TLS Source: https://integration.wpay.com.au/guides/api/connectivity Review the TLS and security requirements for integrating with the Gr4vy API. ## TLS versions Gr4vy supports and requires **TLS 1.2** or **TLS 1.3** for all API connections. To ensure the security of data and maintain PCI compliance, older protocols are strictly unsupported: * SSL (all versions) * TLS 1.0 * TLS 1.1 ## Cipher suites Gr4vy accepts connections using the following cipher suites. Please ensure your client supports at least one of these suites. | TLS Version | Cipher Suite | | :- | :- | | **TLS 1.3** | `TLS_AES_128_GCM_SHA256` | | | `TLS_AES_256_GCM_SHA384` | | | `TLS_CHACHA20_POLY1305_SHA256` | | **TLS 1.2** | `TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256` | | | `TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256` | | | `TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256` | | | `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256` | | | `TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384` | | | `TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384` | ## Server name indication (SNI) Server name indication (SNI) is **required** for all API requests. Your HTTP client must support and send the SNI extension during the TLS handshake to ensure the correct certificate is presented. ## Webhooks For Gr4vy to successfully deliver webhooks, your receiving endpoint's server must also support the TLS versions and cipher suites listed preceding. Gr4vy cannot deliver webhooks to servers that do not meet these security standards. ## Testing connectivity You can verify your system's ability to connect to Gr4vy using the following `curl` command to reach the sandbox environment: ```bash theme={"system"} curl -v https://api.sandbox.gr4vy.com ``` If the connection is successful, you see a TLS handshake completion message in the verbose output. # Environments Source: https://integration.wpay.com.au/guides/api/environments Sandbox and Production environments within an instance. Each instance has two environments. * `production` - The environment intended for use within your live app. * `sandbox` - The environment intended for use by developers to test transactions and within your test or quality assurance systems. These two environments have separate infrastructures so resources such as buyers and payment services are not shared between them. This also means that there are different hostnames for `sandbox` and `production` APIs and dashboards. The hostnames for the APIs are as follows: | Environment | Hostname | | - | - | | `sandbox` | `api.sandbox.{instance_id}.gr4vy.app` | | `production` | `api.{instance_id}.gr4vy.app` | > [!NOTE] > > The instance ID is the unique identifier for the deployment of the system and is included in every API call. > Together with the environment (sandbox or production) it is used to connect to the right APIs, as well as dashboard. > > The possible values for your ID are one of `wpay`, `wpay1`, `wpay2`, or `wpay3`. You are informed > in which instance your account has been set up. Test payment services, card rules, and other settings in the `sandbox` environment, before migrating them to the `production` environment. ## Why environments? Environments are an essential part of any development process. A developer might run the app on their development machine, and to try and test the integration they might want to try and create some payments. To stop the dashboard from being populated with lots of testing data it would be best for the developer to create their test transactions against the `sandbox` environment instead. Similarly, running either automated or manual tests against the API as part of QA and testing is common. To stop this from interfering with live environments, set these tests to run against the `sandbox` environment. ## Environments and payment service APIs The environment determines which payment service API environment should connect to. For example, Braintree transactions made in the `sandbox` environment are processed by the Braintree sandbox environment, while transactions made in the `production` environment are processed by the Braintree live environment. Other actions such as registering a digital wallet are also dependent on the environment. Apple Pay digital wallets registered using the `sandbox` environment are registered with Apple Pay's sandbox environment. This wallet is not recognized by Apple Pay's live environment and thus cannot be used in `production`. In short, the `production` environment always connects to the live API of a payment service, whereas the `sandbox` environment always connects to its sandbox API. ## Register Gr4vy IP addresses with a payment service Some payment services only accept requests from IP addresses that you register with them. Each instance has its own outbound IP addresses, and they differ between the `sandbox` and `production` environments. Contact [support](https://support.gr4vy.com) to get the IP addresses for your instance, then give the payment service the addresses for each environment you connect. # Transaction error codes Source: https://integration.wpay.com.au/guides/api/error-codes A list of all error codes for transactions. A transaction's `error_code` value can be null (for successful transactions) or one of the following for failed and declined transactions. ## Error codes and payment service responses The `error_code` is a normalized category. Each connector maps the codes it receives from the payment service to one of the values on this page, so several different reasons from a payment service can share one `error_code`. For example, a decline because the card exceeded a spending limit and a decline because the account has too little money can both appear as `insufficient_funds`. To see the exact reason, for example to decide when to retry a payment, read the fields that carry the payment service's own response. These fields are available on the [transaction](/reference/transactions/get-transaction) in the API and in the dashboard. | Field | Description | | :- | :- | | `raw_response_code` | The response code received from the payment service. It isn't standardized across payment services. | | `raw_response_description` | The response description received from the payment service. It isn't standardized across payment services. | | `auth_response_code` | The response code received from the processor. | | `iso_response_code` | The ISO 8583 response code received from the payment service. | | `merchant_advice_code` | The merchant advice code received from the payment service, which gives insight into the reason why the payment failed. | Not every payment service returns every field, so any of these fields can be null. ### Merchant advice codes Gr4vy passes the `merchant_advice_code` through as the payment service returns it. It isn't normalized or mapped to another value. A transaction that returns a merchant advice code isn't retried with another connection. When a stored card is declined with merchant advice code `01`, the [real-time account updater](/guides/features/account-updater/real-time) can check for updated card details. See the [account updater](/guides/features/account-updater/overview) for more information. ## Core failures The system can decline or fail a transaction itself rather than a connector or payment service rejecting the transaction. When this happens, one of the following error codes can be set as the `error_code` value. | `error_code` | Description | | :- | :- | | `incomplete_buyer_approval` | Occurs when the buyer doesn't complete an approval step, such as 3-D Secure or a redirect to a payment page, before the transaction's `approval_expires_at` time. This normally means the buyer closed the page or pop-up without authenticating. The approval window depends on the connection. When Gr4vy runs 3-D Secure itself, the payment is never sent to the payment service, so the payment service has no record of it. The dashboard shows this as **3DS incomplete** | | `failed_buyer_approval` | Occurs when a buyer fails to authenticate through 3DS. This transaction can be attempted again | | `missing_redirect_url` | Occurs when a transaction requires a redirect but was created without `payment_method.redirect_url`. Add the `payment_method.redirect_url` and retry | | `flow_decline` | Occurs when a transaction is declined by a Flow rule. This could be due to fraud or an alternate system configuration declining the transaction | | `all_attempts_skipped` | Occurs when a transaction fails because no outcome in the matched Flow rule can be used to process the transaction reliably, for example, network token provisioning failed and no PAN outcome follows. To avoid this, make sure to define there is a default suitable outcome for each possible transaction scenario | | `canceled` | Occurs when a transaction is canceled, this means a previously triggered cancel action has finished successfully | | `timeout` | Occurs when the payment service doesn't respond in time and Gr4vy can't safely retry the request. The payment service may still have processed the payment, so check the payment at the payment service before you charge the buyer again. See [timeouts](/guides/api/statuses/transactions#timeouts) | ## Connector declines When a payment service declines or fails the transaction the following `error_code` values can be set based on the response received from the payment service. The original values received from the payment service are available in the [payment service response fields](#error-codes-and-payment-service-responses) of a transaction record. > [!NOTE] > > The amount column in the tables below specifies the `amount` that can be used > to simulate the `error_code` when using the **Card simulator** connector. See > [simulator test values](/guides/features/simulators/overview#test-values) for > all the values you can use, including for 3-D Secure, captures, voids, and refunds. > [!WARNING] > > **Retriable** means that Gr4vy can make the next attempt in the matching > [Flow rule](/guides/dashboard/flow/card-transactions#retries-and-cascading), > either through the same connection under a different condition or through > another connection. It doesn't mean that > sending the same request again succeeds. > > The **Retriable?** column in the tables below only applies when the payment > service returns no ISO 8583 response code. When an ISO response code is > present, that code determines whether the transaction is retried, and the > [retriable ISO response codes](/guides/dashboard/flow/card-transactions#retriable-iso-response-codes) > list supersedes this column. A transaction is also never retried when the > payment service explicitly marks the attempt as non-retriable, or when the > response contains a `merchant_advice_code`. | `error_code` | Description | Simulator amount | Retriable? | | :- | :- | :- | :- | | `canceled_payment_method` | The payment method reported lost, stolen, or otherwise canceled, request another payment method | 200001 | | | `expired_authorization` | The authorization has expired, create a new authorization and use the new one | 200004 | | | `expired_payment_method` | The payment method has expired, use another payment method | 200005 | | | `incorrect_billing_address` | The billing address does not match the account. Prompt the customer to correct the billing address and retry | 200006 | | | `incorrect_country` | The country code was rejected by the service or issuer, request another payment method | 200007 | Yes | | `incorrect_currency` | The currency code was rejected by the service or issuer, request another payment method | 200008 | Yes | | `incorrect_cvv` | The CVV was incorrect. Prompt the customer to correct the CVV and retry | 200009 | | | `incorrect_expiry_date` | The expiry date is incorrect or the payment method has expired. Prompt the customer to correct the expiry date and retry or request another payment method | 200010 | | | `insufficient_funds` | The amount exceeds the available balance on the payment method. Prompt the customer to check their balance | 200011 | | | `issuer_decline` | The payment was declined by the issuer. Prompt the customer to check with their issuer | 200012 | Yes | | `other_decline` | The transaction failed for an unknown reason, may succeed if retried | 200013 | Yes | | `refused_transaction` | The transaction was refused due to legal reasons (for example watch list, embargo, sanctions), request another payment method | 200015 | | | `service_decline` | The payment was declined by service, request another payment method | 200016 | Yes | | `suspected_fraud` | The service flagged the transaction as suspected fraud. Prompt the customer to check with their issuer | 200017 | | ## Connector failures | `error_code` | Description | Simulator amount | Retriable? | | :- | :- | :- | :- | | `cancelled_buyer_approval` | The buyer canceled the payment at an approval or redirect step, for example, on a wallet or payment page. Not used when a bank or issuer stops a payment | 200022 | | | `disputed_transaction` | The transaction cannot be refunded as a chargeback has been initiated | 200002 | | | `duplicate_transaction` | The transaction is a duplicate of a previous transaction. Check to ensure the transaction was submitted only once | 200003 | | | `insufficient_service_permissions` | The service credentials lack permission to perform the requested action, check relevant configuration | 300001 | Yes | | `invalid_amount` | The amount not supported by service, check relevant configuration | 300002 | Yes | | `invalid_payment_method` | The payment method is not supported by the service (for example card scheme is not supported), request another payment method | 300003 | Yes | | `invalid_service_configuration` | The service is incorrectly configured, check relevant configuration | 300004 | | | `invalid_service_credentials` | The service credentials are not valid, check relevant configuration | 300005 | Yes | | `invalid_service_response` | The service response could not be parsed , check relevant configuration | 300006 | Yes | | `invalid_tax_identifier` | The tax identifier is invalid (for example GB VAT number is in an invalid format, or is of the wrong kind), correct identifier | 300007 | | | `missing_billing_address` | The billing address is required. Add billing address and retry | 300008 | Yes | | `missing_cvv` | The CVV is required. Add CVV and retry | 300009 | Yes | | `missing_shipping_address` | The shipping address is required. Add shipping address and retry | 300010 | Yes | | `missing_tax_identifier` | The tax identifier is required. Add tax identifier and retry | 300011 | Yes | | `refund_period_expired` | The refund can not be performed due to the refund period expiring, credit the customer in another way | 300012 | | | `requires_buyer_authentication` | The issuer requested authentication or additional credentials, for example, 3-D Secure or the security code (CVV). Add these and retry | 200014 | | | `service_error` | The service reported an internal server error or upstream processing error, check relevant configuration | 300013 | Yes | | `service_network_error` | The service was unreachable or experienced a timeout, wait before retrying | 300014 | Yes | | `service_rate_limit` | The service responded with a rate-limiting error, wait before retrying | 300015 | Yes | | `internal_error` | An internal error has occurred, check relevant configuration | 400001 | | | `invalid_billing_address` | The billing address is invalid. Correct billing address and retry | 400002 | | | `invalid_operation` | The service/method is not implemented, and operation is not supported for this request, check relevant configuration | 400003 | Yes | | `invalid_request_parameters` | The one or more request parameters are invalid, check payload and retry | 400004 | | | `invalid_service_request` | The service request could not be parsed, check payload and retry | 400005 | Yes | | `invalid_shipping_address` | The shipping address is invalid. Correct shipping address and retry | 400006 | | | `service_resource_conflict` | The service could not create a resource due to a conflict, check relevant configuration | 400007 | Yes | | `unavailable_payment_method` | The payment method is temporarily frozen or otherwise unavailable. Prompt the customer to check with their issuer or request another payment method | 200018 | | | `unexpected_state` | The service is configured in an unexpected state, check relevant configuration | 400008 | | | `unknown_error` | An unknown error occurred, check relevant configuration | 400009 | | | `unknown_payment_method` | The account is unknown, request another payment method | 200019 | Yes | | `unknown_service_resource` | The resource could not be found by the service, check relevant configuration | 400010 | Yes | | `unrecognised_country` | The country is not valid, correct the country and retry | 400013 | | | `unrecognised_currency` | The currency is not valid, correct the currency and retry | 400014 | | | `unrecognised_payment_method` | The payment method is not valid, request another payment method | 400015 | | | `unrecognised_scheme` | The payment scheme is not valid, request another payment method | 400016 | | | `unsupported_country` | The country is not supported by the service, correct country and retry | 400011 | Yes | | `unsupported_currency` | The currency is not supported by the service, correct currency and retry | 400012 | Yes | | `unsupported_payment_method` | The payment method is not supported by the service (for example card scheme is not supported), request another payment method | 200021 | | | `unsupported_scheme` | The payment scheme is not supported by the service, request another payment method | 400017 | | | `unsupported_transaction` | The payment method does not support this type of purchase (for example gambling is restricted), request another payment method | 200020 | | ## Connector capture / void / refund / payout tests | `error_code` | Description | Simulator amount | Retriable? | | :- | :- | :- | :- | | `service_network_error` | \[Capture] The service was unreachable or experienced a timeout, wait before retrying | 500001 | | | `refused_transaction` | \[Capture] The transaction was refused | 500002 | | | `service_network_error` | \[Void] The service was unreachable or experienced a timeout, wait before retrying | 510001 | | | `issuer_decline` | \[Void] The payment was declined by the issuer. Prompt the customer to check with their issuer | 510002 | | | `disputed_transaction` | \[Refund] The transaction cannot be refunded as a chargeback has been initiated | 520001 | | | `issuer_decline` | \[Refund] The payment was declined by the issuer. Prompt the customer to check with their issuer | 520002 | | | `service_error` | \[Refund] The service reported an internal server error or upstream processing error | 520003 | | | `refund_already_satisfied` | \[Refund] The service reported that the transaction has already been fully refunded | 520004 | | | `service_decline` | \[Payout] The service declined the transaction | 530001 | | | `service_error` | \[Payout] The service reported an internal server error or upstream processing error | 530002 | | # Error handling Source: https://integration.wpay.com.au/guides/api/errors Best practices on handling errors and other exceptions. In some situations, the API may return an error. In general, errors can be classified as either server-side or client-side errors. ## Client errors In case of a badly formatted API request, the API returns an error with an HTTP status code in the 400 to 499 range. Based on the type of client error made, the response is one of the following. * `400` **Bad request** - Returned when the request passed authentication and authorization checks, but the request did not have the data it expected. In most situations, this is because of missing request properties, or incorrect values for properties. * `401` **Unauthorized** - Returned when the bearer token was not valid and could not be used to authorize the API call. In this case please check the [List 4XX errors](/reference/logs/list-api-error-logs) API to inspect the cause of the issue. * `403` **Forbidden** - Returned when the bearer token is valid but the authenticating client does not have permission to access or manipulate the resource. This may be due to insufficient scopes in the JWT, a restricted API key used to sign the JWT, or a resource being locked. * `404` **Not found** - The resource could not be found. In a small number of cases, the resource may indeed exist, but due to permissions it is not accessible to the authenticated client and returning a `403` would provide information that should not be available to the caller. * `405` **Method not allowed** - The HTTP method used was not recognized for the path of the request. * `409` **Duplicate record** - A record already exists that matches one of the unique constrain for this resource. Check the API documentation for the call made to check what fields have a unique constraint. * `429` **Too many requests** - The request exceeded a [rate limit](/guides/api/rate-limiting). Wait for the number of seconds in the `Retry-After` header, if present, before you retry. ```json 400 theme={"system"} { "type": "error", "code": "bad_request", "status": 400, "message": "Request failed validation", "details": [ { "type": "type_error.uuid", "pointer": "/payment_method/buyer_id", "message": "value is not a valid uuid", "location": "body" } ] } ``` ```json 401 theme={"system"} { "type": "error", "code": "unauthorized", "status": 401, "message": "No valid API authentication found", "details": [] } ``` ```json 403 theme={"system"} { "type": "error", "code": "forbidden", "status": 403, "message": "Invalid credentials", "details": [] } ``` ```json 404 theme={"system"} { "type": "error", "code": "not_found", "status": 404, "message": "The resource could not be found", "details": [] } ``` ```json 405 theme={"system"} { "type": "error", "code": "method_not_allowed", "status": 405, "message": "Method Not Allowed", "details": [] } ``` ```json 409 theme={"system"} { "type": "error", "code": "duplicate_record", "status": 409, "message": "Duplicate record", "details": [] } ``` ```json 429 theme={"system"} { "type": "error", "code": "too_many_requests", "status": 429, "message": "Too many requests", "details": [] } ``` The [API reference](/reference/errors/client-errors) has more detail on the format of client errors. The [List 4XX errors](/reference/logs/list-api-error-logs) API can be used to see the latest client errors and the associated requests. ### No active payment service A transaction request can fail with a `400` response and the message `There is no active payment service for this transaction.` This means no active connection can process the transaction as requested. Common causes are the following. * No connection supports the payment method for the transaction's currency and country. * The connection doesn't support the transaction's `intent`. For example, a payment method that only supports direct capture needs `intent` set to `capture`. * The matched Flow rule leaves no connection to route the transaction to. * The request uses a feature that the remaining connections don't support. For example, a transaction with an `installment_count` only routes to connections that support installments, and a transaction with `allow_partial_authorization` set to `true` only routes to connections that support partial authorization. Check the connections that are active for the payment method, and the [Flow](/guides/dashboard/flow/overview) rules that apply to the transaction. ### Empty strings Optional string fields, such as `first_name` and `last_name` in billing and shipping details, must contain at least one character when you send them. An empty string (`""`) fails validation with a `400` response. If you don't have a value, omit the field or send `null`. ## Server errors In case of a server-side error in the system, the API returns an error with an HTTP status code in the 500 to 599 range. None of these errors can be resolved client side. Please contact support to raise any `5XX` errors. A `504` **Gateway timeout** means the request took too long to process. The operation may still complete, so treat the outcome as unknown and follow the guidance on [handling timeouts](/guides/timeouts) before you retry. ```json 500 theme={"system"} { "type": "error", "code": "server_error", "status": 500, "message": "Request could not be processed", "details": [] } ``` ```json 502 theme={"system"} { "type": "error", "code": "bad_gateway", "status": 502, "message": "Request could not be processed", "details": [] } ``` ```json 504 theme={"system"} { "type": "error", "code": "gateway_timeout", "status": 504, "message": "Request could not be processed", "details": [] } ``` The [API reference](/reference/errors/server-errors) has more detail on the format of server errors. # Idempotent requests Source: https://integration.wpay.com.au/guides/api/idempotent-requests Best practices on handling retries in an idempotent way. The API supports idempotent requests allowing the safe retrying of requests without accidentally performing the same operation again. When making an idempotent request, if an error occurs in the request to the API (such as a timeout or loss of connection), the request can be safely retried without the risk of creating a second resource or performing the update twice. Idempotency works by storing the status code and body of the first response for a given idempotency key, regardless of whether the request succeeded or failed. Subsequent requests with the same idempotency key return the same response. An idempotency key is a unique key generated that the API uses to identify subsequent retries of the same request. It is recommended to use V4 UUIDs, or another random string with enough entropy to avoid collisions. Idempotency keys can be up to 255 characters long, and remain valid for 24 hours, after which the idempotency key may be reused for another request. The response of an idempotent request is only saved if the API started executing. If the request fails validation or the request conflicts with another that was performed concurrently, no response is saved. It is safe to retry these requests. ## Supported endpoints The following endpoints support idempotent requests. * `POST /transactions` * `POST /transactions/:id/capture` * `POST /transactions/:id/authorization/increment` * `POST /transactions/:id/void` * `POST /transactions/:id/refunds` * `POST /transactions/:id/refunds/all` The `POST /gift-cards/activations` and `POST /gift-cards/issuances` endpoints also accept an `Idempotency-Key` header. For these endpoints, the key is forwarded to the gift card service, which makes the request idempotent if the service supports it. Gr4vy doesn't store the response. ## Making an idempotent request To make an idempotent request, specify the `Idempotency-Key` header in the request. ```bash theme={"system"} curl -i -X POST "https://api.example.gr4vy.app/transactions" \ -H "Authorization: Bearer [JWT]" \ -H "Idempotency-Key: bffa9ce6-7a8a-449c-889a-65bd2ee86903" \ -d "{...}" ``` > [!NOTE] > > Most of [the SDKs](./authentication) support passing through this header on > the API call. ## Concurrent requests When making an idempotent request using the same `Idempotency-Key` as a previous request, and the original request is still being processed, an error is received. This request can be safely retried. It is recommended to apply an exponential back-off when retrying transactions. ```json theme={"system"} { "type": "error", "code": "concurrent_request", "status": 409, "message": "A request with this idempotency key is still being processed. Retry later.", "details": [] } ``` ## Retrying after a timeout A request that takes too long can end with a `504` response, even though the operation continues and can still complete. Don't retry such a request straight away. Wait for the [webhook](/guides/features/webhooks/overview), or look up the result, for example by searching transactions by `external_identifier`, before you retry with the same `Idempotency-Key` and an exponential back-off. See [handling timeouts](/guides/timeouts) for more strategies. ## Retrying with a new key Always retry with the same `Idempotency-Key` as the original request. The API treats a request with a new key as a separate operation, even when the body is identical. If the original request has already completed, the new request is checked against the current state of the transaction. For example, a capture with a new key on a transaction that's already captured returns a `400` `bad_request` error with a detail of type `not_valid_status`, even though the first capture succeeded. When [multiple captures](/guides/features/multi-captures/overview) are enabled and no capture was marked `final`, a capture with a new key can instead take a second capture. ## Conflicting requests When making an idempotent request using the same `Idempotency-Key` as a previous request, and the request is not the same (for example, the request body is different), an error is received. Retrying this request will not change the error response. The request should be checked. ```json theme={"system"} { "type": "error", "code": "bad_request", "status": 400, "message": "Idempotency key already in use.", "details": [] } ``` # IP address forwarding Source: https://integration.wpay.com.au/guides/api/ip-address-forwarding Best practices on handling the original IP address for a transaction. Some payment service providers require the IP address of the buyer to be sent to them for anti fraud purposes. ## Embed By default, when using [Embed](/guides/payments/embed/options) the IP address of the buyer is passed to the payment service automatically where needed. ## Server-to-server When creating transactions on behalf of a buyer via a server without the use of Embed, pass the IP address of the buyer via the `X-Forwarded-For` header. ```sh theme={"system"} curl --header "X-Forwarded-For: 192.168.0.2" https://api.example.gr4vy.app/... ``` > [!NOTE] > > Most of [the SDKs](./authentication) support passing through this header on the `POST transactions` call. This header is automatically accepted and passed to the payment service where needed. If the header is not provided and the service provider recommends passing the IP address, the IP address of the request's origin is forwarded instead. > [!WARNING] > > Not all endpoints pass the IP address to a connector. It is recommended > to send the IP address whenever it is available. # Authentication with JWTs Source: https://integration.wpay.com.au/guides/api/jwts The API uses **Bearer (Token) Authentication** to authenticate any request. These tokens are **JSON Web Tokens (JWT)** which need to be created server side by your app. By far the easiest way to create a JWT is with one of the [server-side SDKs](./authentication) but it is also possible to generate the JWT without the SDKs using any number of open source JWT libraries. This document defines the API specification for authentication, JWT claims, and signature mechanisms. ## Authenticate an API call Every API endpoint that requires authentication expects an `authorization` HTTP-header with a signed JWT token as its value (prefixed with `bearer`). ```bash theme={"system"} curl -i -X GET "https://api.example.gr4vy.app/transactions" \ -H "authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" ``` ## Create an API key To sign a JWT, create a new API key-pair in the dashboard. To create a new API key visit the **Integrations** panel in the dashboard and click the **Add API key** button. The API key can be stored with the code or in a secure environment accessible to the app. ![API key dashboard](https://mintcdn.com/wpay/c50rx3S48hHhNNxb/assets/images/api-keys/add-key.png?fit=max\&auto=format\&n=c50rx3S48hHhNNxb\&q=85\&s=ffbfb085954c55e845050e44a7c9e73e) ### Merchants access If the instance has **Multi Merchant** enabled, select whether the API key can access **All merchants** or only one. It is recommended to restrict an API key to only one merchant account when integrating into Embed or the SDKs. ### Permissions When creating an API key, select the roles that match how the key is used. A role is a named, fixed set of [scopes](#scopes), and it determines which APIs the key can access within the merchants it can reach. A key can hold several roles, in which case it has the combined scopes of each. | Role | Access | | :- | :- | | **Full access** | Process payments, manage connections, edit flow rules, and manage other resources. | | **Processing only** | Process payments, including the ability to manage payment methods and buyers. This role also grants read access to reports. | | **System management only** | Manage connections and edit flow rules. This role cannot process payments or modify existing payments such as captures and refunds. | | **Transaction reporting only** | Read payments data, including the ability to create and access reports. This role is read-only and cannot access system data such as users, flow rules, and connections. | | **Key management only** | Manage API keys, including creating, editing, and activating or deactivating keys. This role grants no other access, so it has to be combined with another role. | You can change the roles of an existing API key from the **Integrations** panel without deleting and recreating the key. Reassigning roles is the supported way to change the scopes applied to a key. > [!NOTE] > > API access can be further reduced by setting a restrictive set of > [scopes](#scopes) on the JWT. ### Algorithm The API supports 2 algorithms for signing JWTs, namely ECDSA and RSA. It is recommended to use ECDSA unless the environment does not support this stronger and newer algorithm. The RSA algorithm is available for those environments that do not support ECDSA. RSA keys provide considerably less security for the same key size and are currently not supported by the SDKs. ## Generate JWT When not using one of the SDKs, or an SDK in the language is not available, construct and sign the JWT with one of the many libraries available on [`jwt.io`](https://jwt.io). At the high level a JWT is build up out of 3 pieces: * A **header** defining the algorithm and key used to create the JWT. * A set of **claims** that define the token's scope and other permissions. * A cryptographic **signature** based on the header and the claims, signed using the private key. Combine, these 3 pieces make up the JSON Web Token (JWT). See [`jwt.io`](https://jwt.io) for more details on the specification and available libraries for generating JWTs. ### JWT header The JWT header defines the type of encryption algorithm as well as the private key used to generate the signature. ```json theme={"system"} { "typ": "JWT", "alg": "ES512", // OR RS512 depending on your key "kid": "d757c76acbd74b56" } ``` > [!NOTE] > > The `typ` and `alg` are fixed and do not allow for other values. The `kid` is > the ID of the private key, which can be found in the **Integrations** panel of > the dashboard. ### JWT claims The claims define when the token was created and what access it has. ```json theme={"system"} { "iss": "My JWT Generation Tool", "nbf": 1607976645, "exp": 1607977245, "jti": "0fe1fb1b-2f7e-4c8d-b0eb-aae5d0ec98f7", "scopes": ["transactions.read"], "embed": { "amount": "200", "currency": "AUD", "buyer_id": "d757c76a-cbd7-4b56-95a3-40125b51b29c", "metadata": { "key": "value" }, "cart_items": [ { "name": "Joust Duffle Bag", "quantity": "1", "unit_amount": "9000", "tax_amount": "0" } ] } } ``` ### Claims The API supports the following JWT claims. | | Description | Required | | - | :- | :- | | `iss` | A unique ID that represents your code making this call. This helps identify what library made a call to the API. | Yes | | `nbf` | The UNIX timestamp (in seconds) that this token was created at. | Yes | | `exp` | The UNIX timestamp (in seconds) that this token expires at. | Yes | | `iat` | An optional UNIX timestamp (in seconds) for your internal use to indicate when the token was issued. | No | | `jti` | A random unique ID used for cryptographic entropy. This needs to be unique for each JWT. | Yes | | `scopes` | A list of scopes that give this token access to the API. | Yes | | `embed` | A dictionary of key-value pairs used to pin the amount, currency, and buyer info for use in Embed. | No | | `checkout_session_id` | The ID of a checkout session. This can be used to tie multiple transactions together as having originated from the same session. Values stored on the session, such as `cart_items` and `metadata`, replace the values in the transaction request. See [how session values reach the transaction](/guides/payments/secure-fields/quick-start/sdks#how-session-values-reach-the-transaction). | No | > [!NOTE] > > **Checkout sessions** > > When using a server-side SDK, the recommended way to populate the `checkout_session_id` claim is the `getEmbedTokenWithCheckoutSession` helper, which creates a checkout session and returns an Embed token with the ID already pinned. See [generating a token](/guides/payments/embed/quick-start/sdks). > [!NOTE] > > **Timestamps** > > Please be aware that the `nbf`, `exp`, and `iat` values are UNIX timestamps defined as seconds since January 1, 1970 (UTC). Some programming languages return UNIX timestamps as milliseconds, > requiring the removal of the last three digits. ### Scopes The API supports the following values for the `scopes` claims. | Scope | Description | | :- | :- | | `*.read` | Allows read-access to any resource. This is used by default in the SDKs | | `*.write` | Allows write-access to any resource. This is used by default in the SDKs. This does not also allow read access. | | `{resource_name}.read` | Allows read-access to a type or resource. For example, `payment-services.read` enabled read-access for buyers data. | | `{resource_name}.write` | Allows write-access to a type or resource. For example, `payment-services.write` enabled write-access for buyers data. This does not also allow read access. | | `embed` | A scope that represents all the access needed by Embed. | The following resource names are recognized. Please see the reference documentation for more details as to what scope is required per endpoint. * `anti-fraud-services` * `api-logs` * `buyers` * `buyers.billing-details` * `card-scheme-definitions` * `checkout-sessions` * `connections` * `digital-wallets` * `flows` * `payment-methods` * `payment-method-definitions` * `payment-options` * `payment-service-definitions` * `payment-services` * `reports` * `transactions` ### Signature & assembly Finally, the JWT signature is generated by appending the Base64 encoded header and claims (separated with a `.`) and run it through the key's algorithm. ```bash ECDSA theme={"system"} ECDSASHA512( base64UrlEncode(header) + "." + base64UrlEncode(payload), public_key, private_key ) ``` ```bash RSA theme={"system"} RSASHA512( base64UrlEncode(header) + "." + base64UrlEncode(payload), public_key, private_key ) ``` The assembled JWT is then formed by appending the Base64 encoded header, claims, and signature separated by a full stop. ``` base64UrlEncode(header) + "." + base64UrlEncode(payload) + "." + base64UrlEncode(signature) ``` # OpenAPI 3.1.0 Specification Source: https://integration.wpay.com.au/guides/api/openapi An [OpenAPI specification](https://github.com/gr4vy/gr4vy-openapi) is provided for a large part of the API, including all endpoints required for payment processing. This specification is automatically generated when new features are released, and is used to drive automatic changes to the [server-side SDKs](/guides/get-started#server-side-sdks), [CLI](/guides/tools/cli), and [Postman collection](/guides/tools/postman). > The OpenAPI 3.1.0 specification > > [Open](https://github.com/gr4vy/gr4vy-openapi) > Latest changes > > [Open](https://github.com/gr4vy/gr4vy-openapi/commits/main/openapi.json) # Pagination Source: https://integration.wpay.com.au/guides/api/pagination Best practices on handling pagination in the API. Many of the API endpoints support cursor-based pagination. When a page is requested, the response includes cursors for the next and previous page of results. ```json Response theme={"system"} { "items": [ { "id": "42074b2a-d2fa-417a-b7cf-5776b069689d", "type": "buyer", "display_name": "Gerry Vine", "external_identifier": null, "created_at": "2021-03-12T15:05:27.199271+00:00", "updated_at": "2021-03-12T15:05:27.199271+00:00" }, { "id": "9e2e7380-5177-4b8e-8677-3854bf94d6c3", "type": "buyer", "display_name": "Retha Palm", "external_identifier": null, "created_at": "2021-03-12T10:47:51.989637+00:00", "updated_at": "2021-03-12T10:47:51.989637+00:00" }, ... ], "limit": 20, "next_cursor": "eQA2M2E2ZmQwZS1lYzhkLTQ3ZWItYjI0Yy0zMDlhYzg0OTViNTMAMjAyMS0wMi0yN1QyMjoyNDo1MS4wMTIxNTA", "previous_cursor": null } ``` > [!NOTE] > > All of [our SDKs](./authentication) have built-in pagination support. Please see the individual GitHub documentation for your preferred SDK for more details. ## Using cursors If present, the `next_cursor` and `previous_cursor` values can be used to request the next and previous page of results by passing the `cursor` query parameter with the value of either cursor. ```bash theme={"system"} curl -i -X GET "https://api.example.gr4vy.app/buyers?cursor=eQA2M2E2ZmQwZS1lYzhkLTQ3ZWItYjI0Yy0zMDlhYzg0OTViNTMAMjAyMS0wMi0yN1QyMjoyNDo1MS4wMTIxNTA" \ -H "Authorization: Bearer [JWT_TOKEN]" ``` > [!TIP] > > When you filter [list transactions](/reference/transactions/list-transactions) on broad values such as > `amount_eq`, `currency`, `country`, or `method`, combine the filter with a `created_at_gte` and > `created_at_lte` range for the best performance. ## Page limits Any of the APIs that support cursor pagination also support the `limit` query parameter to control the number of items returned. Check the reference documentation for each endpoint for the range of limit values allowed. ```bash theme={"system"} curl -i -X GET "https://api.example.gr4vy.app/buyers?limit=1" \ -H "Authorization: Bearer [JWT_TOKEN]" ``` ```json theme={"system"} { "items": [ { "id": "42074b2a-d2fa-417a-b7cf-5776b069689d", "type": "buyer", "display_name": "Gerry Vine", "external_identifier": null, "created_at": "2021-03-12T15:05:27.199271+00:00", "updated_at": "2021-03-12T15:05:27.199271+00:00" } ], "limit": 1, "next_cursor": "cAA5ZTJlNzM4MC01MTc3LTRiOGUtODY3Ny0zODU0YmY5NGQ2YzMAMjAyMS0wMy0xMlQxMDo0Nzo1MS45ODk2Mzc", "previous_cursor": null } ``` # Prefer header Source: https://integration.wpay.com.au/guides/api/prefer-header Some API endpoints support custom `Prefer` headers to indicate preferences for specific server behaviors during request processing. This allows the API to evolve, adding new features, while maintaining backwards compatibility where possible. The `Prefer` header is optional for all API requests. Endpoints that don't support specific `Prefer` values ignore them without causing errors. ## `Prefer: resource=` Currently, only the `Prefer: resource` header is supported by the API. This header allows a merchant to opt into a different resource to return. Both the [void](/reference/transactions/void-transaction) and [capture](/reference/transactions/capture-transaction) endpoints support this header to opt into returning a specific resource type. **Void** For example, by setting the `Prefer: resource=transaction-void` on a void call, the response type is a `transaction-void` resource, rather than the standard `transaction` returned by this API. ```sh Request theme={"system"} POST /transactions/1a032928-abc0-4fb8-971e-fb917291e75f/void Prefer: resource=transaction-void ``` ```json Response theme={"system"} { "type": "transaction-void", "status": "failed", "code": "service_error", "raw_response_code": "1001", "raw_response_description": "Internal error, please retry", "transaction": { "type": "transaction", "id": "1a032928-abc0-4fb8-971e-fb917291e75f", ... } } ``` **Capture** For example, by setting the `Prefer: resource=transaction-capture` on a capture call, the response type is a `transaction-capture` resource, rather than the standard `transaction` returned by this API. ```sh Request theme={"system"} POST /transactions/1a032928-abc0-4fb8-971e-fb917291e75f/capture Prefer: resource=transaction-capture ``` ```json Response theme={"system"} { "type": "transaction-capture", "status": "failed", "code": "service_error", "raw_response_code": "1001", "raw_response_description": "Internal error, please retry", "transaction": { "type": "transaction", "id": "1a032928-abc0-4fb8-971e-fb917291e75f", ... } } ``` > [!NOTE] > > Please note that HTTP header names are case-insensitive, as per HTTP standards, so `prefer`, `PREFER`, or `Prefer` can all be used. The value > of a header is case-sensitive and needs to be lower-case to match both the `resource` key and the `` value. # Rate limiting Source: https://integration.wpay.com.au/guides/api/rate-limiting Rate limits and enumeration prevention The system does not apply any rate limits for server-to-server API calls. Instead, the system aims to grow with the business to handle the throughput needs required. That said, certain rate limits are applied on browser/app-to-server API calls in order to prevent enumeration attacks, preventing a malicious party from enumerating credit, debit, or scheme cards to check their validity. ## Enumeration prevention To prevent enumeration attacks, the following limits are applied. | Token & Endpoints | Limit | | - | - | | A [JWT token](./authentication) with the `embed` scope used with the following endpoints. [`POST /gift-cards/balances`](/reference/gift-cards/list-gift-card-balances) [`POST /gift-cards`](/reference/gift-cards/new-gift-card) [`POST /gift-cards/activations`](/reference/gift-cards/activate-gift-card) [`POST /transactions`](/reference/transactions/new-transaction) | This limits tokens for use with Embed or other frontend integrations from iterating over scheme and gift card numbers. Server-to-server calls are not affected. | | A [Checkout Session ID](/reference/checkout-sessions/new-checkout-session) used with the following endpoint. [`PUT /checkout/sessions/:id/fields`](/reference/checkout-sessions/update-checkout-session-fields) | This endpoint is used by Secure Fields for storing scheme card details and prevents the over-use of the session ID to store and validate card details. | > [!WARNING] > > **Rate limit** > > The current rate limit for these endpoints is set to approximately 50 requests per minute across all endpoints, per token. This value may be adjusted downward in time to adjust for enumeration attacks. > > Generating a new checkout session ID or JWT token for every checkout is recommended to prevent a user from being rate-limited. When a token or checkout session exceeds the limit, the API returns a `429` response with a `Retry-After` header that holds the number of seconds to wait before you retry. ```json theme={"system"} { "type": "error", "code": "too_many_requests", "status": 429, "message": "Too many requests", "details": [] } ``` ## Traffic peaks There's no fixed request quota for server-to-server API calls. Capacity scales automatically with your traffic. During a very sudden spike, some requests can be slower or return a `429` response while capacity scales up. * Retry `429` responses with an exponential back-off, and send an [`Idempotency-Key`](/guides/api/idempotent-requests) on requests that support it so a retry can't create a duplicate. * Tell [support](https://support.gr4vy.com) about planned peaks, such as a sale or a seasonal event, ahead of time. Include the dates and the volume you expect so capacity can be raised in advance. # Payment method statuses Source: https://integration.wpay.com.au/guides/api/statuses/payment-methods Learn what each payment method status means, which stored payment methods you can charge, how a stored payment method can fail, and how to tell whether a transaction stored its payment method. A payment method's `status` value can be one of the following depending on the state within the system and the status within the used payment service. | `status` | Description | | :- | :- | | `processing` | The payment method is stored but isn't ready to use yet. Gr4vy is waiting for the payment service to confirm the setup, for example through a notification after the buyer completes a redirect. | | `buyer_approval_required` | Storing the payment method needs approval from the buyer. Send the buyer to the `approval_url` to continue. This mainly applies to alternative payment methods like PayPal, where the buyer is redirected to approve. | | `succeeded` | The payment method is stored and can be used to create transactions. | | `paused` | The payment service paused the agreement or mandate behind the stored payment method. The payment method can't be used until the payment service resumes it. | | `failed` | The payment method couldn't be stored, or the payment service reported that it's no longer valid. | ## Charge a stored payment method Only a payment method with the status `succeeded` can be used to create a transaction with the `id` method. For any other status, the [new transaction](/reference/transactions/new-transaction) request fails with an HTTP `400` error. The error detail points to `/payment_method/id` and includes the current status, for example `Payment method has not been successfully stored. Current status: failed.` Before you create a merchant-initiated transaction with a stored payment method, [get the payment method](/reference/payment-methods/get-payment-method) and check that its `status` is `succeeded`. If it isn't, ask the buyer to provide a payment method again. ## How a stored payment method fails A payment method moves to `failed` in the following cases. * The payment service doesn't confirm the setup in time. Each payment service has a time limit for a payment method that is `processing` or `buyer_approval_required`. When the limit passes, the payment method moves to `failed`. * The payment service reports that the setup failed. * The payment service reports that a payment method that already `succeeded` is no longer valid, for example because the provider canceled the agreement behind it. This applies to payment methods other than cards and digital wallets. A stored card or digital wallet keeps the status `succeeded` once it reaches it. If you store a card as part of a transaction and that transaction doesn't succeed, the card isn't kept for later use. > [!WARNING] > > Gr4vy doesn't send a webhook when a stored payment method moves to `failed`. > Check the payment method's `status` before you charge it. When the payment service pauses or resumes a stored payment method, Gr4vy sends the `payment-method.paused` and `payment-method.resumed` [webhooks](/guides/features/webhooks/events). ## Check whether a transaction stored its payment method To find out if a transaction stored its payment method, [get the transaction](/reference/transactions/get-transaction) and look at `payment_method.id`. * If `payment_method.id` is `null`, the payment method was never stored. Either the transaction wasn't created with `store: true`, or it was and the transaction didn't succeed. * If `payment_method.id` has a value, the transaction used or stored that payment method. The ID stays on the transaction after you [delete the payment method](/reference/payment-methods/delete-payment-method). To check whether a stored payment method still exists, [get the payment method](/reference/payment-methods/get-payment-method) with that ID. An HTTP `404` response means it's deleted. Don't use `payment_source`, `merchant_initiated`, or `is_subsequent_payment` for this check. These fields describe the payment, not whether the payment method was stored. In the dashboard, the **Usage** row in the **Payment method** panel of a transaction shows the same information. It reads **Transient** when the payment method was never stored, **Stored** when it's stored, and **Stored & Deleted** when it was stored and has since been deleted. # State diagram The following state diagram serves as an overview of all the different `status` values and how they relate to each other. A card or bank account stored directly through the API starts as `succeeded`. ```mermaid theme={"system"} flowchart LR start(( )) --> processing start --> succeeded processing --> buyer_approval_required processing --> failed buyer_approval_required --> failed buyer_approval_required --> succeeded processing --> succeeded succeeded --> paused paused --> succeeded succeeded -->|Not cards or digital wallets| failed ``` # Transaction statuses Source: https://integration.wpay.com.au/guides/api/statuses/transactions A list of all statuses for transactions. A transaction's `status` value can be one of the following depending on the state within the system and the status within the used payment service. | `status` | Description | | :- | :- | | `processing` | The transaction record has been created in the system and is now being processed with payment services. | | `buyer_approval_pending` | The transaction was created but needs approval from the buyer either by redirecting them to a hosted page or their bank for 3DS. | | `authorization_succeeded` | The transaction has been successfully authorized but not yet captured. | | `authorization_failed` | The transaction could not be authorized with any payment service due to technical issues or limitations of the payment service. | | `authorization_declined` | The transaction was declined by the payment service. | | `capture_pending` | The transaction has successfully been submitted for capture with a payment service and is now pending with them. | | `capture_succeeded` | The transaction has been successfully authorized and captured. | | `authorization_void_pending` | The transaction authorized transaction is in the process of being voided. | | `authorization_voided` | The transaction was successfully authorized but has since been voided before it was captured. | ## State diagram The following state diagram serves as an overview of all the different `status` values and how they relate to each other. ### Separate authorization and capture Transactions where the original `intent` is `authorize`. ```mermaid theme={"system"} flowchart LR start(( )) --> processing processing --> buyer_approval_pending processing --> authorization_succeeded processing --> authorization_failed buyer_approval_pending --> authorization_declined processing --> authorization_declined buyer_approval_pending --> authorization_failed buyer_approval_pending --> processing authorization_succeeded --> capture_pending authorization_succeeded --> authorization_void_pending capture_pending --> capture_succeeded capture_pending --> authorization_succeeded capture_pending --> authorization_declined authorization_void_pending --> authorization_voided authorization_void_pending --> authorization_succeeded ``` ### Direct capture Transactions where the original `intent` is `capture`. ```mermaid theme={"system"} flowchart LR start(( )) --> processing processing --> capture_succeeded processing --> authorization_failed buyer_approval_pending --> authorization_declined processing --> buyer_approval_pending buyer_approval_pending --> processing processing --> authorization_declined ``` > [!NOTE] > > In some cases, a transaction with `intent` set to `capture` is internally split > into a separate authorization and capture. When this happens, the transaction may > pass through `authorization_succeeded` and `capture_pending` before reaching a > terminal state such as `capture_succeeded`, or before being voided or declined. > See [split authorization and capture](/guides/features/webhooks/technical-considerations#split-authorization-and-capture) > for more details. ## Cancel, void, or refund Which action reverses a transaction depends on its `status`. | `status` | Action | | :- | :- | | `processing` or `buyer_approval_pending` | [Cancel](/reference/transactions/cancel-transaction) the transaction. | | `authorization_succeeded` | [Void](/reference/transactions/void-transaction) the authorization. | | `capture_succeeded` | [Refund](/reference/transactions/refund-transaction) the transaction. | Only some connections support cancel. When the connection doesn't support it, or the transaction has a different status, the cancel isn't processed. Once a transaction reaches `capture_succeeded`, its status doesn't change again, and refunds don't change it either. A refund is always made against a single transaction and returns the funds to the payment method used for that transaction. You can't create a refund without a transaction, or combine several transactions in one refund. To send funds to a card without a previous transaction, use [payouts](/guides/features/payouts/overview). ## Refresh a transaction's status To fetch the latest status of a transaction from the payment service, [sync the transaction](/reference/transactions/sync-transaction). In the dashboard, open the transaction, select **Actions**, and then select **Refresh**. A refresh can change details such as the status, amount, captured amount, and the payment service's transaction reference. ## Timeouts When Gr4vy forwards a payment request to a connector, network conditions can occasionally cause the request to time out before a response is received. In these cases, the outcome of the request is unknown — the connector may or may not have processed it. Rather than failing immediately or requiring you to re-submit, Gr4vy automatically retries these requests in the background when the connection supports idempotent requests. This guarantees the connector can return the result of the original request without creating a duplicate charge. Not every connection supports idempotent requests. When it doesn't, Gr4vy can't safely retry, and the request fails instead. See [Timeouts without a retry](#timeouts-without-a-retry). ### Transaction timeouts When a `POST /transactions` request to a connector times out, Gr4vy queues a background task to retry that transaction. The transaction moves to `processing` status while Gr4vy resolves the outcome. No action is required on your side — in particular, **do not re-submit the transaction** while it is in `processing`. Retries stop after a maximum of **24 hours** from the original request, at which point the transaction is resolved to a final status. #### Possible outcomes | Outcome | Transaction status | Webhook event | | - | - | - | | Authorization succeeded | `authorization_succeeded` | `transaction.authorized` | | Authorization failed | `authorization_failed` | `transaction.failed` | | Unresolved after 24 hours | `authorization_failed` | `transaction.failed` | In all cases a webhook event is sent when the outcome is known. Gr4vy recommends relying on webhooks rather than polling the transaction status. ### Capture timeouts When a capture request to a connector times out, and the connection supports idempotent requests, Gr4vy queues a background task to retry the capture. The capture moves to `pending` status while Gr4vy resolves the outcome. Retries stop after a maximum of **24 hours** from the original request, at which point the transaction is resolved to a final status. For a capture you request, Gr4vy only retries a capture that times out. A capture that's declined or fails for another reason isn't retried. To try again, call the [capture endpoint](/reference/transactions/capture-transaction) again. When Gr4vy splits a transaction with `intent` set to `capture` into a separate authorization and capture, and the capture fails without being declined, Gr4vy can make another capture attempt in the background. The transaction's events can then show a failed capture followed by a successful one. A declined capture isn't retried. #### Possible outcomes | Outcome | Capture status | Webhook event | | - | - | - | | Capture succeeded | `succeeded` | `capture.succeeded` | | Capture failed | `failed` | `capture.failed` | | Capture declined | `declined` | `capture.declined` | A webhook event is sent as soon as the outcome is known. ### Refund timeouts When a refund request to a connector times out, Gr4vy queues a background task to retry the refund. The refund moves to `pending` status while Gr4vy resolves the outcome. Retries stop after a maximum of **24 hours** from the original request, at which point the transaction is resolved to a final status. #### Possible outcomes | Outcome | Refund status | Webhook event | | - | - | - | | Refund succeeded | `succeeded` | `refund.succeeded` | | Refund failed | `failed` | `refund.failed` | | Refund declined | `declined` | `refund.declined` | A webhook event is sent as soon as the outcome is known. ### Timeouts without a retry When a connection doesn't support idempotent requests, a transaction whose request to the payment service times out ends as `authorization_failed` with `error_code` set to `timeout`. Gr4vy doesn't fail over to another connection in the Flow rule for a timed-out attempt. The payment service may still have processed the payment. Gr4vy doesn't apply results that arrive from the payment service after the transaction failed, and [syncing the transaction](/reference/transactions/sync-transaction) doesn't change its final status. Before you charge the buyer again, check the payment at the payment service, for example, in its dashboard. If the payment went through, reverse it there. ### Recommendations * **Listen for webhooks.** The webhook event is the authoritative signal that a timeout has been resolved. Prefer it over polling. * **Do not re-submit.** A resource with a `processing` or `pending` status is still being resolved. Re-submitting may risk duplicate charges. * **Allow up to 24 hours.** The resolution window for create timeouts is bounded at 24 hours. For captures the window depends on the connector. * **Contact support if nothing arrives.** If 24 hours have passed and you have not received a webhook, contact Gr4vy support with the transaction ID. # Best practices Source: https://integration.wpay.com.au/guides/best-practices/overview A list of best practices for integrating. In no particular order, here are some useful recommendations when integrating the API, Embed, or Secure Fields. ## Associate transactions with a buyer A Buyer allows transactions to be linked with customers as well as send buyer-specific information such as `name`, `email`, `address` and `tax ID`, etc. See [Buyers](/reference/buyers) for further details. ## Set a unique reference ID for each transaction A unique transaction reference ID helps link transactions to orders (see step 2). A unique reference ID can either be set as an external identifier or metadata on the transaction. This is available when using Embed, Secure Fields, or the API directly. Embed is sometimes displayed at checkout before an order is created by the system. After all, creating an order every time a customer loads a checkout page is not desired. An `onBeforeTransaction` callback in Embed has been introduced that can be used to set the `externalIdentifier` or metadata right before a transaction is created. This allows a unique reference ID to be created just-in-time before a transaction is created by Embed. ```js Update the external identifier theme={"system"} embed.setup({ onBeforeTransaction: async () => { // Merchant could make a call to a server here to get an order ID. // let { orderId } = await client.getOrder() return { externalIdentifier: orderId, } } }) ``` Metadata can be updated in a similar way. In the case that the metadata has been pinned, the JWT token also needs to be updated because the metadata may be pinned in the token. ```js Update the metadata theme={"system"} embed.setup({ onBeforeTransaction: async ({ metadata }) => { // Merchant could make a call to a server here to get an order ID. // let { orderId, newToken } = client.getOrder() return { token: newToken, metadata: { ...metadata, orderId: orderId } } } }) ``` ## Use webhooks to listen for transaction updates It is recommended to listen for updates to transactions by using [webhooks](/guides/features/webhooks/overview). As a unique reference ID has been set (see preceding section), this can be used to listen to webhook events, which notify of a transaction completing, failing, or being declined. Please reach out to the team to set up the webhook URL. > [!WARNING] > > It is highly recommended to use webhooks and not exclusively rely on any > in-browser events like Embed's `transactionCreated` event or `onComplete` > callback. Because browsers are an unreliable environment this message between > Embed and the system could get lost for many reasons, and so listening to > webhook updates is generally considered the only fully reliable option when > dealing with transaction updates. ## Use idempotent requests Sometimes transactions fail, either because of bad input or because of some kind of network or other system error. In the case of an error where the response was not received by the buyer's browser (or the system), a transaction can be retried using a special header. ```sh theme={"system"} curl -i -X POST "https://api.{gr4vyId}.gr4vy.app/transactions" \ -H "Authorization: Bearer {jwtToken}" \ -H "Idempotency-Key: bffa9ce6-7a8a-449c-889a-65bd2ee86903" \ -d "{...}" ``` The `Idempotency-Key` header is used to uniquely identify a transaction when retrying the same transaction after a network error or some kind of timeout. The second request is matched using this key against the original request, and rather than creating a new transaction the original response is returned. See [Idempotent Requests](/guides/api/idempotent-requests) for further details. > [!NOTE] > > If Embed is being used then idempotent requests are already in use. ## Transaction request information Some payment methods require specific 'non-standard' information to process. For example, some Buy Now Pay Later (BNPL) methods require cart items to successfully process payments. Therefore to future-proof the integration, integrating as much information in the transaction request as possible is recommended. This allows new additional methods of payment to be added without having to update the integration. Here are a few examples of optional request information to include. * `buyer` information * `cart_items` * `shipping` information * `connection_options` (where applicable) * `payment_source` * `is_subsequent_payment` * `merchant_initiated` # 3-D Secure in Embed Source: https://integration.wpay.com.au/guides/features/3ds/embed Any merchant using Embed automatically benefits from **Universal 3-D Secure** once it has been [set up](/guides/features/3ds/setup) and turned on for a payment service. ![3-D Secure prompt](https://mintcdn.com/wpay/c50rx3S48hHhNNxb/assets/images/3ds/3dsecure-prompt.png?fit=max\&auto=format\&n=c50rx3S48hHhNNxb\&q=85\&s=c62306fdddf4b556a4d929059b1be5bd) Example of a sandbox 3DS prompt Additionally, **Dynamic 3-D Secure** can be set up using Flow rules, allowing dynamic skipping or forcing of 3-D Secure based on anti-fraud decisions, cart items, or any other transaction data. ## Buyers and billing details To optimize 3-D Secure it is highly recommended to provide a buyer with billing details attached to Embed. A buyer can be set up via the [`POST /buyers`](/reference/buyers/new-buyer) API. The buyer can then be attached to Embed by using the `buyerId` or `buyerExternalIdentifier` property. The buyer's name, email address, and billing address are used to help reduce buyers being asked to complete a 3-D Secure challenge. ```js React theme={"system"} ``` ```js Node theme={"system"} gr4vy.setup({ gr4vyId: "example", amount: 1299, currency: "AUD", country: "AU", buyerExternalIdentifier: "user-1234", ... }); ``` > [!NOTE] > > The instance ID is the unique identifier for the deployment of the system and is included in every API call. > Together with the environment (sandbox or production) it is used to connect to the right APIs, as well as dashboard. > > The possible values for your ID are one of `wpay`, `wpay1`, `wpay2`, or `wpay3`. You are informed > in which instance your account has been set up. We will use these billing details when contacting payment services, 3-D Secure Server, anti-fraud services, and more. ## Features For every transaction, we will handle the following steps. 1. Detect if any Flow rule explicitly requires or skips 3-D Secure 2. Detect if the selected payment service for this transaction has 3-D Secure enabled 3. Detect if the card used in the transaction is enrolled for 3-D Secure 4. Handle the seamless frictionless 3-D Secure flow including the device finger printing 5. Handle the 3-D Secure challenge flow, where a buyer is directed in-page to their bank to approve the transaction ## Usage To use 3-D Secure in Embed, [set up](/guides/features/3ds/setup) 3-D Secure and turn it on for the enabled payment services. Once configured, Embed handles all the user interactions. # Pass-through External 3-D Secure data Source: https://integration.wpay.com.au/guides/features/3ds/external 3-D Secure can be handled by a third party server, allowing you to pass through 3DS authentication data from that service to a preferred payment service. To use 3-D Secure with a separate server, it is assumed that 3-D Secure has been handled separately and the relevant data received at the end of the flow has been collected. The transaction API supports directly passing 3-D Secure data [as part of the transaction request](/reference/transactions/new-transaction#body-three-d-secure-data). This data is passed along to the payment service assuming it supports this data via their API. ```json POST /transactions theme={"system"} { "amount": 1299, "currency": "AUD", "payment_method": { "method": "card", "number": "4111111111111111", "expiration_date": "11/25", "security_code": "123", "redirect_url": "https://example.com/callback" }, "three_d_secure_data": { "cavv": "3q2+78r+ur7erb7vyv66vv8=", "eci": "05", "version": "2.1.0", "directory_response": "C", "caav_algorithm": "1", "xid": "ODgxNDc2MDg2MDExODk5MAAAAAA=", "authentication_response": "Y" } } ``` The `authentication_response` field follows the EMV 3DS transaction status values. See [Authentication results](/guides/features/3ds/statuses) for the full reference. > [!WARNING] > > **Payment service compatibility** > > Not all payment services support external 3-D Secure data, and therefore even > when this data is provided a `redirect_url` is still expected as well. If none of > the selected payment services support the pass-through of 3-D Secure data the > API might [return a pending transaction](./hosted) with an `approval_url` in > the response. > > Some payment services also need to turn on the acceptance of 3-D Secure results from > another provider for your account before they accept this data. Check the > [connector page](/connections/catalog) for your payment service. # Hosted 3-D Secure Source: https://integration.wpay.com.au/guides/features/3ds/hosted **Hosted 3-D Secure** is available when using the API or [Secure Fields](/guides/payments/secure-fields/), as long as 3-D Secure has been [set up](./setup) and turned on for at least one payment service. This integration equally supports **Dynamic 3-D Secure** which can be set up using Flow rules, allowing dynamic skipping or forcing of 3-D Secure based on anti-fraud decisions, cart items, or any other transaction data. ## Usage To use 3-D Secure via the API, [set up](./setup) 3-D Secure, turn it on for the enabled payment services, and pass a `redirect_url` when creating the transaction. ```json POST /transactions theme={"system"} { "amount": 1299, "currency": "AUD", "buyer_external_identifier": "user-1234", "payment_method": { "method": "card", "number": "4111111111111111", "expiration_date": "11/25", "security_code": "123", "redirect_url": "https://example.com/callback", } } ``` With 3-D Secure turned on for a payment service, automatic detection occurs if it is required to get the buyer's approval for a transaction. If it is required, the API returns a transaction with the status `buyer_approval_pending` and an `approval_url` that the buyer can be sent to. ```json theme={"system"} { "type": "transaction", "id": "869df4fc-a3db-43db-806a-14464d9e6920", "status": "buyer_approval_pending", "payment_method": { "approval_url": "https://embed.example.gr4vy.app/...", ... }, ... } ``` Treat the `approval_url` as an opaque value. Redirect the buyer to it exactly as returned, without parsing, rebuilding, or shortening it. The URL carries data for the issuer's 3-D Secure steps, so its length varies by issuer and it can be long. Make sure anything that stores or forwards it doesn't truncate it. After the redirect, all the intricacies of 3-D Secure are handled. Once the buyer has approved (or rejected) the transaction, the browser is redirected back to the `redirect_url` that was set when the transaction was created. Gr4vy appends both a transaction ID and status to the `redirect_url`. ``` https://example.com/callback?gr4vy_transaction_id=123&gr4vy_transaction_status=capture_succeeded ``` > [!WARNING] > > Confirming the actual transaction status by fetching the > updated transaction by its `id` is strongly recommended. ## Buyers and billing details To optimize 3-D Secure it is highly recommended to provide a buyer with billing details attached when making an API call. A buyer can be set up via the API or provided as an [in-line guest buyer](/reference/transactions/new-transaction#body-buyer-one-of-0). ## Features For every transaction, the following steps are handled. 1. Detect if any Flow rule explicitly requires or skips 3-D Secure. For cards whose issuer mandates authentication, Gr4vy attempts 3-D Secure even when a rule skips it. See [Issuer-mandated 3-D Secure](/guides/features/3ds/overview#issuer-mandated-3-d-secure). 2. Detect if the selected payment service for this transaction has 3-D Secure enabled 3. Detect if the card used in the transaction is enrolled for 3-D Secure 4. Handle the seamless frictionless 3-D Secure flow including the device finger printing once the user is redirected to the `approval_url`. 5. Handle the 3-D Secure challenge flow, where a buyer is directed to their bank to approve the transaction. # 3-D Secure with multiple connections Source: https://integration.wpay.com.au/guides/features/3ds/multiple-connections Learn how 3-D Secure works when a transaction can be routed to more than one connection, and how to set up 3DS configurations for multiple acquirers. Many merchants route card transactions to more than one connection, for example to fail over when a connection is down or to use different acquirers per currency. This page explains how 3-D Secure (3DS) behaves in those setups, and how to set up your [3DS configurations](./setup) to match. ## How 3DS works across connections A [Flow routing rule](/guides/dashboard/flow/card-transactions#action-route-to-connection) can list more than one connection. Gr4vy tries each connection in order, and moves on to the next one after a [failure or retriable decline](/guides/dashboard/flow/card-transactions#failures-and-retriable-declines). 3DS runs at most once per transaction: 1. Gr4vy authenticates the card on the first connection in the list that has 3DS turned on. It uses the acquirer details of the scheme profile or merchant account configuration that [matches the transaction](./setup#scheme-profile-resolution). 2. The connection authorizes the transaction with the 3DS result. 3. If the transaction is retried on the next connection, Gr4vy sends the same 3DS result to that connection. The buyer isn't authenticated again. Some details affect which connections receive the 3DS result: * Connections that can't accept 3DS data from Gr4vy are skipped once a transaction has a 3DS result. * Some payment services only accept 3DS results from another provider, including Gr4vy, after they turn this on for your account. Check the [connector page](/connections/catalog) for your payment service. * If a retry uses a different card scheme, for example with [co-badged routing](/guides/dashboard/flow/card-transactions#co-badged-routing), the 3DS result isn't sent. The result is only valid for the scheme it was created for. * If 3DS fails, the transaction isn't retried on another connection. [Native 3DS](./native) and [Click to Pay](/guides/features/click-to-pay/tas) work the same way. They authenticate before a connection is chosen, and the same rules decide which connections receive the result. ### Connections that run their own 3DS Some payment services run 3DS themselves and redirect the buyer to their own page. Once a payment service redirects the buyer, Gr4vy can't retry the transaction on another connection. Connector pages list this as **3-D Secure (provider-hosted)**. Some of these connectors record the payment service's 3DS result in the transaction's `three_d_secure` field, in the same format as Gr4vy's own 3DS results. Currently this applies to [Adyen](/connections/payments/adyen-card) and [dLocal](/connections/payments/dlocal-card). The connector pages describe any settings the payment service needs. * The result is only recorded when the payment service returns 3DS data. Some payment services only return it after a setting is turned on, and some return less detail than Gr4vy's own 3DS, such as only the final status. * When the payment service applies an exemption and doesn't authenticate the buyer, `three_d_secure` is empty. ### Connections with 3DS turned off 3DS isn't always run on the first connection. If the first connection in the list has 3DS turned off, Gr4vy sends the transaction there without 3DS. If that connection declines and Gr4vy retries on a connection with 3DS turned on, the buyer is asked to authenticate at that point. To keep the experience consistent, use the same 3DS setting on every connection in a routing rule, or list the connections with 3DS turned on first. ## Liability shift after a retry The 3DS result contains the acquirer details of the configuration that Gr4vy used to authenticate. When a transaction is retried on a connection with a different acquirer, the authorizing acquirer doesn't match those details. Most acquirers accept a 3DS result obtained with the details of another acquirer, and pass it on to the issuer. However, liability shift isn't guaranteed in this case. It depends on the acquirer and the card scheme rules. This is a trade-off between two risks: * **Without failover**, a transaction fails when its connection is unavailable. * **With failover**, a transaction can still succeed, but may lose liability shift when it's authorized by a different acquirer. Most merchants prefer to keep failover and accept the risk of losing liability shift on those transactions. If liability shift matters more than acceptance for some transactions, don't route them to connections with a different acquirer. ## Setting up 3DS configurations for multiple acquirers A merchant account configuration isn't linked to a connection. Gr4vy picks it based on the card scheme, currency, and metadata of the transaction only. See [matching a merchant account configuration](./setup#matching-a-merchant-account-level-configuration) for the full rules. Use the pattern that fits how you split transactions between acquirers. ### One acquirer for all transactions Add one configuration per card scheme, for all currencies, with empty metadata. This is the simplest setup, and it also covers failover connections. ### Different acquirers per currency Add one configuration per card scheme and currency, with the details of the acquirer for that currency. Route each currency to the matching connection with a Flow rule. Gr4vy picks the configuration for the transaction's currency automatically. Optionally, add a configuration for all currencies as a fallback. Configurations for a specific currency are always picked before it. ### Different acquirers per card scheme Each configuration already belongs to one card scheme. Add each scheme's configuration with the details of the acquirer that processes that scheme, and route each scheme to the matching connection with a Flow rule. ### Choosing the acquirer per transaction If your own system decides which acquirer processes each transaction, send that choice as transaction `metadata`. Use the same key and value in both places: * A [Flow routing rule](/guides/dashboard/flow/card-transactions#conditions) with a metadata condition that routes the transaction to the connection for that acquirer. * A 3DS configuration with the same metadata and the details of that acquirer. For example, a transaction with the following metadata matches both a routing rule and a 3DS configuration that use `"acquirer": "acquirer-b"`. ```json theme={"system"} { "metadata": { "acquirer": "acquirer-b" } } ``` Gr4vy doesn't check that the routing rule and the 3DS configuration agree, so keep them in sync when you change either one. For [native 3DS](./native) and [Click to Pay](/guides/features/click-to-pay/tas), set the metadata on the checkout session. > [!WARNING] > > A configuration with empty metadata for the same scheme and currency is picked > before a newer configuration with metadata. See > [choosing between matching configurations](./setup#choosing-between-matching-configurations). ### Many acquirers or merchant IDs in one account Avoid setting up many connections with different acquirer merchant IDs in a single merchant account. Features like [split routing](/guides/dashboard/flow/card-transactions#split-routing-condition) and failover can send a transaction to any of those connections, but the 3DS configuration can only match one acquirer. This makes it hard to predict which acquirer details are used. Instead, use a separate [merchant account](/guides/features/merchant-accounts/overview) for each set of acquirer details. Each merchant account has its own connections, Flow rules, and 3DS configurations. If your account doesn't have multiple merchant accounts enabled, contact your account manager. # Native 3-D Secure Source: https://integration.wpay.com.au/guides/features/3ds/native **Native 3-D Secure** gives you full control over the authentication experience by building the 3-D Secure challenge UI directly into your app. This approach is ideal for merchants who want a seamless, native experience without redirects or hosted pages. The implementation automatically handles all device fingerprinting requirements, including collecting device IP address, user agent, and other authentication metadata. This ensures full compliance with 3-D Secure specifications without requiring manual fingerprint collection. Native 3-D Secure is available across multiple platforms: * **Web**: Using [Secure Fields](/guides/payments/secure-fields/) for React or vanilla JavaScript integrations * **iOS**: Using the native SDK with Swift * **Android**: Using the native SDK with Kotlin Additionally, **Dynamic 3-D Secure** can be configured via Flow rules, allowing you to dynamically skip or force 3-D Secure based on anti-fraud decisions, cart items, or other transaction data. ## Prerequisites Native 3-D Secure requires [merchant account-level 3-D Secure configuration](/guides/features/3ds/setup#add-a-merchant-account-configuration) to be set up before you begin. This is different from per-connection setup. Gr4vy matches the configuration against the checkout session's `metadata`, not the transaction's. If your configurations use metadata, set the same key-value pairs on the checkout session. ## Integration guides Choose your platform to get started: > **[Web (Secure Fields)](/guides/payments/secure-fields/three-d-secure)** > > Implement Native 3DS in web applications using React or vanilla JavaScript. > **[iOS (Swift)](/guides/payments/ios/options)** > > Add Native 3DS to your iOS app with Swift. > **[Android (Kotlin)](/guides/payments/android/options)** > > Add Native 3DS to your Android app with Kotlin. # 3-D Secure Source: https://integration.wpay.com.au/guides/features/3ds/overview The system provides flexible 3-D Secure integration options that let you choose the approach best suited for your use case. Each method handles Strong Customer Authentication (SCA) with varying levels of control and customization. Most integrations automatically handle the device fingerprinting, card enrollment checks, authentication, challenge, and other 3DS nuances. Additionally, the system supports **Dynamic 3-D Secure**, allowing you to dynamically skip or force 3-D Secure based on anti-fraud decisions, cart items, or any other transaction-level data. To authenticate only when the issuer requires it, add a Flow rule that skips 3-D Secure. Gr4vy still runs 3-D Secure for [issuers with a known mandate](#issuer-mandated-3-d-secure). The flexible solution offers the following 3-D Secure integration methods. * [Embed](/guides/features/3ds/embed) automatically detects 3-D Secure for a transaction and handles all the user interactions to complete the transaction. * [Hosted 3DS](/guides/features/3ds/hosted) offers 3-D Secure via the API, allowing capture of card data that then relies on the hosted 3-D Secure to handle the user interaction. This can also be used in combination with [Secure Fields](/guides/payments/secure-fields/). * [Native 3DS](/guides/features/3ds/native) provides a fully customizable 3-D Secure experience built directly into your app. Available for web ([Secure Fields](/guides/payments/secure-fields/)), iOS (Swift), and Android (Kotlin). **Requires merchant account-level 3DS configuration.** * [External](/guides/features/3ds/external) 3-D Secure data can be passed in to create a card checkout experience including a custom 3-D Secure integration. Except for External, each method needs 3DS to be [set up](/guides/features/3ds/setup) first. Native 3DS requires a merchant account configuration. If you route transactions to more than one connection, also see [3DS with multiple connections](/guides/features/3ds/multiple-connections). Whichever method you use, the authentication result follows the same set of values. See [Authentication results](/guides/features/3ds/statuses) for the full reference. To control what happens when 3-D Secure can't complete, see [3-D Secure outcomes](/guides/dashboard/flow/card-transactions#3-d-secure-outcomes). ## Authentication and capture 3-D Secure authenticates the buyer. It doesn't capture the payment. The transaction's `intent` controls capture. With an `intent` of `authorize`, call the [capture endpoint](/reference/transactions/capture-transaction) once the transaction reaches `authorization_succeeded`, including after a 3-D Secure redirect or challenge. ## Store a card with 3-D Secure 3-D Secure runs as part of a transaction. Creating a payment method with the [create payment method](/reference/payment-methods/new-payment-method) endpoint doesn't run 3-D Secure, and neither does storing a card in a checkout session. To authenticate a card when you store it, create a transaction with an `amount` of `0` and `store` set to `true`, for example through Embed or Secure Fields. ## Failed authentication When the [authentication result](/guides/features/3ds/statuses) is `N`, the buyer failed authentication or the issuer denied the transaction. Gr4vy declines the transaction without sending it for authorization, so the buyer's bank shows no failed payment. The buyer can try again or use another card. The exception is `N` with reason `87`, which means the card is excluded from attempts processing. With **Attempt 3DS, continue if not enrolled**, the transaction continues without 3-D Secure. With **Force 3DS, decline if not enrolled**, it's declined. See [3-D Secure outcomes](/guides/dashboard/flow/card-transactions#3-d-secure-outcomes) for each case. ## Issuer-mandated 3-D Secure > [!NOTE] > > This feature is available in most environments, but not all. Contact the > support team to confirm availability for your account. Some card issuers require Strong Customer Authentication (SCA) on every transaction, regardless of your own 3-D Secure preferences. This is common for issuers in the European Economic Area (EEA) and the United Kingdom under the Second Payment Services Directive (PSD2). Some issuers in other regions apply equivalent mandates. To reduce authorization declines on these cards, Gr4vy identifies issuers with a known authentication mandate using bank identification number (BIN) data. When the issuer requires authentication, Gr4vy runs 3-D Secure even if a [Flow rule](/guides/dashboard/flow/card-transactions#action-3-d-secure) would otherwise skip it. The behavior depends on the transaction's resolved 3-D Secure outcome. * **No rule matches, or no 3-D Secure rule is configured.** Gr4vy attempts 3-D Secure. This is unchanged from the standard behavior. * **A rule skips 3-D Secure.** For an issuer with a known mandate, Gr4vy overrides the rule and attempts 3-D Secure instead. Cards without a known mandate continue to skip. * **A rule forces 3-D Secure.** Gr4vy forces 3-D Secure for all cards. This is unchanged. When the issuer's authentication requirement can't be determined — for example, the BIN isn't present in the dataset — Gr4vy treats the card as having no mandate and follows your configured behavior. Issuer mandates never override a connection with 3-D Secure turned off: in that case Gr4vy doesn't run 3-D Secure, even for a mandated issuer. An overridden skip rule is still recorded as having matched, so [Authentication Insights](/guides/dashboard/insights/authentications) continue to reflect the rule that applied. ## Digital wallets Gr4vy doesn't run 3-D Secure for Apple Pay, Paze, and Google Pay device tokens. The wallet authenticates the buyer, and the payment carries a cryptogram instead of a 3-D Secure result. Google Pay can also return a card saved in the buyer's Google Account (`PAN_ONLY`) rather than a device token (`CRYPTOGRAM_3DS`). Gr4vy processes these cards like any other card, so 3-D Secure can apply. See [3-D Secure with Google Pay](/guides/features/google-pay/overview#3-d-secure). ## Challenge language The issuer's access control server (ACS) shows the 3-D Secure challenge and decides which language to use. In browser-based flows, Gr4vy passes the buyer's browser language to the issuer. Gr4vy's iOS and Android SDKs don't set a language. You can't set the challenge language through the API or the SDKs, and an issuer that doesn't support the buyer's language shows the challenge in a language of its choice. # 3DS Scenarios Source: https://integration.wpay.com.au/guides/features/3ds/scenarios The 3DS Scenarios API allows a merchant to manage custom 3D Secure authentication scenarios for the hosted 3DS flow. Scenarios define specific authentication behaviors and response patterns for testing purposes. ## Overview 3DS Scenarios are configuration templates that define how 3D Secure authentication should behave for transactions. Use scenarios to: * Test specific authentication flows and outcomes * Configure consistent challenge behaviors across payment tests * Simulate different 3DS authentication scenarios > [!NOTE] > > 3DS Scenarios are only available for the hosted 3DS flow. For other integration methods, refer to the [3DS testing guide](/guides/features/3ds/testing) to use test cards from the underlying 3DS providers. ## Registering a scenario Register a new 3DS scenario by making a request to the [create scenario endpoint](/reference/three-ds-scenarios/new-three-ds-scenario): ```csharp C# theme={"system"} var res = await sdk.ThreeDsScenarios.CreateAsync(threeDSecureScenarioCreate: new ThreeDSecureScenarioCreate() { Conditions = new ThreeDSecureScenarioConditions() { CardNumber = "4242424242424242", }, Outcome = new ThreeDSecureScenarioOutcome() { Version = "2.2.0", Authentication = new ThreeDSecureScenarioOutcomeAuthentication() { TransactionStatus = "Y", }, }, }); ``` ```go Go theme={"system"} conditions := components.ThreeDSecureScenarioConditions{ CardNumber: gr4vy.String("4242424242424242"), } outcome := components.ThreeDSecureScenarioOutcome{ Version: "2.2.0", Authentication: &components.ThreeDSecureScenarioOutcomeAuthentication{ TransactionStatus: "Y", }, } scenarioCreate := components.ThreeDSecureScenarioCreate{ Conditions: conditions, Outcome: outcome, } res, err := client.ThreeDsScenarios.Create(ctx, scenarioCreate) ``` ```java Java theme={"system"} CreateThreeDsScenarioResponse response = gr4vyClient.threeDsScenarios().create() .threeDSecureScenarioCreate(ThreeDSecureScenarioCreate.builder() .conditions(ThreeDSecureScenarioConditions.builder() .cardNumber("4242424242424242") .build()) .outcome(ThreeDSecureScenarioOutcome.builder() .version("2.2.0") .authentication(ThreeDSecureScenarioOutcomeAuthentication.builder() .transactionStatus("Y") .build()) .build()) .build()) .call(); ThreeDSecureScenario scenario = response.threeDSecureScenario().orElse(null); ``` ```php PHP theme={"system"} $scenarioCreate = new ThreeDSecureScenarioCreate( conditions: new ThreeDSecureScenarioConditions( cardNumber: '4242424242424242' ), outcome: new ThreeDSecureScenarioOutcome( version: '2.2.0', authentication: new ThreeDSecureScenarioOutcomeAuthentication( transactionStatus: 'Y' ) ) ); $response = $sdk->threeDsScenarios->create($scenarioCreate); $scenario = $response->threeDSecureScenario; ``` ```python Python theme={"system"} scenario = client.three_ds_scenarios.create( conditions={ "card_number": "4242424242424242" }, outcome={ "version": "2.2.0", "authentication": { "transaction_status": "Y" } } ) ``` ```ts TypeScript theme={"system"} const scenario = await gr4vy.threeDsScenarios.create({ conditions: { cardNumber: "4242424242424242" }, outcome: { version: "2.2.0", authentication: { transactionStatus: "Y" } } }); ``` A 3DS scenario is used any time its conditions match a new transaction. A scenario doesn't need to match every condition to apply. Gr4vy picks the scenario with the most matching conditions, so a scenario can apply when only one of its conditions matches, such as the card number. When two scenarios match the same number of conditions, the oldest one applies. > [!TIP] > > Use a different card number for each scenario. Scenarios that share a card number can match each other's transactions. ## Simulating a card that isn't enrolled To simulate a card that isn't enrolled for 3-D Secure, set the outcome `version` to `null`. Gr4vy treats the matching transaction like a real card that isn't enrolled. The transaction continues without 3-D Secure, unless a [Flow rule](/guides/dashboard/flow/card-transactions#action-3-d-secure) forces 3-D Secure, in which case it's declined. The `authentication` object is still required, but its value isn't used. ```json theme={"system"} { "conditions": { "card_number": "4111111111111111" }, "outcome": { "version": null, "authentication": { "transaction_status": "Y" } } } ``` Once created, you can manage scenarios using the 3DS Scenarios API endpoints, such as [List 3DS Scenarios](/reference/three-ds-scenarios/list-three-ds-scenarios). # Setup 3DS card schemes Source: https://integration.wpay.com.au/guides/features/3ds/schemes Configure a 3-D Secure scheme profile for each card scheme, and learn what to enter in each field. When you [set up 3DS](./setup), add a scheme profile to your merchant account for each card scheme you want to authenticate. Gr4vy sends these details to the 3DS server when it authenticates a transaction. Most of them come from your acquirer, and can differ per card scheme. | Field | API field | Description | | - | - | - | | **Acquirer BIN** | `merchant_acquirer_bin` | The acquirer BIN for the card scheme. Your acquirer provides this value. Up to 11 characters. | | **Acquirer Merchant ID** | `merchant_acquirer_id` | The merchant ID provided by the acquirer used by your payment service. Up to 35 characters. | | **Merchant Name** | `merchant_name` | Your merchant name. This is sent on to the 3DS provider. Up to 40 characters. | | **Merchant Country Code** | `merchant_country_code` | The merchant's country, as an ISO 3166-1 numeric three-digit code. For example, `840` for the United States. | | **Merchant Category Code** | `merchant_category_code` | The four-digit merchant category code (MCC) for the merchant account. This describes the type of business, product, or service offered. | | **Merchant URL** | `merchant_url` | The URL of the website on which payments are made. | > [!NOTE] > > **Where to find these details** > > Most of these identifiers can be found in the payment service's dashboard, or > requested from your acquirer or payment service. If you use more than one > acquirer, see [3DS with multiple connections](./multiple-connections). ## Merchant account-level fields A [merchant account-level configuration](./setup#add-a-merchant-account-configuration) has these extra fields. They control which transactions use the configuration, not what's sent to the 3DS server. | Field | API field | Description | | - | - | - | | **Scheme** | `scheme` | The card scheme this configuration applies to. | | **Currency** | `currency` | The ISO 4217 currency this configuration applies to. Select **All currencies** in the dashboard, or set `null` in the API, to use it for every currency. A configuration for a specific currency is used before one for all currencies. | | **Metadata** | `metadata` | Key-value pairs that a transaction's `metadata` must contain for this configuration to be used. Leave this empty to use the configuration for every transaction with this scheme and currency. | > [!WARNING] > > Metadata on a 3DS configuration is a filter. If you add metadata, the > configuration is only used for transactions that send all of the same > key-value pairs. If no configuration matches, the transaction is processed > without 3DS. See [scheme profile resolution](./setup#scheme-profile-resolution) > for details. # 3-D Secure Setup Source: https://integration.wpay.com.au/guides/features/3ds/setup Configure 3-D Secure for your merchant account, turn it on for your connections, and learn how Gr4vy picks a scheme profile for each transaction. Setting up 3-D Secure (3DS) takes two steps: 1. Add a 3DS configuration for each card scheme to your merchant account. 2. For Embed, Hosted 3DS, and API transactions, turn on 3DS for each card connection that should use it. This setup is only required when using Gr4vy's 3DS solutions, such as Gr4vy Embed, Hosted 3DS, or native 3DS. No configuration is needed if [passing through 3DS data](./external) from an external source. ## Add a merchant account configuration A 3DS configuration holds the acquirer details that Gr4vy sends to the card scheme when it authenticates a transaction. Configurations belong to the merchant account and are shared across all its connections. You can add more than one configuration per card scheme, and Gr4vy picks one for each transaction based on its currency and metadata. See [scheme profile resolution](#scheme-profile-resolution) for how Gr4vy picks a configuration. To add a configuration, follow these steps: 1. Go to **Settings** in the Gr4vy Admin Panel. 2. Click **Edit merchant** and select the **3-D Secure** tab. 3. Add a [scheme profile](./schemes) for each card scheme to enable 3DS for. You can also manage these configurations with the [3DS configuration API](/reference/merchant-accounts/three-ds-configurations/new-three-ds-configuration). If you use more than one connection, see [3DS with multiple connections](./multiple-connections) before adding your configurations. ## Turn on 3DS for a connection For transactions created through Embed, Hosted 3DS, or the API, 3DS only runs on connections that have it turned on. 1. Go to the **Connections** tab. 2. Select an active card connection and go to the **3-D Secure** tab. 3. Switch the **Enable 3DS** toggle on. The **3-D Secure** tab is only shown for connections that accept 3DS data from Gr4vy. [Native 3DS](./native) and [Click to Pay](/guides/features/click-to-pay/tas) authenticate before a connection is chosen, so they use the merchant account configuration directly. ## Connection-level scheme profiles Some accounts also have scheme profiles configured on individual connections. This was the original way to set up 3DS, and it's no longer available by default. New accounts use merchant account configurations only. If a connection has a scheme profile for the card's scheme, Gr4vy uses it instead of the merchant account configuration. Connection-level profiles don't support Click to Pay or native 3DS. To move your 3DS setup to the merchant account, contact the support team. ## Scheme profile resolution When a transaction requires 3DS, Gr4vy determines which configuration to use based on the following order: 1. **Connection-level profile**: If the connection has a [connection-level scheme profile](#connection-level-scheme-profiles) for the card's scheme, that profile is used. 2. **Merchant account configuration**: Otherwise, Gr4vy uses a merchant account configuration that matches the transaction. ### Matching a merchant account-level configuration A merchant account-level configuration matches a transaction when all of the following are true: * The **scheme** equals the card scheme of the transaction. * The **currency** equals the currency of the transaction, or the configuration applies to all currencies. * Every key-value pair in the configuration's **metadata** is also present in the transaction's `metadata`, with the same value. The transaction can include other keys. For [native 3DS](/guides/features/3ds/native) and [Click to Pay](/guides/features/click-to-pay/tas), Gr4vy uses the checkout session's `metadata` instead. The buyer's country, the amount, and the connection aren't used to match a configuration. > [!WARNING] > > Metadata on a 3DS configuration is a filter, not a label. A configuration > with metadata only matches transactions that send all of its metadata > key-value pairs. Leave the metadata empty unless you want to use different > 3DS details for different transactions. A configuration with empty metadata matches every transaction for its scheme and currency, including transactions without metadata. A transaction without metadata only matches configurations with empty metadata. ### Choosing between matching configurations When more than one configuration matches a transaction, Gr4vy picks one in this order: 1. A configuration for the transaction's currency is used before a configuration for all currencies. 2. If more than one configuration remains, the oldest one is used. The number of metadata keys doesn't affect this order. An older configuration with empty metadata is picked before a newer configuration with matching metadata. To use a configuration for specific transactions only, make sure no older configuration with empty metadata exists for the same scheme and currency. Each configuration needs a unique combination of scheme, currency, and metadata. Adding a duplicate returns a `409` error. ### When no configuration matches If 3DS is enabled on the connection but no scheme profile or merchant account configuration matches the transaction, the transaction continues without 3DS. Gr4vy doesn't return an error. The transaction is processed without authentication, and its `three_d_secure` field is `null`. This happens before Flow rules are applied, so a Flow rule that forces 3DS doesn't change the outcome. To avoid this, add a configuration with empty metadata for each card scheme you accept, and use it as a fallback. If you also add configurations with metadata, create the fallback last, or make it apply to all currencies while the others set a currency. Otherwise the fallback is picked before them. # 3-D Secure testing Source: https://integration.wpay.com.au/guides/features/3ds/testing ## Testing approaches There are two approaches to testing 3D Secure: 1. **Test cards from 3DS provider** (documented below): Use specific card numbers provided by the underlying 3DS providers to simulate different authentication outcomes directly in your payment flows. 2. **Register custom scenarios**: Programmatically create and manage authentication scenarios via the [3DS Scenarios API](/guides/features/3ds/scenarios). This approach provides more flexible testing and configuration, but is only available for the hosted 3DS flow. > [!NOTE] > > This guide covers testing for **Embed**, **Hosted**, and **External** 3DS approaches only. > > If you're building a **Native 3DS** integration with Secure Fields, iOS (Swift), or Android (Kotlin), refer to the dedicated testing sections in those guides: > > * [Secure Fields Native 3DS testing](/guides/payments/secure-fields/three-d-secure#testing-and-validation) > * [Native SDK testing](/guides/payments/native/3ds#testing-and-validation) > > Native 3DS uses its own test cards. The cards on this page don't trigger a challenge in a Native 3DS integration. ## Configuration Before testing 3DS in the sandbox environment, ensure: * For Embed and Hosted 3DS, 3DS is [configured](/guides/features/3ds/setup) for each card scheme (Visa, Mastercard, American Express, and others) and turned on for your card connection. If you use merchant account configurations, one must [match the transaction](/guides/features/3ds/setup#matching-a-merchant-account-level-configuration), or 3DS is skipped * Flow rules do not skip 3DS for the test scenarios you want to verify * You're using the correct test card numbers for your desired outcome ## Testing with hosted embed and web flows The Gr4vy 3DS integration uses 3DSecure.io as the 3DS provider. Use the following test cards to simulate different authentication scenarios. ### Frictionless authentication - successful These cards return a frictionless response with successful authentication (`transStatus: Y`). | Card Number | Scheme | Expiry | Security Code | Outcome | | - | - | - | - | - | | `5200000000001203` | Mastercard | 03/30 | 100 | Frictionless success | | `4111111111101203` | Visa | 03/30 | 100 | Frictionless success | | `340000000022003` | American Express | 03/30 | 1000 | Frictionless success | | `3558111111121203` | JCB | 03/30 | 100 | Frictionless success | ### Challenge flow - manual and automatic These cards trigger a challenge that requires user interaction. | Card Number | Scheme | Expiry | Security Code | Outcome | | - | - | - | - | - | | `5240000000001072` | Mastercard | 03/30 | 100 | Challenge required | | `4111111111181072` | Visa | 03/30 | 100 | Challenge required | | `340000000082072` | American Express | 03/30 | 1000 | Challenge required | | `3558111111121872` | JCB | 03/30 | 100 | Challenge required | When a challenge is triggered, complete the verification process in the challenge modal. The outcome depends on your selection: * **Manual challenge (card ending in 72)**: You can choose to pass or fail the challenge. * **Automatic challenge (card ending in 70)**: The challenge automatically passes. ### Card not enrolled Cards without 3DS enrollment skip authentication. | Card Number | Scheme | Expiry | Security Code | Outcome | | - | - | - | - | - | | `9000100111111111` | Visa | 03/30 | 100 | Not enrolled | ### Issuer-mandated authentication These cards simulate an issuer that mandates authentication. Gr4vy attempts 3-D Secure for these cards even when a Flow rule skips it. See [Issuer-mandated 3-D Secure](/guides/features/3ds/overview#issuer-mandated-3-d-secure). | Card Number | Scheme | Expiry | Security Code | Outcome | | - | - | - | - | - | | `4111412911111172` | Visa | 03/30 | 100 | Enrolled — challenge required even with a skip rule | | `9000100111111117` | Visa | 03/30 | 100 | Not enrolled — 3-D Secure attempted, card not enrolled | To test the override, configure a Flow rule that skips 3-D Secure, then run a transaction with one of these cards. Gr4vy attempts 3-D Secure anyway because the simulated issuer mandates it. ### Advanced testing scenarios For more detailed testing including message version variations and 3DS method handling, refer to the [3DSecure.io sandbox documentation](https://docs.3dsecure.io/3dsv2/sandbox.html). The following test card patterns can be used for specific scenarios: * **Message version testing**: Cards with patterns like `xxxx` in positions 2-4 determine the 3DS message version (2.1, 2.2, or 2.3.1). * **3DS Method**: Cards with `x0xx` pattern include 3DS Method, `x1xx` exclude it, `x2xx` simulate timeout. * **Challenge outcomes**: Cards with `xx70` auto-pass, `xx71` auto-fail, `xx72` allow manual selection. # Manual account updates Source: https://integration.wpay.com.au/guides/features/account-updater/integration Submit stored cards to the batch account updater through the API and handle the updated details. The batch account updater is provided as an on-demand and asynchronous service, available via API. Currently Visa, Mastercard, American Express, and Discover are supported. > [!NOTE] > > American Express's updater service is only available to merchants with a direct American Express acceptance > agreement (American Express Direct) and their own Service Establishment (SE) number. It only returns updates for > cardholders who have previously made a successful payment under that SE number. ## Setup To have the batch account updater enabled on your instance please fill out [this form][form]. The enrollment happens directly with the card schemes and may take up to two weeks. The customer support team may reach out with more questions if needed. If you're already enrolled for this service with the schemes, filling out the form is still required to set up the necessary data points in the systems as well as the downstream partner's system. ## Request an update check Once enabled it is possible to call the account updater service using the [`POST /account-updater/jobs` endpoint][endpoint]. The request can contain any set of stored card IDs. The set of cards is then submitted to the schemes to receive any updates on their account details. This process can take up to 7 days. Any updates to the card are done automatically. Therefore there is no need to handle the response of the account updater service. If the payment method is made unavailable, either because the card has been canceled or the buyer has indicated to stop recurring payments by this merchant, then the card does not get updated but transactions are declined. At this point, deleting the stored card via the API is recommended. > [!WARNING] > > **Scheme enrollment limitations** > > Account updates are enabled individually per card scheme and per merchant ID (MID). It is important to be aware of > which schemes your MID has been enrolled in. If an update job is submitted for a card in a scheme for which you are not enrolled, > then your update may remain stuck in a pending state for up to 7 days. ## Handling updated details When the results are ready, the new account details are stored on the payment method but left in a "standby" mode. This means the details have been received but are not used immediately. The reason for this is that it can happen that details have been updated, for example, a new card with a new expiry date has been issued, but these details are pending activation of the card holder. The old card might still be in use in the meantime. Card details are eventually updated when it is determined that the old details are no longer valid. After the card details have been updated the payment method ID remains the same but the `last_replaced_at` field on the payment method is updated. This way, as a merchant, you can keep using the same identifier to trigger recurring payments using up-to-date details. There are three scenarios in which the actual replacement occurs. * When the card has an expiry date in the past * When an update has been received that only changes the expiration date * When a transaction using this card is declined with any of the following codes * `canceled_payment_method` * `expired_payment_method` * `unavailable_payment_method` * `unknown_payment_method` ## Webhooks Once a payment method has been updated a webhook is sent out for the payment method to notify you of the change. You can use this webhook to pull in the latest expiry date, last 4 digits, or BIN for use in your own UI. [form]: https://gr4vy.atlassian.net/servicedesk/customer/portal/1/group/7/create/32 [endpoint]: /reference/account-updater/new-account-updater-job # Account Updater Source: https://integration.wpay.com.au/guides/features/account-updater/overview Keep stored card details up to date automatically with batch and real-time account updates. To keep card details up to date most card schemes have created services to inform customers about changes to card and account details, for example, when the expiry date of a card changes. Using an account updater, it is possible to update the details of stored card details automatically. This way it is possible to keep all information fresh and up to date and maintain continuity in your payment cycle, without the buyer having to re-fill their card details after they have changed. Card details are automatically updated when the following events occur. * A new expiry date is available * A new card is issued, replacing the old card * A card is reported as stolen or lost * The buyer has no longer authorized the merchant to automatically charge the card Two types of account updater are offered; batch and real time. The batch account updater allows you to define a set of stored cards to update whereas the real time account updater checks for card updates during transaction processing. > [!NOTE] > > Gr4vy only updates stored card details through its own account updater. If a payment service runs its own > account updater on cards it stores, those updates aren't applied to the card details stored in Gr4vy. ## Enable account updater To enable an account updater on your instance, fill out the [account updater enrollment form][form]. One form covers [manual](/guides/features/account-updater/integration), [scheduled](/guides/features/account-updater/scheduler), and [real-time](/guides/features/account-updater/real-time) updates, so select every type you want to use. List every merchant account, merchant ID (MID), and region that you want to enroll. For other questions, [contact support](https://support.gr4vy.com). If you're already enrolled for this service with the schemes, filling out the form is still required to set up the necessary data points in the systems as well as the downstream partner's system. ### Before you enroll Enrollment happens with each card scheme, so the form asks for your business details and the identifiers that your acquirer or payment service provider assigns to you for each scheme. Collect these before you start. * **Business details**: Parent company name, line of business, business address, internet address, merchant category codes, merchant descriptors, telephone number, contact name, and email address. * **Card Acceptor ID (CAID)**: Your card acceptor ID for Visa. Your acquirer or processor can provide it. For Mastercard, the merchant ID is often the same value. * **Acquirer identifiers**: The acquirer identifiers used for your merchant accounts, such as the Visa acquirer bank identification number (BIN). This is usually a six to eight digit value that starts with a 4. * **American Express Service Establishment (SE) number**: If you have several SE numbers under one top-level SE number, provide the top-level number. * **Discover merchant ID**: Your Discover MID. [form]: https://gr4vy.atlassian.net/servicedesk/customer/portal/1/group/7/create/32 # Real time account updates Source: https://integration.wpay.com.au/guides/features/account-updater/real-time Update stored card details automatically when a merchant-initiated payment is declined, and test it with the simulator. The real time account updater automatically checks for updated card details at the point of a decline. Where new card details are available, the payment method is updated in place and the transaction is retried with the same connector. The payment method keeps its ID, and Gr4vy sends a `payment-method.updated` [webhook](/guides/features/webhooks/events). This approach requires no integration changes. ## Setup To have the real time account updater enabled on your instance please fill out [this form][form]. The enrollment happens directly with the card schemes and may take up to two weeks. The customer support team may reach out with more questions if needed. If you're already enrolled for this service with the schemes, filling out the form is still required to set up the necessary data points in the systems as well as the downstream partner's system. ## Usage and limitations The real-time account updater only runs when all of the following are true. * The transaction uses a stored card. * The transaction is a subsequent merchant-initiated transaction, with `merchant_initiated` and `is_subsequent_payment` set to `true`. * The connection was sent the card number (PAN). Digital wallets and network tokens don't qualify. Neither does a [closed-loop](/guides/features/recurring-payments/open-loop) connection that charges its own stored token for the card. * The decline has one of the following error codes, or a merchant advice code (MAC) of `01`. * `canceled_payment_method` * `expired_payment_method` * `incorrect_expiry_date` * `issuer_decline` * The card hasn't been excluded from updates after an earlier final response from the updater. The updater runs at most once per transaction. Only Visa and Mastercard are currently supported. Cards with pending updates from the batch account updater use the new stored credentials instead. [form]: https://gr4vy.atlassian.net/servicedesk/customer/portal/1/group/7/create/32 > [!NOTE] > > As with other updates, merchants are only charged for cards that are updated. ## Simulator Your sandbox instance is automatically enabled to use the real time account updater simulator. You can test different scenarios by using the `connection_options` property when creating a transaction. Please see the various scenarios and examples below. > [!NOTE] > > Using the card simulator connection is the easiest way to test different scenarios as this connector returns specific decline error codes depending on the transaction amount. The real time account updater logic only triggers for subsequent merchant-initiated transactions that send the card number, so set up your test as follows. 1. In the dashboard, open the Card simulator connection and turn on **Open loop**. It's off by default, and without it the connection charges its own token instead of the card number. 2. Create a stored payment method, for example with a customer-initiated transaction with `store` set to `true`. 3. Create a subsequent merchant-initiated transaction with the payment method ID, an amount that the simulator declines with a qualifying error code, and the `connection_options` for the scenario. For example, the amount `200005` returns `expired_payment_method`. See the [simulator amounts](/guides/features/simulators/overview). When the updater returns a new card number or expiry date, the payment method is updated, the transaction is retried on the same connection, and Gr4vy sends a `payment-method.updated` webhook. ```json Request theme={"system"} { "amount": 200005, "country": "AU", "currency": "AUD", "payment_service_id": "46973e9d-88a7-44a6-abfe-be4ff0134ff4", "merchant_initiated": true, "payment_source": "card_on_file", "is_subsequent_payment": true, "payment_method": { "method": "id", "id": "f6933999-362f-468b-83c3-ed16b1a4ca17", "redirect_url": "https://gr4vy.com/callback" }, "connection_options": { "account-updater": { "response_code": "updated", "account_number": "4242424242424242" } } } ``` ### Examples The following are a few examples of how to use the `connection_options` to trigger particular situations. ```json Full new details theme={"system"} { ... "connection_options": { "account-updater": { "response_code": "updated", "account_number": "4242424242424242", "expiration_month": "12", "expiration_year": "2050" } } } ``` ```json New number only theme={"system"} { ... "connection_options": { "account-updater": { "response_code": "updated", "account_number": "4242424242424242" } } } ``` ```json Unchanged theme={"system"} { ... "connection_options": { "account-updater": { "response_code": "unchanged" } } } ``` ```json Error theme={"system"} { ... "connection_options": { "account-updater": { "error_code": "error" } } } ``` # Scheduled account updates Source: https://integration.wpay.com.au/guides/features/account-updater/scheduler The batch account updater supports automatic scheduling of an entire vault for updates every 12 days. This allows your entire vault to stay up to date on a monthly basis, without the need to manually submit cards to the account updater. ## Setup To use the account updater service, you need to have the service enabled. To have the batch account updater enabled on your instance, please fill out [this form][form]. The enrollment happens directly with the card schemes and may take up to two weeks. The customer support team may reach out with more questions if needed. If you're already enrolled for this service with the schemes, filling out the form is still required to set up the necessary data points in the systems as well as the downstream partner's system. Next, the scheduler needs to be set up, which requires a change to be made on your instance. When you fill in the form, please select if you want to use the scheduler or not. ## Usage and limitations * The entire content of the vault is updated every 12 days, allowing for 2 full updates within 1 calendar month * You are only charged for cards that are updated * Any card already submitted to the account updater in some other way does not get submitted as part of the scheduler * Any card already updated but pending its first use in the vault does not get updated until the new card has been automatically used. This may happen when a card is updated, but the old card is still in use. * Only Visa, Mastercard, American Express, and Discover cards are supported by the account updater, and any others are ignored by the scheduler * Any cards previously marked as dead (`LCA`) are ignored [form]: https://gr4vy.atlassian.net/servicedesk/customer/portal/1/group/7/create/32 # Automatic anti-fraud device fingerprinting Source: https://integration.wpay.com.au/guides/features/anti-fraud/fingerprint Enabling automatic device fingerprinting A device fingerprint uniquely identifies the device a customer is using at checkout. Anti-fraud connections use this to determine whether this device or user has been flagged for fraudulent behavior in the past, allowing it to reject the transaction and protecting you from fraud. A device fingerprint is different from the card [`fingerprint`](/guides/features/cards/fingerprints) on a payment method, which identifies the card number rather than the device. Each anti-fraud service has a different way of collecting the device fingerprint, and therefore the `f.js` library wraps around each of these, allowing focus on the preferred anti-fraud service. > [!NOTE] > > If using Embed, then none of the steps below are necessary. Embed automatically loads the > right library and ensures the device fingerprint is collected. ## Usage without Embed To create a device fingerprint, load the `f.js` library into your checkout page. This can be the page on which you render your checkout using Secure Fields, or any other integration. ```html theme={"system"} ``` > [!NOTE] > > In a sandbox environment make sure you use `api.sandbox.{instance_id} > .gr4vy.app/f.js`. This automatically loads the fingerprinting library for your [**Primary**](/guides/features/anti-fraud/overview#connection-mode) anti-fraud service — and, if any [**Silent**](/guides/features/anti-fraud/silent-mode) services are configured, their libraries too — seeding all of them with the same shared fingerprint value. The value is attached to `window.gr4vyAntiFraud.f` and can be sent as the [`anti_fraud_fingerprint`](/reference/transactions/new-transaction#body-anti-fraud-fingerprint) property when creating a transaction. ## Embed and custom When using Embed, the device fingerprint is automatically created and sent along when creating a transaction. This behavior can be overridden by sending in a custom fingerprint to Embed. For example, if the anti-fraud service in use is known, and that service's fingerprint script has already been loaded, then the fingerprint created by that service can be passed directly to Embed or the API. When the `antiFraudFingerprint` value is set, the device fingerprint libraries are not loaded and, instead, the fingerprint provided is passed along as-is when creating a transaction. ```js React theme={"system"} ``` ```js Node / CDN theme={"system"} setup({ gr4vyId: "example", antiFraudFingerprint: "my-fingerprint-id", ... }); ``` > [!NOTE] > > For most anti-fraud services the `fingerprint` represents either the session > ID or device fingerprint ID defined by that service's anti-fraud library. > Please refer to the documentation of your anti-fraud service for more details. ## Custom & native fingerprints In the event the automated solution cannot be used to generate the device fingerprint, use the anti-fraud fingerprint libraries directly and then pass the fingerprint by using the [`anti_fraud_fingerprint`](/reference/transactions/new-transaction#body-anti-fraud-fingerprint) when creating a new transaction. > [!WARNING] > > When accepting PayPal the device fingerprint is sent to both PayPal and > the Anti-Fraud service. To ensure the same fingerprint works for both > services, creating a PayPal fingerprint first is recommended, and then passing > the ID generated by their library to the anti fraud fingerprint library to > seed the value. ## Native mobile fingerprinting If you build a native mobile checkout without one of Gr4vy's [Embed SDKs](/guides/payments/native/sdks) wrapping a web view, the providers' web fingerprint scripts cannot run. Instead, integrate each anti-fraud provider's native iOS and Android SDKs and forward a single shared identifier as `anti_fraud_fingerprint`. The platform forwards that single identifier to every configured provider — Primary and any [Silent](/guides/features/anti-fraud/silent-mode) services — as the device session identifier on each provider's server-side decision request. So the only requirement for native is: **register every provider's device profile under one shared identifier, and pass that exact same identifier to Gr4vy as `anti_fraud_fingerprint` so each provider can correlate the device profile with its decision request.** Shared means the same identifier for every provider in a checkout. It doesn't mean one fixed identifier for all buyers or devices. There are two viable patterns: ### Pattern A — Merchant-generated identifier, pushed into each provider's SDK Generate a unique identifier (for example, a UUID) at the start of the checkout session, then push it into each provider's mobile SDK *before* it emits any events. Each provider registers its device profile under that identifier; the same identifier is then forwarded server-side, and each provider can stitch the profile to the transaction. Use whatever format the providers you're integrating with accept. This is the native equivalent of how `f.js` exposes a fingerprint on `window.gr4vyAntiFraud.f` on web. ```swift Swift theme={"system"} let fingerprintUUID = UUID().uuidString // Push the UUID into the anti-fraud provider SDK BEFORE any events are emitted, // so the device profile is registered under this UUID on the provider's backend. // The method names below are pseudocode — see your provider's mobile SDK docs. ProviderASDK.shared.configure(siteId: PROVIDER_A_SITE_ID) ProviderASDK.shared.setDeviceUID(fingerprintUUID) // Same UUID seeds the second provider's session. ProviderBSDK.profileRequest(sessionID: fingerprintUUID) // Forward the same UUID to your own backend, which then includes it // as `anti_fraud_fingerprint` when calling Gr4vy's Create transaction API. yourCheckoutBackend.startTransaction( amount: 12345, currency: "AUD", antiFraudFingerprint: fingerprintUUID ) ``` ```kotlin Kotlin theme={"system"} val fingerprintUUID = UUID.randomUUID().toString() // Method names below are pseudocode — see your provider's mobile SDK docs. ProviderASDK.getInstance().configure(context, PROVIDER_A_SITE_ID) ProviderASDK.getInstance().setDeviceUID(fingerprintUUID) ProviderBSDK.getInstance().profile( ProfilingOptions().setSessionID(fingerprintUUID), null ) // Forward the same UUID to your own backend, which then includes it // as `anti_fraud_fingerprint` when calling Gr4vy's Create transaction API. yourCheckoutBackend.startTransaction( amount = 12345, currency = "AUD", antiFraudFingerprint = fingerprintUUID ) ``` > [!NOTE] > > The exact "set device UID" / "set event ID" API name varies by provider and SDK version — refer to your provider's mobile SDK documentation. ### Pattern B — Use one provider's native UID as the shared identifier If a provider's mobile SDK auto-generates a stable per-install identifier (for example, a UID derived from iOS's identifier for vendor (IDFV)) and does not expose a setter to inject your own value, let it generate the identifier, read it out, and use it as the seed for any other providers and for Gr4vy. ```swift Swift theme={"system"} // Method names below are pseudocode — see your provider's mobile SDK docs. ProviderASDK.shared.configure(siteId: PROVIDER_A_SITE_ID) let providerAUID = ProviderASDK.shared.getDeviceUID() // SDK-generated // Use Provider A's UID as the seed for Provider B's session. ProviderBSDK.profileRequest(sessionID: providerAUID) // Forward the UID to your own backend, which then includes it // as `anti_fraud_fingerprint` when calling Gr4vy's Create transaction API. yourCheckoutBackend.startTransaction( amount: 12345, currency: "AUD", antiFraudFingerprint: providerAUID ) ``` ```kotlin Kotlin theme={"system"} // Method names below are pseudocode — see your provider's mobile SDK docs. ProviderASDK.getInstance().configure(context, PROVIDER_A_SITE_ID) val providerAUID = ProviderASDK.getInstance().getDeviceUID() ProviderBSDK.getInstance().profile( ProfilingOptions().setSessionID(providerAUID), null ) // Forward the UID to your own backend, which then includes it // as `anti_fraud_fingerprint` when calling Gr4vy's Create transaction API. yourCheckoutBackend.startTransaction( amount = 12345, currency = "AUD", antiFraudFingerprint = providerAUID ) ``` > [!NOTE] > > Each provider may have its own format requirements for the session or device identifier it accepts. Refer to your provider's connector documentation for the exact format expected for `anti_fraud_fingerprint`. ### Which pattern to use * **Use Pattern A** when every provider's mobile SDK exposes a way to inject a merchant-generated identifier. It keeps the seed provider-independent — useful if you may add or swap providers later. * **Use Pattern B** when at least one provider's SDK does not expose an injection hook. It inverts the dependency — that provider becomes the source of the identifier — but always works. ## Merchant accounts In an environment with multiple merchant accounts it's important to add the `merchant_account_id` as a query parameter to the fingerprint script. This ensures the script is loaded for the [**Primary**](/guides/features/anti-fraud/overview#connection-mode) anti-fraud service — and any [**Silent**](/guides/features/anti-fraud/silent-mode) services configured — for that specific merchant account. ```html theme={"system"} ``` Please note that when using an Integration API key with Embed, then the `merchantAccountId` needs to be set explicitly for Embed to load the anti-fraud scripts for that merchant account. ```js React theme={"system"} ``` ```js Node / CDN theme={"system"} setup({ gr4vyId: "example", merchantAccountId: "my-merchant-id" ... }); ``` ## Troubleshooting If the transaction doesn't include a device fingerprint, or the anti-fraud service can't match it to a device profile, check the following. * The `merchant_account_id` on the `f.js` script matches the merchant account of the transaction. Without it, the script loads the anti-fraud services of the `default` merchant account. * The merchant account has an active anti-fraud service. When it has none, `f.js` loads no libraries and doesn't set `window.gr4vyAntiFraud`. * You read `window.gr4vyAntiFraud.f` and send it as `anti_fraud_fingerprint` in the transaction request. The libraries finish loading when `window.gr4vyAntiFraud.ready` is `true`, at which point the `gr4vyAntiFraudReady` event fires on `window`. * Your Content Security Policy (CSP) allows the `f.js` script from your Gr4vy API host and the scripts that each anti-fraud service loads. See your anti-fraud service's documentation for its domains. # Anti-fraud Source: https://integration.wpay.com.au/guides/features/anti-fraud/overview Use decisions and scores from anti-fraud services in Flow rules to manage risk in your transaction flow. As part of Flow the system allows you to build rules which use decisions and scores from anti-fraud services. > [!NOTE] > > For detailed information on setting up and configuring specific anti-fraud providers, see the anti-fraud services in the [connection catalog](/connections/catalog?category=anti-fraud). Cybersource, Forter, Riskified, Sardine, and Sift are supported. ![Flow rule with an anti fraud decision](https://mintcdn.com/wpay/c50rx3S48hHhNNxb/assets/images/anti-fraud/flow-rule.png?fit=max\&auto=format\&n=c50rx3S48hHhNNxb\&q=85\&s=9ba05f355e106533d0e8c3db37f3ed34) You can use these scores and decisions for card transactions to evaluate the risk of a transaction and take appropriate actions to, for example, force 3-D Secure, decline a transaction, or route to a specific payment service. ## How does it work For every transaction, your [**Primary**](#connection-mode) anti-fraud service is only called **once**. When a request is made for a transaction, your active anti-fraud provider is only called once, assuming a rule is triggered for which an anti-fraud score or decision is the condition. This feature prevents incurring any extra cost by calling your anti-fraud service multiple times, or calling your service unnecessarily if the rule is not triggered. ## Connection mode Each of your configured anti fraud services can be set to a **Primary**, [**Silent**](/guides/features/anti-fraud/silent-mode), or **Inactive** mode. | Mode | Description | | :- | :- | | **Primary** | Traffic is processed through this connector's fraud checks. It returns an approval or decline, which determines whether the transaction continues. Only one primary connector can be configured at any time. | | [**Silent**](./silent-mode) | Traffic is processed through this connector, but its decision is not enforced. Results are logged for learning/analysis only. | | **Inactive** | No traffic is processed through this connector, but it is available to be re-activated at any time. | ## Anti-fraud decisions Gr4vy maps each provider's response to one of the following decisions. The transaction's `anti_fraud_decision` field holds the decision from the Primary service, and the **Anti-fraud decision** condition in Flow matches on the same values. | Decision | Flow label | Description | | :- | :- | :- | | `accept` | Accept | The service accepted the transaction. | | `reject` | Reject | The service rejected the transaction. | | `review` | Review | The service flagged the transaction for a [manual review](/guides/features/anti-fraud/reviews). | | `error` | Error | The service returned an error decision, or a decision that Gr4vy can't map to another value. | | `exception` | Error (network error) | The call to the service failed, for example because it timed out or returned a server error. | | `skipped` | Skipped (unable to get decision) | Gr4vy couldn't get a decision, for example because the service rejected the request. | | `pending` | Not applicable | The check hasn't completed yet. | `anti_fraud_decision` is `null` when no anti-fraud check ran for the transaction. For a `review` decision, the field keeps the value `review` after the review is resolved. # Anti-fraud setup Source: https://integration.wpay.com.au/guides/features/anti-fraud/setup Setting up an anti-fraud service To enable anti-fraud you first need to create a new anti-fraud connection which can be completed via the dashboard by heading to **Connections** -> **Catalog**. On this page, scroll down to the bottom to explore the available anti-fraud services. > [!NOTE] > > For detailed setup instructions for each anti-fraud provider, visit the appropriate connector page: > > * [Cybersource](/connections/anti-fraud/cybersource) > * [Forter](/connections/anti-fraud/forter) > * [Riskified](/connections/anti-fraud/riskified) > * [Sardine](/connections/anti-fraud/sardine) > * [Sift](/connections/anti-fraud/sift) ![Anti-fraud connections](https://mintcdn.com/wpay/c50rx3S48hHhNNxb/assets/images/anti-fraud/connections.png?fit=max\&auto=format\&n=c50rx3S48hHhNNxb\&q=85\&s=cd47340b18d69a81db4ab6d0f067410b) To enable your chosen anti-fraud connection: 1. Provide the required credentials and other details to configure the connection. 2. Set the [connection mode](/guides/features/anti-fraud/overview#connection-mode) to Primary. 3. Create a new card transactions flow rule with an anti-fraud decision as its condition. ## Multiple anti-fraud services You can configure multiple anti-fraud services on the same merchant account. Exactly one service can be in **Primary** [connection mode](/guides/features/anti-fraud/overview#connection-mode) at a time, and its decisions drive your Flow rules. Any number of additional services can run in **[Silent](/guides/features/anti-fraud/silent-mode)** mode alongside the Primary. Whenever a Flow rule triggers an anti-fraud check, Silent services are evaluated alongside the Primary and produce decisions that are logged but not enforced. This makes it straightforward to evaluate a new service against real traffic and migrate to it without disruption. See [Silent mode](/guides/features/anti-fraud/silent-mode) for the full migration playbook. # Silent mode Source: https://integration.wpay.com.au/guides/features/anti-fraud/silent-mode Run additional anti-fraud services in Silent mode alongside your Primary service to compare and migrate without enforcing their decisions ## Overview Silent mode lets you run one or more anti-fraud services *alongside* your Primary service without enforcing their decisions. Whenever a Flow rule triggers an anti-fraud check, Silent services are evaluated alongside the Primary, return decisions, and write transaction history events — but those decisions never reject, step up, or otherwise affect the customer's flow. This is the recommended way to: * **Warm up a new service.** Let it gather data and tune its models on real traffic before you switch over. * **Validate a new service before switching over.** Compare its decisions against your Primary in the dashboard. * **Decommission an old service gradually.** Keep the outgoing service in Silent mode for monitoring after switching to a new Primary. ## Connection modes Each configured service is in one of three modes (see [Anti-fraud overview](/guides/features/anti-fraud/overview#connection-mode)): | Mode | Receives traffic? | Decision enforced? | | :- | :- | :- | | **Primary** | Yes (when an anti-fraud check runs) | Yes | | **Silent** | Yes (when an anti-fraud check runs) | No (logged only) | | **Inactive** | No | — | Exactly one service can be Primary at a time. Any number of services can be Silent. ## How it works Anti-fraud services are only called when a Flow rule that depends on an anti-fraud score or decision triggers for a transaction (see [Anti-fraud overview](/guides/features/anti-fraud/overview#how-does-it-work)). When that happens: * The Primary service is called **synchronously** so its decision can be evaluated against your Flow rules. * Each Silent service is called **asynchronously** via the message queue, so it never adds latency to the transaction. * All checks (Primary and Silent) are stored against the transaction. The dashboard surfaces every Silent decision as a transaction history event so you can compare side-by-side without affecting the customer. * If a webhook arrives for a Silent service (for example, a Cybersource Decision Manager review status change), the corresponding check is updated, but no further action is taken on the transaction. ## Fingerprinting with multiple services > [!NOTE] > > No matter how many anti-fraud services you have configured (Primary + any number of Silent), your integration only ever passes **one** device fingerprint value per transaction. There is **no** per-provider fingerprint field. A single shared fingerprint identifier per session is used to seed the fingerprinting library of *every active provider* — that is, the Primary and any Silent services (Inactive services receive no traffic and are skipped). Each provider registers that identifier under its own session/device key. At transaction time you submit the identifier once, on [`anti_fraud_fingerprint`](/reference/transactions/new-transaction#body-anti-fraud-fingerprint), and the platform forwards it to the Primary and each Silent service using whatever per-provider mapping that connector defines. See each connector's documentation for the specific field it expects. ### End-to-end flow with Embed (Primary + Silent) ```mermaid theme={"system"} sequenceDiagram participant Browser as Customer browser (Embed) participant FJS as Gr4vy /f.js participant ACDN as Provider A CDN participant BCDN as Provider B CDN participant API as Gr4vy Transactions API participant Q as Async queue participant A as Provider A Decision API participant B as Provider B Decision API Browser->>FJS: GET /f.js FJS-->>Browser: loader (one shared seed + per-service loaders) Browser->>ACDN: Provider A fingerprint script (seeded with shared id) Browser->>BCDN: Provider B fingerprint script (seeded with shared id) Note over ACDN,BCDN: Both providers now hold the same identifier. Browser->>API: POST /transactions { anti_fraud_fingerprint: id } API->>A: Decision call (id) — synchronous Note over API,A: Provider A — Primary, enforced by Flow rules API->>Q: Enqueue Silent decision (id) Q-->>B: Decision call (id) — asynchronous Note over Q,B: Provider B — Silent, logged only, no added latency ``` When you use Embed (web or mobile), this happens automatically — adding a Silent service requires no code changes in your integration. If you don't use Embed, see [Automatic anti-fraud device fingerprinting](/guides/features/anti-fraud/fingerprint) for how to load `f.js` yourself or how to seed the same identifier into native mobile SDKs. ## Migration pattern The intended migration playbook for switching from one anti-fraud provider to another: **1. Add the new provider as Silent** Configure the new provider in the dashboard with connection mode set to **Silent**. Your existing Primary keeps enforcing decisions; the new provider starts collecting data and emitting decisions side-by-side. **2. Compare in the dashboard** Review the side-by-side decisions on transactions over a representative period (typically a few weeks). Tune the new provider's rules and models as needed. **3. Promote the new provider** Flip the flags: set the new provider to **Primary** and the old provider to **Silent** (or **Inactive**). No client-side change is required — the same `anti_fraud_fingerprint` value keeps flowing on every transaction; only enforcement changes. **4. (Optional) Retire the old provider** Once you're confident, set the old provider to **Inactive** to stop traffic to it entirely. ## API reference Connection mode is set via the [Update anti-fraud service API](/reference/anti-fraud-services/update-anti-fraud-service): ```http theme={"system"} PUT /anti-fraud-services/{anti_fraud_service_id} ``` ```json Request body theme={"system"} { "anti_fraud_service_definition_id": "sift-anti-fraud", "active": false, "silent": true } ``` A service cannot be `active` and `silent` at the same time: * `active: true` makes the service **Primary** (the existing Primary, if any, is automatically deactivated). * `silent: true` makes it **Silent**. * If both are `false`, the service is **Inactive**. `anti_fraud_service_definition_id` is required by the schema, but the underlying provider can't be changed on update — you must echo the service's existing definition string (for example, `sift-anti-fraud`). Read it from the dashboard or from the [Get anti-fraud service](/reference/anti-fraud-services/get-anti-fraud-service) API response for the service you're updating. # Anti-fraud simulator Source: https://integration.wpay.com.au/guides/features/anti-fraud/simulator An simulator anti-fraud connection is provided which can be used for integration testing. This anti-fraud connection makes it straight forward to test all response decisions and asynchronous webhooks. ## Credentials To configure the connection, you need to set the following credentials. | Credential | Description | | :- | :- | | API Key | This is not validated and can be set to any string | ## Decision mapping To test various anti-fraud decisions you must create a buyer with a specific email address (see table below). If you do not pass a buyer with email address the decision is `skipped`. | Buyer email | Decision | Description | | :- | :- | :- | | `accept@gr4vy.com` | `accept` | Simulates a low risk transaction | | `reject@gr4vy.com` | `reject` | Simulates a high risk transaction | | `review@gr4vy.com` | `review` | Simulates a transaction which needs manual review | | `error@gr4vy.com` | `error` | Simulates a bad request | | `timeout@gr4vy.com` | `exception` | Simulates a server timeout/bad response | ## Errors To simulate a network error, for example to simulate a situation where the connector is unable to connect to the anti-fraud provider you can use `unavailable@gr4vy.com`. This results in the decision being skipped and creates an `anti-fraud-decision-error` transaction event. ## Webhooks To test the `review` status you can manually send a POST request to the anti-fraud service webhook URL. You can find this value from the dashboard or via the API when [fetching the anti-fraud service](/reference/anti-fraud-services/get-anti-fraud-service). ```json Response theme={"system"} { "type": "anti-fraud-service", "id": "f80b5fc6-c95d-4a28-a929-4d5268c3e141", "merchant_account_id": "default", "display_name": "Simulator", "anti_fraud_service_definition_id": "mock-anti-fraud", "active": true, "reviews_enabled": true, "created_at": "2024-04-01T00:00:00+00:00", "updated_at": "2024-04-01T00:00:00+00:00", "webhook_url": "https://api.sandbox.example.gr4vy.app/i/af/-AtfxsldSiipKU1SaMPhQWZvcnRlci1hbnRpLWZyYXVk/5bOPmoOpwNRR6bKZaPD7MNjiZcYgSu1cNtNUO0jLUNc", "fields": [] } ``` The expected `POST` body for the webhook URL must contain the UUID of the transaction and a decision of `ACCEPT` or `REJECT`: ```json Simulated webhook theme={"system"} { "transactionId": "d6d2bc16-8a25-4e05-a595-b4cd51fe4932", "decision": "ACCEPT" | "REJECT" } ``` ## Device fingerprinting Device fingerprinting is not currently supported via this connection. # Decrypted Apple Pay tokens Source: https://integration.wpay.com.au/guides/features/apple-pay/decrypted-tokens Some merchants and platforms may have the option to decrypt an Apple Pay token themselves. For this scenario, direct pass through of these decrypted Apple Pay tokens to the API is supported using the [`network-token` method](/reference/transactions/new-transaction#body-payment-method). ```json Payment method theme={"system"} { "method": "network-token", "token": "4111111111111111", "expiration_date": "02/30", "cryptogram": "AAEBAwQjSQAAXX...", "eci": "05", "card_suffix": "1234", "card_scheme": "visa", "card_source": "apple-pay", "cardholder_name": "John Smith" } ``` Because you run the Apple Pay session and decrypt the token yourself, you don't need to register domains or upload the Domain Association File in the dashboard for this integration. You still need a card connection that accepts decrypted Apple Pay tokens. # Apple Pay on iOS with the SDK Source: https://integration.wpay.com.au/guides/features/apple-pay/ios Both the native iOS SDK and React Native SDKs support native Apple Pay out of the box. ## About this integration The SDK requires you to perform the following steps to get set up. * Enable Apple Pay in your merchant dashboard. * Sign up for an Apple Pay Developer account and [register a payment processing certificate](/guides/features/apple-pay/ios#register-a-certificate). * [Add the merchant ID](/guides/features/apple-pay/ios#add-the-certificate-to-your-app) and its processing certificate to your app. * Pass the merchant ID to the constructor of the SDK. ## Enable Apple Pay To enable Apple Pay, head over to your dashboard and then go to **Connections** -> **Catalog** -> **Apple Pay**. Next, complete, and submit the form to create a new Apple Pay service. ### Register a certificate To process Apple Pay in a mobile app, register for an Apple Pay developer account and join the [Apple developer program](https://developer.apple.com/programs/). Once set up, generate a new Apple Pay processing certificate using the dashboard. Go to **Connections** -> **Apple Pay** -> **Certificates** and click **Add certificate** to start the process. ![Apple Pay: Add a new certificate](https://mintcdn.com/wpay/c50rx3S48hHhNNxb/assets/images/apple-pay/new-certificate.png?fit=max\&auto=format\&n=c50rx3S48hHhNNxb\&q=85\&s=5a849b1a989ae12f96855438d3761cb7) Provide a name and download a **Certificate Signing Request** (CSR). Next, visit the [Apple developer dashboard](https://developer.apple.com/account/resources/identifiers/list/merchant) to generate a payment processing certificate. * Create or select a **Merchant ID** to associate a payment processing certificate with. * In the **Apple Pay Payment Processing Certificate** section click **Create Certificate**. * Select **Choose File** to upload the CSR you downloaded from the dashboard, and then Continue. \*Verify the certificate details and **Download** the signed certificate from Apple. Next, go back to the dashboard and upload the signed certificate. ### Add the certificate to your app For the app to accept Apple Pay, set the same Apple **Merchant ID** in the app. In the Xcode project find the **Signing & Capabilities** in the project editor. Select the same **Merchant ID** used to register the payment certificate. Please ensure the provisioning profiles and signing certificates are updated to contain this ID. ## Integrate Apple Pay ### Update SDK initialization Because the Xcode project can be configured with more than one merchant ID, update the initialization of the SDK to pass the `applePayMerchantId` to the constructor. ```swift Swift theme={"system"} import gr4vy_ios let result = Gr4vy.init( gr4vyId: "[GR4VY_ID]", token: "[TOKEN]", amount: 1299, currency: "AUD", country: "AU", buyerId: nil, environment: Gr4vyEnvironment.sandbox, applePayMerchantId: "[MERCHANT_ID]" ) ``` ```swift React Native theme={"system"} import EmbedReactNative from '@gr4vy/embed-react-native'; const handleCheckout = () => { EmbedReactNative.showPaymentSheet({ gr4vyId: '[GR4VY_ID]', environment: 'sandbox', token: '[TOKEN]', amount: 1299, currency: 'AUD', country: 'AU', applePayMerchantId: "[MERCHANT_ID]" ... }) } ``` > [!NOTE] > > The instance ID is the unique identifier for the deployment of the system and is included in every API call. > Together with the environment (sandbox or production) it is used to connect to the right APIs, as well as dashboard. > > The possible values for your ID are one of `wpay`, `wpay1`, `wpay2`, or `wpay3`. You are informed > in which instance your account has been set up. ## Test Apple Pay Apple has an extensive [guide on sandbox testing](https://developer.apple.com/apple-pay/sandbox-testing/). Apple Pay requires a **Sandbox Tester** account to be able to test transactions in the sandbox. Sandbox Testers can add fake test cards to their account that can then be used in Apple Pay. When a Sandbox Tester account is used in production, Apple Pay may fail for various reasons. Similarly, when a regular Apple account is configured on a Mac or iPhone, Apple Pay may fail to load the payment sheet. This may result in the Apple Pay UI quickly appearing and then disappearing. ### Create a sandbox tester account A sandbox tester account can be set up in the [**User and Access section**](https://appstoreconnect.apple.com/access/users/sandbox) of a Apple Developer account. If you do not have an Apple Developer account please contact support and they can set you up with a Sandbox Tester account under an Apple Developer account. To set up an account the following details are needed. * **First and last name**: This can be anything really but using the person's full name is recommended. * **email:** This needs to be a real email address that is not already an Apple account. This needs to be an email that can receive actual emails. * **App Store Country or Region:** This can help with any locale testing. ### Setup on device Once set up, a sandbox tester account can be added to a Mac or iPhone. Sign in to iCloud with the sandbox tester on macOS (or iOS). Sign out of the existing iCloud account and sign in specifically with the sandbox account. This allows you to register the sandbox cards in the Apple Wallet. > [!TIP] > > On a Mac, it helps to create a second user account so that you don't need to > log out of your own iCloud account every time you need to test Apple Pay. > One caveat is that only 1 Apple account per Mac can be used to store cards > in the vault, so you'd not be able to use Apple Pay in your own account. ### Add test cards Next, add a test card from the [Apple Pay docs](https://developer.apple.com/apple-pay/sandbox-testing/). On a Mac this can be done by going to **System Preferences** -> **Wallet & Apple Pay** -> **Add card**. ### Create sandbox transaction To make an Apple Pay sandbox transaction the following are required. * An Apple device with a Sandbox Tester account on it * An app with an Apple merchant ID associated with it * A merchant ID with a signed payment processing certificate associated with it * Apple Pay enabled in the dashboard * The payment processing certificate registered in the dashboard * A card processor enabled that can process Apple Pay in the given currency. For a currency that has a card processor that supports Apple Pay, the payment option should appear in the app. Select Apple Pay and continue with the payment. The Apple Pay sheet should appear. ### Create production transaction To make an Apple Pay transaction in production replace the Sandbox Tester account with a regular Apple account with real cards. ## Common Issues > **Apple Pay option is not shown** > > There could be many reasons why this doesn't show. > > * Make sure the merchant ID is the same as shown in the dashboard > * Make sure the certificate hasn't expired > * Make sure an Apple account is set up on the device and cards are added to it > * Make sure the system is set up to handle Apple Pay > * Check the Apple Pay connection is enabled > * Check a card connector is enabled for the selected country/currency > * Check that a Flow rule doesn't hide Apple Pay > **Apple Pay payment failed** > > There could be a few reasons why this doesn't show but most likely > this means the card was sent to a connector that either doesn't understand > Apple Pay cards, or did not recognize the test card. > **'Apple Pay token could not be decrypted' error** > > The API returns this validation error when it can't find a usable payment processing > certificate for the key that Apple used to encrypt the token. Check the following. > > * The signed certificate from Apple is uploaded under **Connections** -> **Apple Pay** -> **Certificates**, and was created from a CSR downloaded from that same page. > * The certificate hasn't expired. > * The **Merchant ID** set in your app is the one the certificate belongs to. > **Error when adding test card to wallet** > > This seems to happen at times with the test cards provided by Apple. Try a > different card, or try again later. # Apple Pay on iOS without the SDK Source: https://integration.wpay.com.au/guides/features/apple-pay/ios-without-sdk It is possible to use Apple Pay in an iOS app without the SDK. To learn how to integrate Apple Pay, following Apple's [documentation][documentation] is recommended. The steps below highlight the basics and focus on the interaction with the API. A full [sample app][documentation] is available in Apple's developer documentation. ## About this integration Similar to the SDK integration, this setup requires you to perform the following steps. * Enable Apple Pay in your merchant dashboard. * Sign up for an Apple Pay Developer account and [register a payment processing certificate](/guides/features/apple-pay/ios#register-a-certificate). * [Add the merchant ID](/guides/features/apple-pay/ios#add-the-certificate-to-your-app) and its processing certificate to your app. Additionally, you then need to implement the following. * Render your own Apple Pay button. * Create a session with the right merchant ID. * Catch the Apple Pay token and pass it to the API for processing. ## Enable Apple Pay To enable Apple Pay, head over to your dashboard and then go to **Connections** -> **Catalog** -> **Apple Pay**. Next, complete, and submit the form to create a new Apple Pay service. ### Register a certificate To process Apple Pay in a mobile app, register for an Apple Pay developer account and join the [Apple developer program](https://developer.apple.com/programs/). Once set up, generate a new Apple Pay processing certificate using the dashboard. Go to **Connections** -> **Apple Pay** -> **Certificates** and click **Add certificate** to start the process. ![Apple Pay: Add a new certificate](https://mintcdn.com/wpay/c50rx3S48hHhNNxb/assets/images/apple-pay/new-certificate.png?fit=max\&auto=format\&n=c50rx3S48hHhNNxb\&q=85\&s=5a849b1a989ae12f96855438d3761cb7) Provide a name and download a **Certificate Signing Request** (CSR). Next, visit the [Apple developer dashboard](https://developer.apple.com/account/resources/identifiers/list/merchant) to generate a payment processing certificate. * Create or select a **Merchant ID** to associate a payment processing certificate with. * In the **Apple Pay Payment Processing Certificate** section, click **Create Certificate**. * Select **Choose File** to upload the CSR you downloaded from the dashboard, and then Continue. * Verify the certificate details and **Download** the signed certificate from Apple. Next, go back to the dashboard and upload the signed certificate. ### Add the certificate to your app In order for the app to accept Apple Pay, set the same Apple **Merchant ID** in the app. In the Xcode project, find the **Signing & Capabilities** in the project editor. Select the same **Merchant ID** used to register the payment certificate. Please ensure the provisioning profiles and signing certificates are updated to contain this ID. ## Integrate Apple Pay **Swift** ### Display an Apple Pay button An Apple Pay button can be displayed in a few different ways. [Apple's guide](https://developer.apple.com/documentation/passkit/apple_pay/offering_apple_pay_in_your_app#3846467) shows a code sample that checks if Apple Pay is enabled on the device and then adds the button directly to a view with code. ```swift theme={"system"} if let applePayButton = button { let constraints = [ applePayButton.centerXAnchor.constraint(equalTo: applePayView.centerXAnchor), applePayButton.centerYAnchor.constraint(equalTo: applePayView.centerYAnchor) ] applePayButton.translatesAutoresizingMaskIntoConstraints = false applePayView.addSubview(applePayButton) NSLayoutConstraint.activate(constraints) } ``` > [!WARNING] > > The sample app doesn't display the add button if a device can't accept > payments due to hardware limitations, parental controls, or any other reasons. ### Set the merchant ID Once the button is clicked, the guide requires you to set up a new payment request. In this step, set the `merchantIdentifier` to the merchant ID for the Apple Pay certificate you've [registered in the merchant dashboard](/guides/features/apple-pay/ios#enable-apple-pay). For example, if you registered a certificate for merchant ID `merchant.com.example.demo` in the dashboard, then set this same ID in your app code. ```swift theme={"system"} let paymentRequest = PKPaymentRequest() paymentRequest.paymentSummaryItems = paymentSummaryItems paymentRequest.merchantIdentifier = "merchant.com.example.demo" ... ``` > [!TIP] > > Apple's documentation has an [extensive > guide](https://developer.apple.com/documentation/passkit/apple_pay/setting_up_apple_pay) > on setting up your iOS app to accept Apple Pay. ### Create a transaction Finally, once the Apple Pay transaction has been authorized by Apple, create a transaction with the API. > [!NOTE] > > This API call could be made in your frontend code in Swift or Objective C, or > the token could be sent to your backend for processing. ```sh theme={"system"} curl -X POST https://api.example.gr4vy.app/transactions \ -H "Authorization: bearer [JWT]" -d '{ amount: 1299, currency: "AU", currency: "AUD", payment_method: { method: "applepay", token: "[TOKEN]" } }' ``` > [!NOTE] > > In this example, the `[TOKEN]` value is the token from the [PKPayment](https://developer.apple.com/documentation/passkit/pkpayment) object as returned by Apple Pay > in the `paymentAuthorizationController(_:didAuthorizePayment:handler:)` method. **React Native** In React Native you can use any Apple Pay integration, together with your merchant identifier. One of the easiest solutions is to use a library like [React Native Payments](https://github.com/appfolio/react-native-payments) to get an Apple Pay token. On initialization, any of these libraries requires you to set the Apple merchant identifier that you created in the Apple developer dashboard earlier. ```js theme={"system"} const METHOD_DATA = [{ supportedMethods: ['apple-pay'], data: { merchantIdentifier: 'merchant.com.your-app.namespace', supportedNetworks: ['visa', 'mastercard', 'amex'], countryCode: 'AU', currencyCode: 'AUD' } }]; ``` Once the user has approved the payment, your app receives an Apple Pay token from the library. You can pass this library to the server for processing. ```js theme={"system"} paymentRequest.show() .then(paymentResponse => { const { paymentData } = paymentResponse.details; return fetch('https://api.example.gr4vy.app/transactions', { method: 'POST', headers: { "Authorization": "Bearer [TOKEN]" }, body: { amount: 1299, currency: "AU", currency: "AUD", payment_method: { method: "applepay", token: paymentData } } }) .then(res => res.json()) .then(successHandler) .catch(errorHandler) }); ``` ## Common Issues > **Apple Pay option is not shown** > > There could be many reasons why this doesn't show. > > * Make sure the merchant ID is the same as shown in the dashboard > * Make sure the certificate hasn't expired > * Make sure an Apple account is set up on the device and cards are added to it > * Make sure the system is set up to handle Apple Pay > * Check the Apple Pay connection is enabled > * Check a card connector is enabled for the selected country/currency > * Check that a Flow rule doesn't hide Apple Pay > **Apple Pay payment failed** > > There could be a few reasons why this doesn't show but most likely > this means the card was sent to a connector that either doesn't understand > Apple Pay cards, or did not recognize the test card. > **'Apple Pay token could not be decrypted' error** > > The API returns this validation error when it can't find a usable payment processing > certificate for the key that Apple used to encrypt the token. Check the following. > > * The signed certificate from Apple is uploaded under **Connections** -> **Apple Pay** -> **Certificates**, and was created from a CSR downloaded from that same page. > * The certificate hasn't expired. > * The **Merchant ID** set in your app is the one the certificate belongs to. > **Error when adding test card to wallet** > > This seems to happen at times with the test cards provided by Apple. Try a > different card, or try again later. [documentation]: https://developer.apple.com/documentation/passkit/apple_pay/offering_apple_pay_in_your_app # Apple Pay Source: https://integration.wpay.com.au/guides/features/apple-pay/overview ![Apple Pay](https://mintcdn.com/wpay/c50rx3S48hHhNNxb/assets/images/apple-pay/hero.png?fit=max\&auto=format\&n=c50rx3S48hHhNNxb\&q=85\&s=2e627675678f649d417787a08e39c4cc) Apple Pay is supported both on the web and in iOS apps. In both environments, Apple Pay can be integrated using the SDK and without the SDK. ## Web (Embed) On the web, Apple Pay can be easily integrated both with and without Embed. In both offerings, the need to sign up for an Apple Developer account has been removed, reducing additional financial and maintenance costs. | | [Using SDK (Embed)][web] | [Without SDK][web-without] | | - | - | - | | Apple Developer account required? | No | No | | Apple merchant account required? | No | No | | Customization & Theming? | Limited | Full | | Domain association required? | Yes | Yes | | Integrated with Checkout Flow? | Yes | Optional | ## Mobile (iOS) On mobile apps, Apple Pay can be easily integrated both with and without the SDK. In both offerings, signing up for an Apple Developer account and creating an Apple merchant ID and processing certificate is required. | | [Using SDK][mobile] | [Without SDK][mobile-without] | | - | - | - | | Apple Developer account required? | Yes | Yes | | Apple merchant account required? | Yes | Yes | | Customization & Theming? | Limited | Full | | Domain association required? | No | No | | Integrated with Checkout Flow? | Yes | Optional | ## Recurring payments > [!NOTE] > > This is in limited release and is being made generally available soon. Recurring payments are supported on Apple Pay in a similar way to how [recurring payments][recurring-payments] are supported for regular card payments. 1. Take the first payment with the Apple Pay token and `store: true`, with `merchant_initiated` and `is_subsequent_payment` set to `false` and a `payment_source` such as `recurring`. This stores the Apple Pay payment method. 2. Charge later payments with the stored payment method `id`, the same `payment_source`, and `merchant_initiated` and `is_subsequent_payment` set to `true`. A stored Apple Pay payment method has no cryptogram, because the cryptogram only exists for the original payment on the buyer's device. Later payments rely on the recurring flags and the scheme transaction ID from the first payment instead, so payment services may decline a later payment that's sent with the flags of a first payment. [web]: /guides/features/apple-pay/web [web-without]: /guides/features/apple-pay/web-without-sdk [mobile]: /guides/features/apple-pay/ios [mobile-without]: /guides/features/apple-pay/ios-without-sdk [recurring-payments]: /guides/features/recurring-payments # Apple Pay on web with Embed Source: https://integration.wpay.com.au/guides/features/apple-pay/web Apple Pay is the faster, more secure way to pay online using cards saved on your customer's Apple device. Apple Pay is supported out of the box in Embed with minimal configuration. ## About this integration Embed requires minimal configuration to get set up to process Apple Pay. Unlike some other integrations it does not require you to sign up for an Apple Pay Developer account. * Upload the **Domain Association File** to your website. * Enable Apple Pay in your merchant dashboard. ## Enable Apple Pay To enable Apple Pay, head over to your dashboard and then go to **Connections** -> **Catalog** -> **Apple Pay**. Next, complete, and submit the form to create a new Apple Pay service. ### Registering a domain To process Apple Pay via the web, register all the domains where the buyer can checkout. To do this, navigate to the Apple Pay service via the dashboard, **Connections** -> **Apple Pay** -> **Domains** -> **Add domain name**. Register the exact host of the page that shows the Apple Pay button, for example `checkout.example.com`. Apple verifies each host separately, so registering `example.com` doesn't cover `checkout.example.com` or `www.example.com`. Register every host where the button appears, including staging and `www` variants. If the button is inside an iframe, register the host of the top-level page as well. See [Apple Pay inside an iframe](#apple-pay-inside-an-iframe). Gr4vy registers your instance's own domains automatically, so you don't need to add them. The **Add domain name** drawer links to the Domain Association File, so you can download it before you add the domain. > [!WARNING] > > **Domain Association File** > > Before submitting a domain name, please download the Domain Association File from > the dashboard and upload it to every domain you wish to support. > > The file needs to be added to every domain in a predefined location. > > ``` > https://{domain}/.well-known/apple-developer-merchantid-domain-association > ``` > > This file is checked by Apple in the production environment > to verify that permission has been granted to perform > Apple Pay transactions on the dashboard. > > There's no API to fetch the Domain Association File. Download it from the dashboard. The > file can change, so download the current version each time you register a domain rather > than reusing an old copy. > > In production, when you add a domain, the dashboard checks that the file at this location > matches the dashboard's copy exactly and that it isn't served through a redirect. If the > check fails, the domain isn't added. > > Each domain has one file at this location, so a domain can only be verified for one > Apple merchant ID at a time. If you're moving a domain from another Apple Pay setup, > either use a separate subdomain for each setup during the migration, or switch the > file over in one go. ## Test Apple Pay Apple has an extensive [guide on sandbox testing](https://developer.apple.com/apple-pay/sandbox-testing/). Apple Pay requires a **Sandbox Tester** account to be able to test transactions in the sandbox. Sandbox Testers can add fake test cards to their account that can then be used in Apple Pay. When a Sandbox Tester account is used in production, Apple Pay may fail for various reasons. Similarly, when a regular Apple account is configured on a Mac or iPhone, Apple Pay may fail to load the payment sheet. This may result in the Apple Pay UI quickly appearing and then disappearing. ### Create a sandbox tester account A sandbox tester account can be set up in the [**User and Access section**](https://appstoreconnect.apple.com/access/users/sandbox) of a Apple Developer account. If you do not have an Apple Developer account please contact support and they can set you up with a Sandbox Tester account under an Apple Developer account. To set up an account the following details are needed. * **First and last name**: This can be anything really but using the person's full name is recommended. * **email:** This needs to be a real email address that is not already an Apple account. This needs to be an email that can receive actual emails. * **App Store Country or Region:** This can help with any locale testing. ### Setup on device Once set up, a sandbox tester account can be added to a Mac or iPhone. Sign in to iCloud with the sandbox tester on macOS (or iOS). Sign out of the existing iCloud account and sign in specifically with the sandbox account. This allows you to register the sandbox cards in the Apple Wallet. > [!TIP] > > On a Mac, it helps to create a second user account so that you don't need to > log out of your own iCloud account every time you need to test Apple Pay. > One caveat is that only 1 Apple account per Mac can be used to store cards > in the vault, so you'd not be able to use Apple Pay in your own account. ### Add test cards Next, add a test card from the [Apple Pay docs](https://developer.apple.com/apple-pay/sandbox-testing/). On a Mac this can be done by going to **System Preferences** -> **Wallet & Apple Pay** -> **Add card**. ![Apple Pay - Add card](https://mintcdn.com/wpay/c50rx3S48hHhNNxb/assets/images/apple-pay/add-card-mac.png?fit=max\&auto=format\&n=c50rx3S48hHhNNxb\&q=85\&s=e8b0d912c6f8058157b5ee1bd9757ab8) ### Create sandbox transaction To make an Apple Pay sandbox transaction the following are required. * An Apple Pay compatible device with a Sandbox Tester account on it * One or more test cards added to the Sandbox Tester account on that device * Embed loaded on an HTTPS site in an Apple Pay compatible browser * Apple Pay enabled in the dashboard * A card processor enabled that can process Apple Pay in the given currency. For a currency that has a card processor that supports Apple Pay, the payment option should appear in the app. Select Apple Pay and continue with the payment. The Apple Pay sheet should appear. ![Apple Pay - Add card](https://mintcdn.com/wpay/c50rx3S48hHhNNxb/assets/images/apple-pay/web-confirm.png?fit=max\&auto=format\&n=c50rx3S48hHhNNxb\&q=85\&s=4a404777f64f293e145b9186d2c87eed) ### Create production transaction To make an Apple Pay transaction in production replace the Sandbox Tester account with a regular Apple account with real cards. ## Third-party browsers support Users can shop with Apple Pay on devices other than Macs and in third-party browsers. Embed supports this feature by automatically injecting the Apple Pay JS SDK when needed and offering the Apple Pay option in such scenarios if the feature is supported. On submit, a popup displays asking users to scan a code with their iOS 18+ device and complete the payment there. ![Third-party browser popup](https://mintcdn.com/wpay/c50rx3S48hHhNNxb/assets/images/apple-pay/third-party-browser-popup.png?fit=max\&auto=format\&n=c50rx3S48hHhNNxb\&q=85\&s=9b6ee8b3f9eda330efe657a6c7eae9dc) Embed attempts cross-browser Apple Pay under certain conditions only. * If `window.ApplePaySession` is not already defined. This property is typically already defined in Safari browsers on a Mac, or when the Apple Pay JS SDK is already loaded directly outside of Embed. * If the script is loaded successfully. In case of errors (network errors, missing resource, etc), or if the script takes too long to load, Embed won't show the option. Embed logs some information about the Apple Pay JS SDK status when loaded in `debug` mode. For example, when the SDK is loaded correctly: ``` Gr4vy - Apple Pay JS SDK loaded { version: '1.latest' } ``` Loading times can be optimized by adding the script to the head section of the site: ``` ``` > [!WARNING] > > **Content Security Policy** > > Make sure to allow the Apple Pay JS SDK domain in the CSP if using one, otherwise the script won't load and Embed won't show the Apple Pay option in third-party browsers: > > ``` > default-src 'self' applepay.cdn-apple.com ...; script-src 'self' applepay.cdn-apple.com ... > ``` Please see the [Pay with Apple Pay in third-party browsers documentation](https://support.apple.com/en-jo/guide/safari/ibrw9b779bd3/mac) for more information. ## Apple Pay inside an iframe Apple Pay works when your checkout page sits inside an iframe, but it needs extra configuration. Each merchant session is tied to a domain, and that domain has to be the **top level** page, the one shown in the browser's address bar, rather than the frame the payment is requested from. Skip this section if your checkout page is the top level page. > [!WARNING] > > A mismatched domain is not rejected when the merchant session is created. The call to > [`POST /digital-wallets/apple/session`](/reference/digital-wallets/get-apple-pay-session) > succeeds, the Apple Pay sheet opens, the buyer authorizes the payment, and only then does > it fail with a "Payment Not Completed" message. No payment is taken, but nothing looks > wrong until the buyer has already authorized it. ### Allow payments in the iframe Add `allow="payment"` to the iframe that contains your checkout page. ```html theme={"system"} ``` Without it the browser blocks the payment request, so the Apple Pay button either does not appear or the sheet cannot open. This is required on Safari 17 and iOS 17 and later. Permission is delegated one level at a time, so where your checkout page sits more than one iframe deep, **every** iframe in the chain needs the attribute. Missing it on a single ancestor blocks the payment, even when the outer frames carry it. > [!WARNING] > > The permission is bound to the origin it was granted for, and `allow="payment"` grants it > to the origin of the iframe `src`. If the framed page then navigates to a **different** > origin, the permission stops applying, and Apple Pay disappears. A sign-in or SSO redirect > that returns to another domain is the usual way this happens, and it is easy to miss > because the frame still looks the same. > > The browser reports this as `SecurityError: Third-party iframes are not allowed to request > payments unless explicitly allowed via Feature-Policy (payment)`. > > Keep the framed page on one origin for the whole checkout. Where it genuinely has to move > between origins, name each one: > > ```html theme={"system"} > > ``` ### Register the top level domain [Register the domain of the top level page](/guides/features/apple-pay/web#registering-a-domain) for Apple Pay, and upload the domain association file to it, as you would for any other checkout domain. ### Send the top level domain The domain sent when the merchant session is created has to be that same top level domain. This is the step most often missed, and registering the domain without sending it fails in exactly the same way as sending it without registering. When using Embed, set the [`topLevelDomain` option](/guides/payments/embed/options). ```js theme={"system"} gr4vy.setup({ // ... topLevelDomain: "shop.example.com", }); ``` When calling the API directly, send it as `domain_name` rather than deriving it from `document.location`, which resolves to the frame's own domain. ```js theme={"system"} body: JSON.stringify({ validation_url: event.validationURL, domain_name: "shop.example.com", }), ``` > [!NOTE] > > Every domain in the chain must be served over HTTPS with a valid certificate. ## Common issues > **Apple Pay option is not shown** > > There could be many reasons why this doesn't show. > > * Make sure you are using an [Apple Pay compatible browser and device](https://support.apple.com/en-gb/102896) > * Make sure an Apple account is set up on the device and cards are added to it > * Make sure the system is set up to handle Apple Pay > * Check the Apple Pay connection is enabled > * Check the Apple Pay connection is set up to handle transactions on the domain you're testing on > * Check a card connector is enabled for the selected country/currency > * Check that a Flow rule doesn't hide Apple Pay > * When you use Embed, check that the browser passes Embed's own eligibility check. Embed only shows > Apple Pay when `window.ApplePaySession` exists, `ApplePaySession.canMakePayments()` returns `true`, > and `ApplePaySession.supportsVersion(3)` returns `true`. Headless browsers and browser automation > tools usually fail this check, even when Apple Pay appears in the payment options returned by the API. > **Domain verification fails** > > If a domain isn't added, check the Domain Association File on your server. > > * Serve the file over HTTPS at `/.well-known/apple-developer-merchantid-domain-association`, publicly reachable and without a redirect. > * Upload the file exactly as downloaded from the dashboard. Don't edit it, re-encode it, or add a trailing newline. > * Download the current file from the dashboard for each domain rather than reusing an old copy. > > See Apple's [TN3173: Troubleshooting issues with your Apple Pay merchant ID configuration](https://developer.apple.com/documentation/technotes/tn3173-troubleshooting-issues-with-your-apple-pay-merchant-id-configuration) for more checks. > **Apple Pay payment failed** > > There could be a few reasons why this doesn't show but most likely > this means the card was sent to a connector that either doesn't understand > Apple Pay cards, or did not recognize the test card. > **Error when adding test card to wallet** > > This seems to happen at times with the test cards provided by Apple. Try a > different card, or try again later. > **Apple Pay sheet opens and immediately closes** > > When using a real card in a sandbox environment, Apple Pay fails to initialize. This results > in the Apple Pay UI quickly appearing and then disappearing. > > The reason for this happening is that the validation URL generated by Apple Pay is rejected by > the [`POST /digital-wallets/apple/session`](/reference/digital-wallets/get-apple-pay-session) endpoint. Please only use [test cards](./testing) in the sandbox environment. > **'Payment Not Completed' error message** > > If the Apple Pay dialog shows but closes automatically with a "Payment Not Completed" > message, check whether your checkout page is inside an iframe. The merchant session is > tied to a domain, and that domain has to be the top level page, so a nested checkout page > needs the top level domain registered and sent when the session is created. > > See [Apple Pay inside an iframe](/guides/features/apple-pay/web#apple-pay-inside-an-iframe) for the configuration this > requires. # Apple Pay on web without Embed Source: https://integration.wpay.com.au/guides/features/apple-pay/web-without-sdk It is possible to use Apple Pay on the web without the SDK. To learn how to integrate Apple Pay, following Apple's [documentation][documentation] is recommended. The steps below highlight the basics and focus on the interaction with the API. A full [code sample is available](https://github.com/gr4vy/sample-standalone-apple-pay) that shows this integration in action. ## About this integration Similar to the standard Embed integration, there is minimal configuration to get set up to process Apple Pay on the web. There is no need to sign up for an Apple Developer account with this integration. * Upload the **Domain Association File** to your website. * Enable Apple Pay in your merchant dashboard. Additionally, you then need to implement the following. * Load the Apple Pay JS SDK. * Render an Apple Pay button. * Handle the click of the button. * Verify the Apple Pay session. * Catch the Apple Pay token and pass it to the API for processing. ## Enable Apple Pay To enable Apple Pay, head over to your dashboard and then go to **Connections** -> **Catalog** -> **Apple Pay**. Next, complete, and submit the form to create a new Apple Pay service. ### Registering a domain To process Apple Pay via the web, register all the domains where the buyer can checkout. To do this, navigate to the Apple Pay service via the dashboard, **Connections** -> **Apple Pay** -> **Domains** -> **Add domain name**. Register the exact host of the page that shows the Apple Pay button, for example `checkout.example.com`. Apple verifies each host separately, so registering `example.com` doesn't cover `checkout.example.com` or `www.example.com`. Register every host where the button appears, including staging and `www` variants. If the button is inside an iframe, register the host of the top-level page as well. See [Apple Pay inside an iframe](#apple-pay-inside-an-iframe). Gr4vy registers your instance's own domains automatically, so you don't need to add them. The **Add domain name** drawer links to the Domain Association File, so you can download it before you add the domain. > [!WARNING] > > **Domain Association File** > > Before submitting a domain name, please download the Domain Association File from > the dashboard and upload it to every domain you wish to support. > > The file needs to be added to every domain in a predefined location. > > ``` > https://{domain}/.well-known/apple-developer-merchantid-domain-association > ``` > > This file is checked by Apple in the production environment > to verify that permission has been granted to perform > Apple Pay transactions on your dashboard. > > There's no API to fetch the Domain Association File. Download it from the dashboard. The > file can change, so download the current version each time you register a domain rather > than reusing an old copy. > > In production, when you add a domain, the dashboard checks that the file at this location > matches the dashboard's copy exactly and that it isn't served through a redirect. If the > check fails, the domain isn't added. > > Each domain has one file at this location, so a domain can only be verified for one > Apple merchant ID at a time. If you're moving a domain from another Apple Pay setup, > either use a separate subdomain for each setup during the migration, or switch the > file over in one go. ## Integrate Apple Pay ### Load the Apple Pay JS SDK Since iOS 18, users can shop with Apple Pay on devices other than Macs and non-Safari browsers. On submit, a popup displays asking users to scan a code with their iOS 18+ device and complete the payment there. ![Third-party browser popup](https://mintcdn.com/wpay/c50rx3S48hHhNNxb/assets/images/apple-pay/third-party-browser-popup.png?fit=max\&auto=format\&n=c50rx3S48hHhNNxb\&q=85\&s=9b6ee8b3f9eda330efe657a6c7eae9dc) To enable Apple Pay in browsers besides Safari, load the Apple Pay JS SDK into the app. It is safe to load this same library in Safari as well. ```html theme={"system"} ``` #### Content Security Policy Make sure to allow the Apple Pay JS SDK domain in your CSP if you're using one; otherwise, the script does not load and the Apple Pay option does not show in third-party browsers. ``` default-src 'self' applepay.cdn-apple.com ...; script-src 'self' applepay.cdn-apple.com ... ``` Please see the [Pay with Apple Pay in third-party browsers documentation](https://support.apple.com/en-jo/guide/safari/ibrw9b779bd3/mac) for more information. ### Render an Apple Pay button An Apple Pay button can be displayed and customized [with Apple specific CSS](https://developer.apple.com/documentation/apple_pay_on_the_web/displaying_apple_pay_buttons_using_css). ```html theme={"system"} ``` ### Handle button click When a button is clicked, start an [Apple Pay Session](https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/creating_an_apple_pay_session). ```js theme={"system"} const onClick = async () => { const session = new ApplePaySession(3, { countryCode: 'AU', currencyCode: 'AUD', merchantCapabilities: ["supports3DS"], supportedNetworks: ["visa", "masterCard", "amex", "discover"], total: { label: "Demo (Card is not charged)", type: "final", amount: '12.99', }, }); ... session.begin(); }; ``` ### Verify session Next, this is the bit that's specific to the API. Make an API call to the Apple Pay session endpoint to verify the Apple Pay session for your domain. This checks if accepting Apple Pay on your domain is allowed. > [!NOTE] > > For this to work it's important to [register the domain](/guides/features/apple-pay/web#enable-apple-pay) > your domain for use with Apple Pay in the dashboard. ```js theme={"system"} ... session.onvalidatemerchant = async (event) => { const response = await fetch(`http://api.sandbox.example.gr4vy.app/digital-wallets/apple/session`, { method: "POST", headers: { "Authorization": `bearer [JWT]`, "Content-Type": "application/json", }, body: JSON.stringify({ validation_url: event.validationURL, domain_name: document.location.hostname, }), }); const merchantSession = await response.json(); session.completeMerchantValidation(merchantSession); }; session.begin(); ``` > [!WARNING] > > When using a real card in a sandbox environment, the validation URL is rejected by the > [`POST /digital-wallets/apple/session`](/reference/digital-wallets/get-apple-pay-session) endpoint. Please only use test cards in the sandbox environment. If the session endpoint returns a `400` error with the message `Invalid request creating Apple Pay session`, Apple rejected the session request. Check that you send the `validation_url` that Apple provides in the `onvalidatemerchant` event, and that the `domain_name` you send is registered for Apple Pay in the dashboard. Always complete merchant validation with the session that this endpoint returns. If the payment session is validated with another merchant identity, for example a payment service's own Apple Pay setup, Gr4vy can't decrypt the token, and creating the transaction fails with the error `Apple Pay token could not be decrypted`. ### Create a transaction Finally, once the Apple Pay transaction has been authorized by Apple, create a transaction with the API. > [!NOTE] > > This API call could be made in your frontend code, or the token could be sent to your backend for processing. ```js theme={"system"} session.onpaymentauthorized = (event) => { await fetch(`http://api.sandbox.example.gr4vy.app/transactions`, { method: "POST", headers: { "Authorization": `bearer [JWT]`, "Content-Type": "application/json", }, body: JSON.stringify({ amount: 1299, currency: "AUD", country: "AU", payment_method: { method: "applepay", token: event.payment.token, }, }), }); session.completePayment({ status: ApplePaySession.STATUS_SUCCESS, }); }; session.begin(); ``` ## Apple Pay inside an iframe Apple Pay works when your checkout page sits inside an iframe, but it needs extra configuration. Each merchant session is tied to a domain, and that domain has to be the **top level** page, the one shown in the browser's address bar, rather than the frame the payment is requested from. Skip this section if your checkout page is the top level page. > [!WARNING] > > A mismatched domain is not rejected when the merchant session is created. The call to > [`POST /digital-wallets/apple/session`](/reference/digital-wallets/get-apple-pay-session) > succeeds, the Apple Pay sheet opens, the buyer authorizes the payment, and only then does > it fail with a "Payment Not Completed" message. No payment is taken, but nothing looks > wrong until the buyer has already authorized it. ### Allow payments in the iframe Add `allow="payment"` to the iframe that contains your checkout page. ```html theme={"system"} ``` Without it the browser blocks the payment request, so the Apple Pay button either does not appear or the sheet cannot open. This is required on Safari 17 and iOS 17 and later. Permission is delegated one level at a time, so where your checkout page sits more than one iframe deep, **every** iframe in the chain needs the attribute. Missing it on a single ancestor blocks the payment, even when the outer frames carry it. > [!WARNING] > > The permission is bound to the origin it was granted for, and `allow="payment"` grants it > to the origin of the iframe `src`. If the framed page then navigates to a **different** > origin, the permission stops applying, and Apple Pay disappears. A sign-in or SSO redirect > that returns to another domain is the usual way this happens, and it is easy to miss > because the frame still looks the same. > > The browser reports this as `SecurityError: Third-party iframes are not allowed to request > payments unless explicitly allowed via Feature-Policy (payment)`. > > Keep the framed page on one origin for the whole checkout. Where it genuinely has to move > between origins, name each one: > > ```html theme={"system"} > > ``` ### Register the top level domain [Register the domain of the top level page](/guides/features/apple-pay/web#registering-a-domain) for Apple Pay, and upload the domain association file to it, as you would for any other checkout domain. ### Send the top level domain The domain sent when the merchant session is created has to be that same top level domain. This is the step most often missed, and registering the domain without sending it fails in exactly the same way as sending it without registering. When using Embed, set the [`topLevelDomain` option](/guides/payments/embed/options). ```js theme={"system"} gr4vy.setup({ // ... topLevelDomain: "shop.example.com", }); ``` When calling the API directly, send it as `domain_name` rather than deriving it from `document.location`, which resolves to the frame's own domain. ```js theme={"system"} body: JSON.stringify({ validation_url: event.validationURL, domain_name: "shop.example.com", }), ``` > [!NOTE] > > Every domain in the chain must be served over HTTPS with a valid certificate. ## Common issues > **Apple Pay option is not shown** > > There could be many reasons why this doesn't show. > > * Make sure you are using an [Apple Pay compatible browser and device](https://support.apple.com/en-gb/102896) > * Make sure an Apple account is set up on the device and cards are added to it > * Make sure the system is set up to handle Apple Pay > * Check the Apple Pay connection is enabled > * Check the Apple Pay connection is set up to handle transactions on the domain you're testing on > * Check a card connector is enabled for the selected country/currency > * Check that a Flow rule doesn't hide Apple Pay > * When you use Embed, check that the browser passes Embed's own eligibility check. Embed only shows > Apple Pay when `window.ApplePaySession` exists, `ApplePaySession.canMakePayments()` returns `true`, > and `ApplePaySession.supportsVersion(3)` returns `true`. Headless browsers and browser automation > tools usually fail this check, even when Apple Pay appears in the payment options returned by the API. > **Domain verification fails** > > If a domain isn't added, check the Domain Association File on your server. > > * Serve the file over HTTPS at `/.well-known/apple-developer-merchantid-domain-association`, publicly reachable and without a redirect. > * Upload the file exactly as downloaded from the dashboard. Don't edit it, re-encode it, or add a trailing newline. > * Download the current file from the dashboard for each domain rather than reusing an old copy. > > See Apple's [TN3173: Troubleshooting issues with your Apple Pay merchant ID configuration](https://developer.apple.com/documentation/technotes/tn3173-troubleshooting-issues-with-your-apple-pay-merchant-id-configuration) for more checks. > **Apple Pay payment failed** > > There could be a few reasons why this doesn't show but most likely > this means the card was sent to a connector that either doesn't understand > Apple Pay cards, or did not recognize the test card. > **Error when adding test card to wallet** > > This seems to happen at times with the test cards provided by Apple. Try a > different card, or try again later. > **Apple Pay sheet opens and immediately closes** > > When using a real card in a sandbox environment, Apple Pay fails to initialize. This results > in the Apple Pay UI quickly appearing and then disappearing. > > The reason for this happening is that the validation URL generated by Apple Pay is rejected by > the [`POST /digital-wallets/apple/session`](/reference/digital-wallets/get-apple-pay-session) endpoint. Please only use [test cards](./testing) in the sandbox environment. > **'Payment Not Completed' error message** > > If the Apple Pay dialog shows but closes automatically with a "Payment Not Completed" > message, check whether your checkout page is inside an iframe. The merchant session is > tied to a domain, and that domain has to be the top level page, so a nested checkout page > needs the top level domain registered and sent when the session is created. > > See [Apple Pay inside an iframe](/guides/features/apple-pay/web#apple-pay-inside-an-iframe) for the configuration this > requires. [documentation]: https://developer.apple.com/documentation/apple_pay_on_the_web/ # Card fingerprints Source: https://integration.wpay.com.au/guides/features/cards/fingerprints A unique hash for every card in the vault A card fingerprint can be used to seamlessly identify card numbers stored in your vault without the burden of PCI compliance. Key features of card fingerprints. * Secure and irreversible fingerprint generation for every card received by the system * Inclusion of fingerprint values in API responses wherever payment methods are returned, ensuring seamless integration with your system ## Use-cases There are a few use cases for card fingerprints. 1. Identifying fraud: Because the API does not return the original raw card number back via the API, it's hard to know if a card has been used previously in a fraudulent transaction. For example, two transactions with the same masked card number of `4111 11** **** 1111` could be the same number or two different numbers. 2. Identifying duplicate cards: Similarly, you could use the card fingerprint to determine if the same card is stored twice for a user without needing to see the original number. ## How fingerprints are generated Gr4vy generates a fingerprint as a keyed hash of the card number, using a secret key that Gr4vy holds. Because the key is never shared, you can't compute a fingerprint yourself, and you can't compare it with a fingerprint or hash from a payment service or another system. Compare fingerprints only with other fingerprints that Gr4vy returns. A card fingerprint identifies a card number. It isn't the device fingerprint that's sent to anti-fraud services. For that, see [anti-fraud device fingerprinting](/guides/features/anti-fraud/fingerprint). ## Availability & limitations Every card `payment_method` returned by the API has a `fingerprint` field. The value is set once Gr4vy has generated the fingerprint, as described below. Fingerprints are the same for every unique card number used. Because of this, payments made with Apple Pay, Click to Pay, and some Google Pay have a different fingerprint than cards filled in by hand by your customers. A fingerprint is a 64-character string of lowercase hexadecimal characters (`0-9` and `a-f`). If you store fingerprints in your own system, a field of 64 characters is enough. Gr4vy usually generates the fingerprint while it processes the request. If it can't do this in time, it generates the fingerprint in the background, and the `fingerprint` is `null` in the response. In that case, get the transaction or payment method again later to read the fingerprint. ## Comparison to PAR [PAR](./payment-account-reference.mdx) and fingerprints go hand in hand to detect duplicate and fraudulent cards. * PAR has limited availability and depends on the data received back from the payment service, while card fingerprints are available for every card transaction * A PAR represents the underlying account, while a fingerprint represents the actual card number. As a result, the fingerprint is different when using a digital wallet like Apple Pay or Google Pay. # Payment account references Source: https://integration.wpay.com.au/guides/features/cards/payment-account-reference A unique ID for the account behind the card The Payment Account Reference (PAR) is a unique identifier assigned to the underlying account for which a card is used. This PAR is generally the same for different cards tied to the same bank account. For example, the number on a physical card, a virtual card for the same account, a network token, and an Apple or Google Pay token would all have the same PAR. This also applies to updated cards that have changed the card number due to the card expiring or being lost. ## Use-cases There are a few use cases for PARs. 1. Identifying fraud: Because the API does not return the original raw card number back via the API, it's hard to know if a card has been used previously in a fraudulent transaction. For example, two transactions with the same masked card number of `4111 11** **** 1111` could be the same number or two different numbers. 2. Identifying duplicate cards: Similarly, you could use the PAR to determine if the same card is stored twice for a user without needing to see the original number. ## Availability The availability of a PAR depends on a few factors. Firstly, the payment service needs to return the PAR to the system. Secondly, the connector needs to be implemented to pick up the PAR and return it back. When available, the `payment_account_reference` field is returned as part of any `payment_method` on a [transaction](/reference/transactions/list-transactions), Please reach out to support if the preferred connector does not yet have PAR enabled. ## Comparison to fingerprints [Fingerprints](./fingerprints.mdx) and PARs go hand in hand to detect duplicate and fraudulent cards. * PAR has limited availability and depends on the data received back from the payment service, while card fingerprints are available for every card transaction * PARs represent the underlying account, while a fingerprint represents the actual card number. As a result, the fingerprint is different when using a digital wallet like Apple Pay or Google Pay. # Customer Authentication Module (CAM) Source: https://integration.wpay.com.au/guides/features/click-to-pay/cam Customer Authentication Module (CAM) is a feature provided by Mastercard's Click to Pay that adds an extra layer of security to transactions. It verifies the identity of the customer by analyzing device and behavioral data during the checkout process, reducing the risk of fraud. When CAM is enabled, it silently collects device and behavioral data in the background as the customer interacts with the checkout page. This data is used to create a risk profile for the transaction. Based on this profile, CAM determines whether the transaction is low-risk and can proceed without interruption, or if it requires additional verification. If a higher risk is detected, CAM may prompt the customer for a one-time password (OTP) or another form of step-up authentication to confirm their identity before the payment is completed. This process is designed to be as seamless as possible, only introducing friction when necessary. Passkey authentication is part of the [Token Authentication Service (TAS)](/guides/features/click-to-pay/tas#passkeys) and can't be turned off separately. ## Benefits * **Enhanced Security**: Adds a robust layer of authentication to prevent fraudulent transactions. * **Reduced Friction**: Most legitimate transactions are processed without any extra steps for the customer. * **Increased Trust**: Provides customers with greater confidence that their payments are secure. ## Implementation To enable Customer Authentication Module when using Secure Fields for Click to Pay, add the `authenticate.consumer` property to your Secure Fields configuration and set it to `true`. When enabled, CAM automatically collects and analyzes data to authenticate customers during checkout, providing enhanced security without any additional integration effort. ```jsx React {9} theme={"system"} ``` ```js Node {11} theme={"system"} const clickToPay = secureFields.addClickToPay('#click-to-pay', { srcDpaId: '{SRC_DPA_ID}', dpaName: '{DPA_NAME}', dpaLocale: 'en_AU', cardBrands: ['mastercard', 'visa', 'american-express'], consentCheckbox: "#click-to-pay-consent-checkbox", learnMoreLink: "#click-to-pay-learn-more-link", cardForm: "#cc-form", signIn: "#click-to-pay-sign-in" authenticate: { consumer: true, } }) ``` ## Testing in sandbox Use the following guidance to test passkeys with [TAS](/guides/features/click-to-pay/tas) and [CAM](/guides/features/click-to-pay/cam) in the `sandbox` environment. ### Use the right test cards To test Mastercard passkeys and TAS, use the cards listed under **Tokenization Eligible Test Cards For Token Authentication Framework** in the [Mastercard test cards](https://developer.mastercard.com/unified-checkout-solutions/documentation/testing/test_cases/click_to_pay_case/#mastercard-test-cards). Other Mastercard Click to Pay test cards fall back to one-time password (OTP) authentication. Mastercard maintains this list, so check their page for the current card numbers. ### Reproduce the passkey step With CAM enabled, Click to Pay tries to authenticate the customer in the following order: 1. A Merchant Recognized Token (MRT) cookie from a previous session. 2. A passkey. 3. An OTP. The passkey step only appears when no valid MRT exists. To reproduce it, clear your cookies or use a fresh browser profile or private window. > [!NOTE] > > Mastercard decides whether to offer a passkey. Availability also depends on the card, the issuer, and the device. Don't assume that the passkey step always appears, or that it never does. ### Don't block the passkey popup Passkey authentication runs in a Mastercard popup window, not inside the Click to Pay iframe. The customer completes the biometric prompt in that popup. > [!WARNING] > > Don't cover the page with overlays, loading spinners, or other blocking elements while the Click to Pay flow is active. Blocking the page makes the popup and the biometric prompt unusable. ### Control the 3DS outcome TAS requires [merchant account-level 3DS](/guides/features/3ds/setup#add-a-merchant-account-configuration). The 3DS sandbox triggers outcomes based on the last four digits of the card number. The Mastercard Click to Pay test cards don't match these triggers, so 3DS returns an `Unknown test` result. To control the 3DS outcome for these cards, register a [3DS scenario](/guides/features/3ds/scenarios) for the test card number. ## Common issues > **3DS overlay pops up but automatically closes** > > This occurs when the 3DS acquirer configuration does not match the required values. Check the 3DS acquirer configuration and verify that the acquirer details for each card scheme are configured exactly as per the [Mastercard Unified Checkout Solutions SDK reference](https://developer.mastercard.com/unified-checkout-solutions/documentation/sdk-reference/parameters/#acquirer-data), listed below. > > **Mastercard** > > | Parameter | Value | > | - | - | > | Acquirer BIN | `545301` | > | Acquirer Merchant ID | `SRC3DS` | > | MCC | `0001` | > > **Visa** > > | Parameter | Value | > | - | - | > | Acquirer BIN | `432104` | > | Acquirer Merchant ID | `33334444` | > | MCC | `0001` | > **No 3DS prompt when you expect a challenge flow** > > On supported devices, authentication can complete with a passkey and no 3DS prompt appears. If you expect a challenge-based 3DS flow and no prompt appears, work through the following checks. > > **1. Check which features are enabled** > > In the browser developer tools, open the **Network** tab and find the request to `click-to-pay.html`. > Copy the **Request URL** and URL-decode it twice. The decoded payload should include an `authenticate` block reflecting the features you have enabled: > > ```json theme={"system"} > "authenticate": { > "checkout": true, > "consumer": true > } > ``` > > `authenticate.checkout` controls [TAS](/guides/features/click-to-pay/tas) and `authenticate.consumer` controls [CAM](/guides/features/click-to-pay/cam). If either value is missing or set to `false`, the feature is not enabled. > > **2. Check the `dynamicDataType` field** > > In the same decoded payload, ensure that the `dynamicDataType` field is set to `CARD_APPLICATION_CRYPTOGRAM_SHORT_FORM`. > Secure Fields uses this value by default, unless you set the `dynamicDataType` option to `NONE`. > This configuration requests a dynamic network token, which [TAS](/guides/features/click-to-pay/tas) requires. > > **3. Verify `acquirer_data` is present in the acquirer response** > > In the **Network** tab, locate the acquirer request. > The response must have values for `acquirer_data`. If there is no acquirer data, it is likely due to the checkout session initiation. > Ensure the checkout session setup was created with the correct data fields. # Click to Pay Source: https://integration.wpay.com.au/guides/features/click-to-pay/overview Click to Pay is a digital payment solution that provides a streamlined and secure guest checkout experience for online shopping. Backed by major card networks including American Express, Discover, Mastercard, and Visa, it allows customers to make purchases without creating an account or manually entering their card details for every transaction. ## How it works Click to Pay simplifies the checkout process by maintaining a secure payment profile for customers across different merchants. The typical checkout flow is: 1. The customer enters their email address at checkout. 2. Click to Pay recognizes the customer and prompts them to authenticate. 3. Once authenticated, the customer can select from their saved cards. 4. The payment is completed using a secure network token, without exposing sensitive card information. ## Benefits of Click to Pay Integrating Click to Pay offers several advantages: * **Increased Conversion**: A faster and more convenient checkout process leads to higher conversion rates and reduced cart abandonment. * **Enhanced Security**: Payments are processed using network tokens, reducing the risk of fraud and data breaches. * **Improved Customer Experience**: Customers no longer need to remember passwords or manually enter card details for each purchase. * **Cross-Merchant Recognition**: Once enrolled, customers can use Click to Pay across any participating merchant. ## Advanced security features Click to Pay supports additional security features that can be enabled to further enhance transaction security and improve the customer experience: ### Token Authentication Service Token Authentication Service generates a cryptogram that authenticates payment tokens during checkout, providing EMV-level security for online transactions. TAS can replace traditional 3-D Secure flows, eliminating redirect steps while maintaining strong authentication. On supported devices, TAS leverages passkeys for biometric authentication. [Learn more about TAS →](./tas) ### Customer Authentication Module Customer Authentication Module analyzes device and behavioral data to create a risk profile for each transaction. It silently authenticates most legitimate transactions without friction, only prompting for step-up authentication (like OTP) when higher risk is detected. [Learn more about CAM →](./cam) ## Click to Pay with Secure Fields The platform makes it easy to add Click to Pay to your checkout flow through Secure Fields. When a customer's email is recognized as being enrolled in Click to Pay, the option is automatically presented to them, providing a seamless and secure payment experience. For detailed implementation instructions, read the [setup guide](./setup). For a practical implementation guide, check out the [Click to Pay sample repository](https://github.com/gr4vy/sample-standalone-click-to-pay). # Click to Pay Setup Source: https://integration.wpay.com.au/guides/features/click-to-pay/setup ## Merchant setup To process Click to Pay payments, register as a Digital Payments App (DPA). Set up Click to Pay in the Connections dashboard which automatically registers and configures the DPA with Mastercard. ## Prepare the server Before loading Click to Pay with Secure Fields into the checkout, generate a checkout session via the API. This is made easy with the server-side SDKs. ### Install a server-side SDK Use the package manager in the preferred programming language to install the server-side SDK. Generating a checkout session can only be done server side and doing this client side is not recommended as it exposes the API key to customers. ```sh C# theme={"system"} dotnet add package Gr4vy ``` ```sh Go theme={"system"} go get github.com/gr4vy/gr4vy-go ``` ```sh Java theme={"system"} # Please check for the latest version implementation 'com.gr4vy:sdk:1.0.0' ``` ```sh PHP theme={"system"} composer require "gr4vy/gr4vy-php" ``` ```sh Python theme={"system"} pip install gr4vy ``` ```sh TypeScript theme={"system"} npm install @gr4vy/sdk # or: yarn add @gr4vy/sdk ``` > [!WARNING] > > Please install the [latest release](/guides/get-started#server-side-sdks) of the preferred SDK. ### Initialize the SDK client Next, initialize the SDK with the ID of the instance and the private key. ```csharp C# theme={"system"} using Gr4vy; using Gr4vy.Models.Components; using System.Collections.Generic; // Loaded the key from a file, env variable, // or anywhere else var privateKey = "..."; var sdk = new Gr4vySDK( id: "example", server: SDKConfig.Server.Sandbox, bearerAuthSource: Auth.WithToken(privateKey), merchantAccountId: "default" ); ``` ```go Go theme={"system"} package main import ( "context" gr4vy "github.com/gr4vy/gr4vy-go" "github.com/gr4vy/gr4vy-go/models/operations" "log" "os" ) func main() { ctx := context.Background() privateKey := "...." // Private key loaded from disk or env var withToken := gr4vy.WithToken(privateKey, []JWTScope{ReadAll, WriteAll}, 60) s := gr4vy.New( gr4vy.WithID("example"), gr4vy.WithServer(gr4vy.ServerSandbox), gr4vy.WithSecuritySource(withToken), gr4vy.WithMerchantAccountID("default"), ) } ``` ```java Java theme={"system"} package hello.world; import com.gr4vy.sdk.BearerSecuritySource; import com.gr4vy.sdk.Gr4vy; import com.gr4vy.sdk.Gr4vy.AvailableServers; import com.gr4vy.sdk.models.components.AccountUpdaterJobCreate; import com.gr4vy.sdk.models.errors.*; import com.gr4vy.sdk.models.operations.ListTransactionsRequest; import java.lang.Exception; import java.util.List; public class Application { public static void main(String[] args) throws Exception { String privateKey = "-----BEGIN PRIVATE KEY-----\n...."; // a valid private key Gr4vy sdk = Gr4vy.builder() .id("example") .server(AvailableServers.SANDBOX) .merchantAccountId("default") .securitySource(new BearerSecuritySource.Builder(privateKey).build()) .build(); } } ``` ```php PHP theme={"system"} declare(strict_types=1); require 'vendor/autoload.php'; use Gr4vy; use Gr4vy\Auth; // Loaded the key from a file, env variable, // or anywhere else $privateKey = "..."; $sdk = Gr4vy\SDK::builder() ->setId('example') ->setServer('sandbox') ->setSecuritySource(Auth::withToken($privateKey)) ->setMerchantAccountId('default') ->build(); ``` ```python Python theme={"system"} from gr4vy import Gr4vy, auth import os client = Gr4vy( id="example", server="production", merchant_account_id="default", bearer_auth=auth.with_token(open("./private_key.pem").read()) ) ``` ```ts TypeScript theme={"system"} import fs from "fs"; import { Gr4vy, withToken } from "@gr4vy/sdk"; const gr4vy = new Gr4vy({ server: "sandbox", id: "example", bearerAuth: withToken({ privateKey: fs.readFileSync("private_key.pem", "utf8"), }), }); ``` > [!WARNING] > > This SDK needs to be initialized on your server-side. Please do not expose > your private key to your client-side code. This assumes the key you created in the previous step is kept in a secure location and can be loaded as a string. You could store this key in an environment variable or a secure vault. > [!NOTE] > > To learn more about the API authentication, and how to generate a bearer token > for use without the SDKs, please see the more extensive [authentication > guide](/guides/api/authentication). ### Create a Checkout Session With the SDK installed, use it to create a checkout session for use with Click to Pay. ```js Node theme={"system"} const response = await client.newCheckoutSession(); ``` ```py Python theme={"system"} response = client.create_new_checkout_session() ``` ```php PHP theme={"system"} $checkout_session = $client->newCheckoutSession() ``` ```java Java theme={"system"} CheckoutSession session = client.newCheckoutSession() ``` ```shell cURL theme={"system"} curl -i -X POST "https://api.{instance_id}.gr4vy.app/checkout-sessions" \ -H "Authorization: Bearer [JWT_TOKEN]" \ -H "Content-Type: application/json" ``` > [!NOTE] > > Please refer to the API reference to [learn more about our Checkout > Sessions](/reference/checkout-sessions/new-checkout-session) and the > additional options that can be passed when creating them. ## Create your checkout With your checkout session ready, it's time to start creating a card form that can support Click to Pay. The frontend integration is called Secure Fields and it essentially wraps each of your credit card input fields in a secure element, keeping you PCI-compliant and allowing for seamless integration with Click to Pay. > [!NOTE] > > For more information on Secure Fields, please see the [detailed > guide](/guides/payments/secure-fields/) which covers all the available > options, events, and theming customizations available. ### Install Secure Fields There are three ways to install **Secure Fields** for use with Click to Pay: either as a React library, a Node library, or straight off the CDN. **React** ```bash theme={"system"} npm install @gr4vy/secure-fields-react --save # or: yarn add @gr4vy/secure-fields-react ``` ### Initializing Secure Fields With **Secure Fields** installed it's now possible to initialize the connection to your instance. It expects the checkout session ID you created earlier. ```jsx theme={"system"} import { SecureFields } from "@gr4vy/secure-fields-react";
``` > [!WARNING] > > When `SecureFields` is loaded without a valid `sessionId` it will error. We recommend only loading > the component if the session ID is set. > > ```jsx theme={"system"} > {sessionId && ( > > ... > > )} > ``` ### Create a card form Next, add any of the 3 individual card fields to the form. Mix and match these with custom forms, and add custom labels. Let's start by adding the form with 4 fields: the card holder's name, the card number, card expiration date, and the security code for the card. ```jsx theme={"system"} import { CardForm, CardNumber, ExpiryDate, SecurityCode, useSecureFields } from '@gr4vy/secure-fields-react' const secureFields = useSecureFields() {/* This card form will automatically hide */} { console.log('Card form visibility changed. Visible', visible) }} >
{/* You can add your own fields as well as secure fields */} {/* These are fields that will become secure fields */}
``` Each secure field should now be loaded on the page. Please see our more extensive guides on how to add [event listeners](/guides/payments/secure-fields/events) and custom [styles](/guides/payments/secure-fields/theming) to **Secure Fields**. ### Add Click to Pay Next, add the Click to Pay components to show and hide their UI on demand. ```jsx theme={"system"} import { ClickToPay, ClickToPaySignIn } from '@gr4vy/secure-fields-react' {/* The main Click to Pay component */} {/* An optional sign-in with email or phone for Click to Pay would go here */} {/* This shows how to sign in with email. For the mobile number use the `mobileNumber` with a `countryCode` and `number` */} {/* ... */} {/* ... */} ``` > [!NOTE] > > The `srcDpaId` and `dpaName` options are optional. When you leave either of them out, Secure Fields fetches > both values for the checkout session. Use the `CLICK_TO_PAY_INITIALIZED` event to know when Click to Pay has > loaded. > [!NOTE] > > Secure Fields will automatically display and hide the Click to Pay Component, the sign in form, and your card form, based on the > status of Click to Pay. ### Handle form submission Once the data has been submitted, use the `onCardVaultSuccess` property on the `SecureFields` component to create a transaction. ```tsx theme={"system"} { // call your API }} >... ``` > [!NOTE] > > For more information on Secure Fields, please see our [detailed > guide](/guides/payments/secure-fields/) which will cover all the available > options, events, and theming customizations available. **Node/CDN** ```bash Node theme={"system"} npm install @gr4vy/secure-fields --save # or: yarn add @gr4vy/secure-fields ``` ```html CDN theme={"system"} ``` > [!NOTE] > > When using the CDN the latest version of the library is always pulled straight > from the server for every request. ### Initializing Secure Fields With **Secure Fields** installed it's now possible to initialize the connection to your instance. It expects the checkout session ID you created earlier. ```js theme={"system"} import SecureFields from "@gr4vy/secure-fields"; // set up the environment const gr4vyId = "example"; const environment = "sandbox"; const sessionId="8e2adc90-681d-442b-acd4-2d5182e62c72" // initialize the SDK const secureFields = new SecureFields({ gr4vyId, environment, sessionId, }); // enable debugging secureFields.setDebug(true); ``` ### Create a card form Next, add any of the 3 individual card fields to the form. Mix and match these with custom forms, and add custom labels. Let's start by adding the form with 4 fields: the card holder's name, the card number, card expiration date, and the security code for the card. ```html theme={"system"} Pay ``` It is important to add all the card inputs including the number, expiration date, and security code. Without each of these fields, a transaction can not be processed. Additional inputs can be added to your form but they will not be handled by our library. Next, initialize each of the inputs as a secure field. ```js theme={"system"} // Add fields using a querySelector compatible string const number = secureFields.addCardNumberField("#cc-number", { placeholder: "Enter card number", }); const securityCode = secureFields.addSecurityCodeField("#cc-security-code", { placeholder: "Enter CVV", }); // Alternatively an HTML element can be passed directly const expiryDate = secureFields.addExpiryDateField( document.querySelector("#cc-expiry-date"), { placeholder: "Enter expiry date", } ); ``` This example replaces each of the `` fields for the card form with a new PCI secure field. It's worth noting that we did not attach a secure field for the card holder name, instead, this field will remain a regular HTML input > [!NOTE] > > Every field that Secure Field is attached to needs to be an HTML element. In > this case, we attached the fields to `` fields. The fields can be > attached to any element using a `querySelector`-compatible query. Each secure field should now be loaded on the page. Please see our more extensive guides on how to add [event listeners](/guides/payments/secure-fields/events) and custom [styles](/guides/payments/secure-fields/theming) to **Secure Fields**. ### Add Click to Pay Next, add a placeholder element to your form to set where the Click to Pay component renders. Then add two checkboxes for shoppers who are not yet enrolled. Use the first to let them share their card details with Click to Pay, and the second to let them stay remembered in this browser. Finally, add the rest of the Click to Pay components to show and hide their UI on demand. ```html theme={"system"}
``` Next, attach the secure field for Click to Pay to this element. This will require an HTML query for the consent checkbox that controls if the card data for a new card is shared with Click to Pay or not. Next to the consent label, add a link to let users learn more about Click to Pay. References to the card form and sign in HTML containers you added previously are also required. ```js theme={"system"} const clickToPay = secureFields.addClickToPay('#click-to-pay', { srcDpaId: '{SRC_DPA_ID}', dpaName: '{DPA_NAME}', dpaLocale: 'en_AU', cardBrands: ['mastercard', 'visa', 'american-express'], consentCheckbox: "#click-to-pay-consent-checkbox", rememberMeCheckbox: "#click-to-pay-remember-me-checkbox", learnMoreLink: "#click-to-pay-learn-more-link", cardForm: "#cc-form", signIn: "#click-to-pay-sign-in" // email: 'john@example.com', // optional: define a Click to Pay user e-mail instead of capturing it with the sign-in form /* mobileNumber: { countryCode: "61", phoneNumber: "491570159" }, */ // optional: define a Click to Pay user phone instead of capturing it with the sign-in form }) ``` > [!NOTE] > > The `srcDpaId` and `dpaName` options are optional. When you leave either of them out, Secure Fields fetches > both values for the checkout session. Use the `CLICK_TO_PAY_INITIALIZED` event to know when Click to Pay has > loaded. > [!NOTE] > > Secure Fields will automatically display and hide the Click to Pay Component, the sign in form, and your card form, based on the > status of Click to Pay. ### Handle form submission To handle a form submission, listen to the submission of the `
` element or a click of the submit button. Then prevent the default behavior and submit Secure Fields. This will instruct the fields to post the card data (regular, or Click to Pay) to the API. ```js theme={"system"} document.querySelector('#cc-form').addEventListener( "submit", (e) => { e.preventDefault() secureFields.submit() } ) ``` Once the data has been submitted, use the `secureFields` object to listen to the card data being successfully, or unsuccessfully vaulted. ```js theme={"system"} secureFields.addEventListener( SecureFields.Events.CARD_VAULT_SUCCESS, () => { // Make an API call to your server to complete the transaction } ) secureFields.addEventListener( SecureFields.Events.CARD_VAULT_FAILURE, () => { // Handle the inability to store the card data } ) ``` > [!NOTE] > > In the case of a single-page app, it might be useful to unload any of the event > listeners when the form is unloaded. ## Create a payment > [!NOTE] > > Don't send `store: true` with a Click to Pay transaction. Gr4vy doesn't store Click to Pay payment methods > or provision its own network tokens for them, so a Click to Pay payment can't start a recurring series. To store a card for later merchant-initiated > payments, take the first payment through the card form with `store: true`. With the card data stored in the vault, create a transaction on the server-side. To create a transaction, pass the amount, currency, as well as the payment method, in this case a checkout session. ```csharp C# theme={"system"} var transaction = await client.Transactions.CreateAsync( transactionCreate: new TransactionCreate() { Amount = 1299, Currency = "AUD", Country = "AU", PaymentMethod = TransactionCreatePaymentMethod.CreateCheckoutSessionWithUrlPaymentMethodCreate( new CheckoutSessionWithUrlPaymentMethodCreate() { Id = checkoutSession.Id, } ), } ); ``` ```go Go theme={"system"} amount := int64(1299) currency := "AUD" checkoutSessionWithURLPaymentMethodCreate := components.CheckoutSessionWithURLPaymentMethodCreate{ ID: checkoutSession.ID, } paymentMethod := components.CreateTransactionCreatePaymentMethodCheckoutSessionWithURLPaymentMethodCreate(checkoutSessionWithURLPaymentMethodCreate) transactionCreate := components.TransactionCreate{ Amount: amount, Currency: currency, PaymentMethod: &paymentMethod, } transaction, err := client.Transactions.Create(ctx, transactionCreate, nil, nil) // handle response ``` ```java Java theme={"system"} CreateTransactionResponse transactionResponse = gr4vyClient.transactions().create() .transactionCreate(TransactionCreate.builder() .amount(1299L) .currency("AUD") .paymentMethod(TransactionCreatePaymentMethod.of(CheckoutSessionWithUrlPaymentMethodCreate.builder() .id(checkoutSession.id()) .build())) .build()) .call(); Transaction transaction = transactionResponse.transaction().orElse(null); // handle response ``` ```php PHP theme={"system"} $transactionCreate = new TransactionCreate(amount: 1299, currency: 'AUD', paymentMethod: new CheckoutSessionWithUrlPaymentMethodCreate(id: $checkoutSession->id)); $response = self::$sdk->transactions->create($transactionCreate); $transaction = $response->transaction; ``` ```python Python theme={"system"} transaction: models.Transaction = client.transactions.create( amount=1299, currency="AUD", payment_method={ "method": "checkout-session", "id": checkout_session.id, } ) ``` ```ts TypeScript theme={"system"} const transaction = await gr4vy.transactions.create({ amount: 1299, currency: "AUD", paymentMethod: { method: "checkout-session", id: checkoutSession.id } }) ``` The returned transaction includes details about the payment method used, and the status of the transaction. ```json Response theme={"system"} { "type": "transaction", "id": "fe26475d-ec3e-4884-9553-f7356683f7f9", "status": "authentication_succeeded", "amount": 1299, "currency": "AUD", "payment_method": { "type": "payment-method", "id": "77a76f7e-d2de-4bbc-ada9-d6a0015e6bd5", "method": "card", "scheme": "visa", "expiration_date": "07/24", ... }, ... } ``` > [!WARNING] > > ### Client-side vs server-side > > A transaction should be created server-side instead of client-side, as > there are many reasons why a success message may never reach your server, leaving > you in a state where it's unclear if your transaction has succeeded. ## Error handling The available error codes for Click to Pay are as follows. It is recommended to catch and handle Click to Pay errors as required. * `CODE_INVALID` - The user entered an incorrect OTP. An error message is also displayed on the OTP component. * `INVALID_CARD` - The user entered incorrect card details, or the details of a card that is not one of the supported schemes. * `ACCT_INACCESSIBLE` - The user's account is temporarily not accessible. * `USER_NOT_RECOGNIZED` - The user is not recognized by Click to Pay (typically when a user hasn't signed in yet). * `UNKNOWN` - An unknown error occurred. These are usually unrecoverable. For example, the Click to Pay library was not initialized due to an invalid parameter. * `RETRIES_EXCEEDED` - The user has exceeded the maximum number of retries filling in their one-time password (OTP) and their profile is locked. An error message is also displayed in the OTP component. **It is recommended to hide the Click to Pay component for the user at this point.** * `SIGN_OUT_FAILED` - When attempting to sign out via the "Not you?" link, it is possible that Click to Pay is not able to sign out the user. **It is recommended to hide the Click to Pay component for the user at this point.** **React** ```js theme={"system"} { if (error.code === 'RETRIES_EXCEEDED' || error.code === 'SIGN_OUT_FAILED') { // Hide the Click to Pay component setClickToPayVisible(false); } console.error(error); }} /> ``` **Node/CDN** ```js theme={"system"} secureFields.addEventListener( SecureFields.Events.CLICK_TO_PAY_ERROR, ({ error }) => { console.error(error); if ((error.code === 'RETRIES_EXCEEDED' || error.code === 'SIGN_OUT_FAILED') && clickToPay) { // Hide the Click to Pay form clickToPay.hide(); } } ) ``` ## Events > [!NOTE] > > Use `METHOD_CHANGE` to update your UI, and read `secureFields.method` when the buyer submits to know which > method is active. A `card` method doesn't rule out Click to Pay. When a recognized Click to Pay shopper enters > a new card and the consent checkbox is checked, Secure Fields submits the card through Click to Pay. **React** The following props can be used on the `SecureFields` component to listen to specific Click to Pay events. | Name | Event | Description | | - | - | - | | `onMethodChange` | `METHOD_CHANGE` | Triggered when a user switches between the card and the Click to Pay payment methods. Also triggered once when Click to Pay is added, with the method set by the initial state of the consent checkbox. | | `onClickToPayInitialized` | `CLICK_TO_PAY_INITIALIZED` | Triggered when Click to Pay is loaded and its `init` method called successfully. | | `onClickToPayReady` | `CLICK_TO_PAY_READY` | Triggered when Click to Pay has looked up the shopper. The event data includes `buyerExists`, which shows whether the shopper is enrolled. For a shopper who verifies with a one-time password, it's triggered after verification. When no email address, phone number, or recognized browser is available, `CLICK_TO_PAY_ERROR` with `USER_NOT_RECOGNIZED` is triggered instead. | | `onClickToPaySignOut` | `CLICK_TO_PAY_SIGN_OUT` | Triggered when a Click to Pay user signs out using the "Not me" / "Not my cards" functionality. | | `onClickToPayError` | `CLICK_TO_PAY_ERROR` | See [Error Handling](#error-handling) | | `onClickToPayCancel` | `CLICK_TO_PAY_CANCEL` | Triggered when a Click to Pay user cancels the checkout process from within the UI. | | `onClickToPayCheckoutWithNewCard` | `CLICK_TO_PAY_CHECKOUT_WITH_NEW_CARD` | Triggered when a Click to Pay user starts the process of checking out with a new (not enrolled) card. | | `onClickToPayUnableToLoadDpa` | `CLICK_TO_PAY_UNABLE_TO_LOAD_DPA` | Triggered when `srcDpaId` or `dpaName` isn't set and Secure Fields can't fetch them for the checkout session. Secure Fields then hides Click to Pay and shows the card form. A `srcDpaId` or `dpaName` that you pass in isn't validated. | Additionally, you can add the prop `onVisibilityChange` to the `ClickToPay`, `ClickToPaySignIn` and `CardForm` components to listen to visibility change events (`CLICK_TO_PAY_VISIBILITY_CHANGE`, `CLICK_TO_PAY_CARD_FORM_VISIBILITY_CHANGE`, `CLICK_TO_PAY_SIGN_IN_VISIBILITY_CHANGE`). ```js theme={"system"} console.log('Card form visible', visible)} /> ``` **Node/CDN** The following Click to Pay events can be listened to by attaching an event handler to the `SecureFields` instance using the `addEventListener` method. | Name | Description | | - | - | | `METHOD_CHANGE` | Triggered when a user switches between the card and the Click to Pay payment methods. Also triggered once when Click to Pay is added, with the method set by the initial state of the consent checkbox. | | `CLICK_TO_PAY_INITIALIZED` | Triggered when Click to Pay is loaded and its `init` method called successfully. | | `CLICK_TO_PAY_READY` | Triggered when Click to Pay has looked up the shopper. The event data includes `buyerExists`, which shows whether the shopper is enrolled. For a shopper who verifies with a one-time password, it's triggered after verification. When no email address, phone number, or recognized browser is available, `CLICK_TO_PAY_ERROR` with `USER_NOT_RECOGNIZED` is triggered instead. | | `CLICK_TO_PAY_SIGN_OUT` | Triggered when a Click to Pay user signs out using the "Not me" / "Not my cards" functionality. | | `CLICK_TO_PAY_ERROR` | See [Error Handling](#error-handling) | | `CLICK_TO_PAY_CANCEL` | Triggered when a Click to Pay user cancels the checkout process from within the UI. | | `CLICK_TO_PAY_CHECKOUT_WITH_NEW_CARD` | Triggered when a Click to Pay user starts the process of checking out with a new (not enrolled) card. | | `CLICK_TO_PAY_UNABLE_TO_LOAD_DPA` | Triggered when `srcDpaId` or `dpaName` isn't set and Secure Fields can't fetch them for the checkout session. Secure Fields then hides Click to Pay and shows the card form. A `srcDpaId` or `dpaName` that you pass in isn't validated. | | `CLICK_TO_PAY_VISIBILITY_CHANGE` | Triggered when the Click to Pay element is shown or hidden. | | `CLICK_TO_PAY_CARD_FORM_VISIBILITY_CHANGE` | Triggered when the card form container is shown or hidden. | | `CLICK_TO_PAY_SIGN_IN_VISIBILITY_CHANGE` | Triggered when the sign in container is shown or hidden. | ```js theme={"system"} // Listen for the different Click to Pay events secureFields.addEventListener( SecureFields.Events.METHOD_CHANGE, () => { console.log('Method changed'); } ) ``` ## Testing To test Click to Pay in the `sandbox` environment, use the test resources below. These sandbox environments and test cards are provided and maintained by the card networks. * [Click to Pay test cards](https://developer.mastercard.com/unified-checkout-solutions/documentation/testing/test_cases/click_to_pay_case/#test-cards) — test card numbers for Click to Pay scenarios. To test Mastercard passkeys with TAS or CAM, use the cards under **Tokenization Eligible Test Cards For Token Authentication Framework**. Other Mastercard test cards fall back to OTP. See testing guidance for [TAS](/guides/features/click-to-pay/tas#testing-in-sandbox) and [CAM](/guides/features/click-to-pay/cam#testing-in-sandbox). * [Mastercard sandbox account](https://sandbox.src.mastercard.com/profile) — manage your Mastercard Click to Pay sandbox account and enrolled cards. * [Visa sandbox account](https://sandbox.secure.checkout.visa.com/) — manage your Visa Click to Pay account and enrolled cards. ## Links > **Quick start:** Secure fields > > [Open](/guides/payments/secure-fields/quick-start/overview) > **Guide:** Vault Forward > > [Open](/guides/features/vault-forwarding/overview) > **API Reference:** New checkout session > > [Open](/reference/checkout-sessions/new-checkout-session) > **API Reference:** New transaction > > [Open](/reference/transactions/new-transaction) # Token Authentication Service (TAS) Source: https://integration.wpay.com.au/guides/features/click-to-pay/tas Token Authentication Service (TAS) is a security feature provided by Mastercard for Click to Pay transactions. TAS generates a cryptogram that authenticates the payment token during checkout, providing an additional layer of security similar to EMV chip transactions for online payments. > [!NOTE] > > TAS is only available for Mastercard and Visa transactions. Other card networks supported by Click to Pay (American Express and Discover) do not currently support this feature. ## How TAS works When TAS is enabled, a cryptogram is generated during the checkout process. This cryptogram is a unique, one-time code that validates the authenticity of the payment token being used. The cryptogram is sent along with the transaction to the card issuer for verification. This process happens seamlessly in the background without requiring any additional action from the customer. The cryptogram provides strong authentication that the payment token is being used by the authorized card holder, reducing the risk of fraud. On supported devices, TAS leverages passkeys (biometric authentication such as fingerprint or face recognition) to further streamline the authentication process while maintaining high security standards. When a customer authenticates with a passkey, the transaction is no longer treated as a 3-D Secure flow. The merchant no longer sees 3DS data on the transaction. Instead, it is processed as a network token transaction with a cryptogram and an Electronic Commerce Indicator (ECI) that reflects the authentication outcome. ### Passkeys Passkeys are part of TAS, and you can't turn them off on their own. To stop customers from authenticating with passkeys, you have to turn off TAS by setting `authenticate.checkout` to `false`, which also removes TAS authentication from your Click to Pay transactions. ## Benefits * **Enhanced Security**: Adds cryptographic authentication to token-based transactions, significantly reducing fraud risk. * **Higher Authorization Rates**: Issuers can approve transactions with greater confidence, leading to fewer declined payments. * **Replaces 3-D Secure**: Provides strong authentication that can replace traditional 3-D Secure (3DS) flows, eliminating redirect steps and improving conversion rates. * **Seamless Experience**: Works transparently without adding friction to the customer checkout flow. * **EMV-Level Security**: Brings chip card security standards to online and mobile transactions. ## Prerequisites TAS requires 3-D Secure to be configured at the merchant account level, not at the connection level. This is because TAS authentication occurs before transaction routing, at a point where the payment service connection has not yet been determined. The authentication result applies to whichever connection processes the transaction. To set up 3DS at the merchant account level, follow the instructions in the [3-D Secure setup guide](/guides/features/3ds/setup#add-a-merchant-account-configuration). Make sure to configure a scheme profile for each card scheme you want to support with Click to Pay. Gr4vy matches the configuration against the checkout session's `metadata`, not the transaction's. If your configurations use metadata, set the same key-value pairs on the checkout session. ## Implementation The platform makes it simple to enable Token Authentication Service when using Secure Fields for Click to Pay. To activate TAS, add the `authenticate.checkout` property to your Secure Fields configuration and set it to `true`. When enabled, TAS automatically generates the cryptogram for each Click to Pay transaction, providing enhanced security without any additional integration effort. ```jsx React {9} theme={"system"} ``` ```js Node {11} theme={"system"} const clickToPay = secureFields.addClickToPay('#click-to-pay', { srcDpaId: '{SRC_DPA_ID}', dpaName: '{DPA_NAME}', dpaLocale: 'en_AU', cardBrands: ['mastercard', 'visa', 'american-express'], consentCheckbox: "#click-to-pay-consent-checkbox", learnMoreLink: "#click-to-pay-learn-more-link", cardForm: "#cc-form", signIn: "#click-to-pay-sign-in" authenticate: { checkout: true, } }) ``` ## Testing in sandbox Use the following guidance to test passkeys with [TAS](/guides/features/click-to-pay/tas) and [CAM](/guides/features/click-to-pay/cam) in the `sandbox` environment. ### Use the right test cards To test Mastercard passkeys and TAS, use the cards listed under **Tokenization Eligible Test Cards For Token Authentication Framework** in the [Mastercard test cards](https://developer.mastercard.com/unified-checkout-solutions/documentation/testing/test_cases/click_to_pay_case/#mastercard-test-cards). Other Mastercard Click to Pay test cards fall back to one-time password (OTP) authentication. Mastercard maintains this list, so check their page for the current card numbers. ### Reproduce the passkey step With CAM enabled, Click to Pay tries to authenticate the customer in the following order: 1. A Merchant Recognized Token (MRT) cookie from a previous session. 2. A passkey. 3. An OTP. The passkey step only appears when no valid MRT exists. To reproduce it, clear your cookies or use a fresh browser profile or private window. > [!NOTE] > > Mastercard decides whether to offer a passkey. Availability also depends on the card, the issuer, and the device. Don't assume that the passkey step always appears, or that it never does. ### Don't block the passkey popup Passkey authentication runs in a Mastercard popup window, not inside the Click to Pay iframe. The customer completes the biometric prompt in that popup. > [!WARNING] > > Don't cover the page with overlays, loading spinners, or other blocking elements while the Click to Pay flow is active. Blocking the page makes the popup and the biometric prompt unusable. ### Control the 3DS outcome TAS requires [merchant account-level 3DS](/guides/features/3ds/setup#add-a-merchant-account-configuration). The 3DS sandbox triggers outcomes based on the last four digits of the card number. The Mastercard Click to Pay test cards don't match these triggers, so 3DS returns an `Unknown test` result. To control the 3DS outcome for these cards, register a [3DS scenario](/guides/features/3ds/scenarios) for the test card number. ## Common issues > **3DS overlay pops up but automatically closes** > > This occurs when the 3DS acquirer configuration does not match the required values. Check the 3DS acquirer configuration and verify that the acquirer details for each card scheme are configured exactly as per the [Mastercard Unified Checkout Solutions SDK reference](https://developer.mastercard.com/unified-checkout-solutions/documentation/sdk-reference/parameters/#acquirer-data), listed below. > > **Mastercard** > > | Parameter | Value | > | - | - | > | Acquirer BIN | `545301` | > | Acquirer Merchant ID | `SRC3DS` | > | MCC | `0001` | > > **Visa** > > | Parameter | Value | > | - | - | > | Acquirer BIN | `432104` | > | Acquirer Merchant ID | `33334444` | > | MCC | `0001` | > **No 3DS prompt when you expect a challenge flow** > > On supported devices, authentication can complete with a passkey and no 3DS prompt appears. If you expect a challenge-based 3DS flow and no prompt appears, work through the following checks. > > **1. Check which features are enabled** > > In the browser developer tools, open the **Network** tab and find the request to `click-to-pay.html`. > Copy the **Request URL** and URL-decode it twice. The decoded payload should include an `authenticate` block reflecting the features you have enabled: > > ```json theme={"system"} > "authenticate": { > "checkout": true, > "consumer": true > } > ``` > > `authenticate.checkout` controls [TAS](/guides/features/click-to-pay/tas) and `authenticate.consumer` controls [CAM](/guides/features/click-to-pay/cam). If either value is missing or set to `false`, the feature is not enabled. > > **2. Check the `dynamicDataType` field** > > In the same decoded payload, ensure that the `dynamicDataType` field is set to `CARD_APPLICATION_CRYPTOGRAM_SHORT_FORM`. > Secure Fields uses this value by default, unless you set the `dynamicDataType` option to `NONE`. > This configuration requests a dynamic network token, which [TAS](/guides/features/click-to-pay/tas) requires. > > **3. Verify `acquirer_data` is present in the acquirer response** > > In the **Network** tab, locate the acquirer request. > The response must have values for `acquirer_data`. If there is no acquirer data, it is likely due to the checkout session initiation. > Ensure the checkout session setup was created with the correct data fields. # Gift Cards Source: https://integration.wpay.com.au/guides/features/gift-cards/overview Gift card payments are supported directly through the API with support for split tender across traditional payment methods and gift cards, as well as automatic reversal. ## Setup Currently, gift cards are supported through Qwikcilver only. ## Features * **Balance checks** - Query the balance of one or more gift cards before checkout. * **Gift card-only payments** - Accept payments funded entirely by gift cards. * **Split tender** - Split a payment across one or more gift cards and a standard payment method. * **Stored gift cards** - Vault gift cards against a buyer for reuse. * **Automatic reversal** - Automatically revert all gift card charges if any single card fails. * **Virtual card issuance** - Issue a new virtual gift card through a supported gift card service. * **Physical card activation** - Activate a physical gift card, with the option to store it in the vault. ## API endpoints The API for gift cards consists of the following endpoints and API features. * [Check gift card balances](/reference/gift-cards/list-gift-card-balances) * [Manage stored gift cards](/reference/gift-cards) and associate them with buyers * [Issue a virtual gift card](/reference/gift-cards/issue-gift-card) * [Activate a physical gift card](/reference/gift-cards/activate-gift-card) * [Enhancements to the transaction endpoints](/reference/transactions/new-transaction) to allow for payments with one or more gift cards, including the ability to split a payment across gift cards and other payment methods. ### Issue a virtual gift card You can issue a new virtual gift card through the primary gift card service configured on the merchant account. ```csharp C# theme={"system"} var res = await sdk.GiftCards.Issuances.CreateAsync(giftCardIssuanceCreate: new GiftCardIssuanceCreate() { Theme = "031111372", Amount = 5000, Currency = "EUR", }); // handle response ``` ```go Go theme={"system"} res, err := s.GiftCards.Issuances.Create(ctx, components.GiftCardIssuanceCreate{ Theme: "031111372", Amount: 5000, Currency: "EUR", }) if err != nil { log.Fatal(err) } if res != nil { // handle response } ``` ```java Java theme={"system"} IssueGiftCardResponse res = sdk.giftCards().issuances().create() .giftCardIssuanceCreate(GiftCardIssuanceCreate.builder() .theme("031111372") .amount(5000L) .currency("EUR") .build()) .call(); if (res.giftCardIssuance().isPresent()) { System.out.println(res.giftCardIssuance().get()); } ``` ```php PHP theme={"system"} $giftCardIssuanceCreate = new Gr4vy\GiftCardIssuanceCreate( theme: '031111372', amount: 5000, currency: 'EUR', ); $response = $sdk->giftCards->issuances->create( giftCardIssuanceCreate: $giftCardIssuanceCreate ); if ($response->giftCardIssuance !== null) { // handle response } ``` ```python Python theme={"system"} res = g_client.gift_cards.issuances.create(theme="031111372", amount=5000, currency="EUR") # Handle response print(res) ``` ```typescript TypeScript theme={"system"} const result = await gr4vy.giftCards.issuances.create({ theme: "031111372", amount: 5000, currency: "EUR", }); console.log(result); ``` The response includes a `url` for the issued gift card. The raw card number and PIN are not returned directly in the API response. If you want to store the card for future use, retrieve the card details from that URL and pass them to the [store a gift card](#store-a-gift-card) endpoint. See the [issue a gift card API reference](/reference/gift-cards/issue-gift-card) for the full list of request and response fields. ### Activate a physical gift card You can activate a physical gift card through the primary gift card service. The `pin` is optional, but if provided, it must be the correct PIN for the card. Set `store` to `true` to also store the activated gift card in the vault, optionally associating it with a buyer using `buyer_id` or `buyer_external_identifier`. A `pin` is required when `store` is `true`. ```csharp C# theme={"system"} var res = await sdk.GiftCards.Activations.CreateAsync(giftCardActivationCreate: new GiftCardActivationCreate() { Number = "4123455541234561234", }); // handle response ``` ```go Go theme={"system"} res, err := s.GiftCards.Activations.Create(ctx, components.GiftCardActivationCreate{ Number: "4123455541234561234", }) if err != nil { log.Fatal(err) } if res != nil { // handle response } ``` ```java Java theme={"system"} ActivateGiftCardResponse res = sdk.giftCards().activations().create() .giftCardActivationCreate(GiftCardActivationCreate.builder() .number("4123455541234561234") .build()) .call(); if (res.giftCard().isPresent()) { System.out.println(res.giftCard().get()); } ``` ```php PHP theme={"system"} $giftCardActivationCreate = new Gr4vy\GiftCardActivationCreate( number: '4123455541234561234', ); $response = $sdk->giftCards->activations->create( giftCardActivationCreate: $giftCardActivationCreate ); if ($response->giftCard !== null) { // handle response } ``` ```python Python theme={"system"} res = g_client.gift_cards.activations.create(number="4123455541234561234", store=False) # Handle response print(res) ``` ```typescript TypeScript theme={"system"} const result = await gr4vy.giftCards.activations.create({ number: "4123455541234561234", }); console.log(result); ``` When `store` is `true`, a `gift-card.created` [webhook](/guides/features/webhooks/events) is sent once the card has been stored. See the [activate a gift card API reference](/reference/gift-cards/activate-gift-card) for the full list of request and response fields. ### Pay with gift cards To pay with gift cards, [create a transaction](/reference/transactions/new-transaction) with a `gift_cards` list. Each entry holds the `amount` to charge to that card, together with either the card's `number` and `pin` or the `id` of a stored gift card. The `amount` of the transaction is the total across all funding sources. The API validates the amounts as follows: * **Gift cards only**: Leave out `payment_method`. The gift card amounts must add up to the transaction `amount` exactly. * **Split tender**: Add a `payment_method` alongside the gift cards. The gift card amounts must add up to less than the transaction `amount`, and Gr4vy charges the remainder to the payment method. * **Gift cards that cover the full amount**: Don't send a `payment_method`. If you send one, the API rejects the request, because nothing remains to charge to it. Every transaction needs at least a `payment_method` or one gift card, and the number of gift cards is subject to the [limit](#limits). ```http theme={"system"} POST /transactions ``` ```json theme={"system"} { "amount": 5000, "currency": "EUR", "gift_cards": [ { "id": "356d56e5-fe16-42ae-97ee-8d55d846ae2e", "amount": 2000 } ], "payment_method": { "method": "id", "id": "ef9496d8-53a5-4aad-8ca2-00eb68334389" } } ``` In this example, Gr4vy redeems 2000 from the gift card and charges the remaining 3000 to the stored payment method. ### Refund a split tender transaction Gr4vy doesn't split a refund across the funding sources of a transaction. You choose the target and amount of each refund: * A [refund](/reference/transactions/refund-transaction) targets the `payment_method` by default. * To refund a gift card, set `target_type` to `gift-card-redemption` and `target_id` to the `id` of the redemption in the transaction's `gift_card_redemptions`. * To refund all funding sources of the transaction in one request, use [refund all](/reference/transactions/refund-all). ## Reversals As part of processing gift cards, the ability to automatically revert an authorized payment method or redeemed gift card before it is reverted is supported. The logic for this feature is as follows. * When a transaction occurs, regular payment methods are authorized (or captured) first, depending on their support for delayed capture. * Gift cards are only redeemed after the (optional) regular payment method has succeeded to authorize/capture. * When no regular payment method is present, gift cards are always redeemed. * In the case that any of the gift cards failed to redeem, any redemptions of other gift cards and regular payments are reverted. * In the case of Qwikcilver, gift cards are processed in a batch mode so any failed gift card automatically results in all gift cards failing. * In the case of an authorized or captured regular payment method, the transaction is reverted by either voiding the authorization or refunding a capture. > [!WARNING] > > In some cases, gift card redemptions are not reverted. Please see the anti-fraud section below. ### Outcome There are two fields returned by the transaction API which allows you to quickly understand if the original intent of a transaction was met. * The `multi_tender` (boolean) field indicates if the transaction included more than one tender. * The `intent_outcome` (enum) field indicates if the original intent (`authorize` / `capture`) was met. This field is set to either `pending` if the transaction has not completed yet, `succeeded` in the case all tenders were processed successfully, and `failed` if any of them failed. This field does not change value after the `succeeded` or `failed` status has been achieved, even if the transaction is subsequently captured, voided, or refunded in any way. ```json theme={"system"} { "type": "transaction", "id": "89bdaeeb-4be8-4ab7-a9e2-6f2c3da5f2d0", "intent": "capture", "multi_tender": true, "intent_outcome": "succeeded", ... } ``` ### Reversals and anti-fraud reviews There is a key difference in how reversals are handled when a transaction encounters any kind of halt in processing. This happens when the transaction is held in anti-fraud review. In these situations, if the approval fails, or if the transaction is rejected, the gift cards are **not refunded**. The reason for this is that because of the time delay between the redemption and refund the user may no longer have the gift card at hand anymore. ## Gift card usage data Each stored gift card tracks usage statistics that help you determine which card to preselect at checkout or prioritize in your UI. | Field | Description | | - | - | | `last_used_at` | The date and time when this gift card was most recently used in any transaction. `null` if the gift card has not been used yet. | | `usage_count` | The number of times this gift card has been used in any transaction. | | `cit_last_used_at` | The date and time when this gift card was most recently used in a customer-initiated transaction (CIT). `null` if the gift card has not been used in a CIT yet. | | `cit_usage_count` | The number of times this gift card has been used in a customer-initiated transaction (CIT). | > [!NOTE] > > Usage data is not back-filled. Only transactions processed after this feature was enabled are counted. ### Sorting by usage The [list buyer gift cards](/reference/gift-cards/list-buyer-gift-cards) endpoint supports a `sort_by` query parameter to order results by any of the four usage fields. ``` GET /buyers/gift-cards?sort_by=last_used_at GET /buyers/gift-cards?sort_by=cit_last_used_at ``` Results are returned in descending order by default, so the most recently used or most frequently used gift card appears first. To sort in ascending order, set `order_by=asc`. ## Stored gift card filtering The API automatically removes any gift cards stored for a buyer with a zero balance or an expiry date in the past. When you add a new card, you may receive an error informing you that you've hit the limit of the number of stored cards (defaults to 10). > [!NOTE] > > You need to actively call the [`GET /buyers/gift-cards`](/reference/gift-cards/list-buyer-gift-cards) to clear out any expired > and zero balance gift cards. ## Limits By default, the number of gift cards that can be used at the same time is limited to 10. This limit applies to the APIs for querying gift card balances, processing gift cards, as well as the number of gift cards that can be stored on a buyer. To change this limit, reach out to the support team. In the event of the limit being exceeded, a `HTTP 400` response with an error message `The buyer's gift card count has already reached the maximum limit of 10 gift cards` is returned. ## Pending features The following features are still in development and are added in the coming weeks. * Support for declines using Flow for transactions not involving scheme cards. [qwikcilver]: /connections/payments/qwikcilver-gift-card # Gift card simulator Source: https://integration.wpay.com.au/guides/features/gift-cards/simulator The gift card simulator is a sandbox-only connector that can be used to simulate responses for balance checks, gift card redemptions and refunds, and [issuing](/reference/gift-cards/issue-gift-card) and [activating](/reference/gift-cards/activate-gift-card) gift cards. The values for the `error_code`, `balance` and `expiration_date` can be simulated by passing in different pin and gift card numbers. This can be used when testing your integration. ## Credentials To configure the connection, you need to set the following credentials. | Credential | Description | | - | - | | API Key | This is not validated and can be set to any string | ## Error codes The `pin` field is used to simulate error codes: * Starts with `0`: Reserved for successful responses. Example: `01000`. * Starts with `1`: Validation error returned by the provider, `error_code` populated as explained below. Example `10001`. * Starts with `2`: Server error, `error_code` populated as explained below. Example: `20010`. * Starts with `3`: Client error, `error_code` populated as explained below. Example: `30003`. The rest of digits of the `pin` number are used to determine the `error_code`: | `error_code` | Description | PIN last digits | | :- | :- | :- | | `invalid_gift_card` | Gift card details are invalid, for example number or pin. | `0000` | | `expired_card` | Gift card expired. | `0001` | | `inactive_card` | Gift card is inactive. | `0002` | | `invalid_service_credentials` | Service credentials are not valid. | `0003` | | `invalid_amount` | Amount is invalid. | `0004` | | `incorrect_currency` | Currency was rejected by service. | `0005` | | `insufficient_funds` | Amount exceeds available balance. | `0006` | | `invalid_service_configuration` | Service is incorrectly configured. | `0007` | | `operation_canceled` | Operation was canceled as part of the batch. | `0008` | | `service_error` | Internal server error or upstream processing error. | `0009` | | `service_network_error` | Service was unreachable or experienced a timeout. | `0010` | | `unknown_error` | Mapping to a more specific error code failed. | `0011` | | `max_gift_cards_reached` | Buyer reached the maximum number of stored gift cards. | `0012` | | `suspected_fraud` | Service flagged the transaction as suspected fraud. | `0013` | | `canceled_payment_method` | Gift card reported lost, stolen, or otherwise canceled. | `0014` | | `duplicate_transaction` | Transaction is a duplicate of a previous transaction. | `0015` | | `unexpected_state` | Service or account is in an unexpected state. | `0016` | Any other value is mapped to `unknown_error` too. ## Refunds Similar logic is applied to trigger error cases for refunds, but the last digit of the refund amount is used, rather than the PIN. * Ends with `0`: Reserved for successful responses. Example: `1000`. * Ends with `1`: Validation error returned by the provider. Example `1001`. * Ends with `2`: Server error. Example: `1002`. * Ends with `3`: Client error. Example: `1003`. ## Balance For endpoints returning the gift card balance, its value is the `pin` number as an integer. The recommendation is that it starts with `0`, so it's always considered a successful response. For example, a PIN of `01234` results in a gift card balance of `12.34`. ## Activation [Activating](/reference/gift-cards/activate-gift-card) a gift card uses the `pin` to simulate errors, and the `error_code` is taken from the remaining digits as described in the error codes section. The prefixes differ slightly from a balance check. * Omit the `pin`: successful activation. * Starts with `1`: Client error. Example: `10001`. * Starts with `2`: Server error. Example: `20010`. * Starts with `3`: Client error. Example: `30003`. * Starts with `4`: Validation error returned by the provider. Example: `40006`. On a successful activation, the resulting balance is the `amount` you send. Omit the amount and the balance is derived from the `pin` exactly as described in the [Balance](#balance) section. ## Issuance [Issuing](/reference/gift-cards/issue-gift-card) a virtual gift card has no `pin` or `number` to simulate errors with, so the `amount` is used instead. The last digit selects the type of error and the remaining digits determine the `error_code`. * Ends with `0`: Reserved for successful responses. Example: `5000`. * Ends with `1`: Client error. Example: `10001`. * Ends with `2`: Server error. Example: `10002`. * Ends with `3`: Client error. Example: `10003`. * Ends with `4`: Validation error returned by the provider. Example: `10004`. ## Expiration date For endpoints returning the expiration date, the gift card `number` is considered. * A gift card number that starts with `0` results in an expiration date in the past. For example: `0000444455556666` results in an expiration date set in the past. Currently, this is set to a month in the past, but this behavior may change. * A gift card number that starts with `1` results in an expiration date in the future. For example: `1000444455556666` results in an expiration date set in the future. Currently, this is set to a month in the future, but this behavior may change. * Otherwise, any other gift card has no expiration date. For example: `9000444455556666` results in a gift card without an expiration date. # Split tender Source: https://integration.wpay.com.au/guides/features/gift-cards/split-tender Wpay supports split tender transactions across one primary payment method and one or more gift cards. The maximum number of gift cards that can be redeemed in a split tender is 10. The primary payment method can be a scheme card (including digital wallets for scheme cards, such as Apple Pay and Google Pay) or an alternative payment method such as PayPal, Afterpay, or Zip. ## Overview of split tender To process a split tender, call the [Transaction API](/reference/transactions/new-transaction) with a combination of a primary payment method and gift cards. Key considerations: * A transaction must include at least one primary payment method or one gift card. You may process a gift card only transaction or a split tender transaction between a primary payment method and gift cards. * Gift cards are treated separately from the primary payment method in the API requests and responses. * The `gift_card_redemptions` array in the response contains details of all gift card redemptions. * The `amount` field represents the total transaction amount (primary payment method + gift cards). * The primary payment method is always processed first, followed by gift cards. * Gift card redemptions are always processed as direct capture. * Split tender transactions are atomic: * If the primary payment method fails, no gift card redemption occurs. * If any gift card redemption fails, the transaction is reversed: * The primary payment method is voided. * Successfully redeemed gift cards are refunded. * Unredeemed gift cards are skipped. ## Storing gift cards in a buyer's wallet You can store gift cards in a buyer’s wallet using the [Store Gift Card API](/reference/gift-cards/new-gift-card). This is the only API that can be used to securely vault a single gift card and validates it against the active gift card service. > [!NOTE] > > Calling a [Transaction API](/reference/transactions/new-transaction) with `store = true` only saves the buyer's primary payment method, but not gift cards. Key considerations: * Only unique, active (non-expired) gift cards can be stored for a buyer. * The API returns a `gift card id` that can be used in gift card only or split tender transactions. * By default, buyers can store up to 10 gift cards. * To remove expired or empty gift cards, call the [GET /buyers/gift-cards](/reference/gift-cards/list-buyer-gift-cards). This automatically archives inactive cards, freeing up space for new ones. ### Store gift card API request ```json theme={"system"} { "number": "6280005562385333923", "pin": "5318", "buyer_id": "2835cac4-6731-4dbf-a0d9-c286c5782f26" } ``` ### Store gift card API response ```json theme={"system"} { "type": "gift-card", "id": "84467b52-8b3d-4a23-bdce-3a56bbdd700c", "merchant_account_id": "default", "gift_card_service": { "type": "gift-card-service", "id": "8b1242fb-2adf-4b72-8c15-6cbc1963cde4", "gift_card_service_definition_id": "qwikcilver-gift-card", "display_name": "Qwikcilver" }, "bin": "628000", "sub_bin": "556", "last4": "3923", "expiration_date": "2026-03-14T13:59:59.999999+00:00", "buyer": { "type": "buyer", "id": "2835cac4-6731-4dbf-a0d9-c286c5782f26", "merchant_account_id": "default", "display_name": "John Smyth", "external_identifier": null, "billing_details": { "first_name": "John", "last_name": "Smyth", "email_address": "john.smyth@gmail.com", "phone_number": null, "address": { "city": "Surry Hills", "country": "AU", "postal_code": "2028", "state": "NSW", "state_code": "AU-NSW", "house_number_or_name": "372", "line1": "Elizabeth St", "organization": "Org" }, "tax_id": null }, "account_number": null, "created_at": "2023-11-30T23:34:49.486829+00:00", "updated_at": "2023-11-30T23:36:46.311226+00:00" }, "created_at": "2025-03-26T00:17:49.656567+00:00", "updated_at": "2025-03-26T00:17:49.656567+00:00" } ``` ## Gift card only transaction To process a payment using only gift cards, set `payment_method` to `null` and include gift card details under the [`gift-cards`](/reference/transactions/new-transaction#body-gift-cards) array. ### Request Example Add new or saved gift cards information in the `gift-cards` array. ```json theme={"system"} { "amount": 1299, "currency": "AUD", "intent": "authorize", "payment_source": "ecommerce", "Payment_method" : null, "gift-cards": [{ "number": "6280005567112008599", "pin": "5962", "amount": 1000 },{ "number": "6280005562385333923", "pin": "5318", "amount": 299 }] ... } ``` ### Response Highlights * intent\_outcome\` reflects the overall transaction result. * `status` represents the state of the primary payment method. In gift card only transactions, this does not really matter as there is no primary payment method involved (primary payment method is `null`). * `amount` represents the total amount charged. `authorized_amount` is `0` since there is no primary payment method being charged. * The `gift_card_redemptions` array contains the redemption details including the `gift_card_redemptions.status`, `gift_card_redemptions.error_code`, and `gift_card_redemptions.refunded_amount`. * If multiple gift cards are used, `multi_tender` is `true`. * If there is only one gift card, `multi_tender` is `false` ```json theme={"system"} { "type": "transaction", "id": "0d4d2f8b-c202-470d-aadb-cb175824a0c8", "amount": 1299, "status": "authorization_succeeded", "gift_card_redemptions": [ { "type": "gift-card-redemption", "id": "5fdd5d98-55a8-43b3-8b09-e35b904d5d97", "status": "succeeded", "amount": 1000, "refunded_amount": 0, "gift_card_service_redemption_id": "23598320", "error_code": null, "raw_error_code": null, "raw_error_message": null, "gift_card": { "type": "gift-card", "id": null, "bin": "628000", "sub_bin": "556", "last4": "8599" } }, { "type": "gift-card-redemption", "id": "d4f45643-8dbb-456d-b0ba-a09e5778d12c", "status": "succeeded", "amount": 299, "refunded_amount": 0, "gift_card_service_redemption_id": "23598321", "error_code": null, "raw_error_code": null, "raw_error_message": null, "gift_card": { "type": "gift-card", "id": null, "bin": "628000", "sub_bin": "556", "last4": "3923" } } ], "gift_card_service": { "type": "gift-card-service", "id": "8b1242fb-2adf-4b72-8c15-6cbc1963cde4", "gift_card_service_definition_id": "qwikcilver-gift-card", "display_name": "Qwikcilver" }, "payment_source": "ecommerce", "intent_outcome": "succeeded", "multi_tender": true, ... } ``` ## Split Tender Transaction (Scheme Card + Gift Cards) To process a split tender transaction with a scheme card and gift cards, specify the scheme card under `payment-methods` object and include gift cards in the `gift-cards` array. You may either include new gift cards (with gift card number and pin) or stored gift cards (with gift card id). > [!NOTE] > > To store gift cards, use the [Store gift card API](/reference/gift-cards/new-gift-card). This API returns the gift-card `id` that can be used in a split tender transaction. Calling a [Transaction API](/reference/transactions/new-transaction) with `store = true` only saves the buyer's primary payment method, but not gift cards. ### Request Example (Stored Gift Cards) ```json theme={"system"} { "amount": 1599, "currency": "AUD", "intent": "authorize", "payment_source": "ecommerce", "payment_method": { "method": "card", "number": "4622943127013705", "expiration_date": "03/30", "external_identifier" : null, "security_code": "100", "redirect_url": "https://example.com/" }, "gift-cards": [{ "id": "5c3ba1d8-f418-416f-9493-5abf23e0d05d", "amount": 1000 },{ "id": "4b74e655-bf33-4952-83a3-149cfff43817", "amount": 299 }] ... } ``` ### Response Highlights * `intent_outcome` represents the overall success/failure of the transaction. * `amount` represents the total amount charged including the scheme card and all gift cards. * The `gift_card_redemptions` array contains gift card processing results. * All of the fields outside of the `gift_card_redemptions` array are related to the primary payment method only. * `multi_tender` is always set to `true`. ```json theme={"system"} { "id": "f82bbc79-c9be-4d2a-811c-2e38740ae230", "amount": 1599, "status": "authorization_succeeded", "authorized_amount": 300, "captured_amount": 0, "intent": "authorize", "payment_method": { "details": { "bin": "462294", "card_type": "debit", "card_issuer_name": null }, "scheme": "visa", "id": "25666304-81cc-46e3-b68b-5d60694a51d5", ... }, "gift_card_redemptions": [ { "type": "gift-card-redemption", "id": "4d2c0c21-143e-455f-a1cb-f17f48bfd2cc", "status": "succeeded", "amount": 1000, "refunded_amount": 0, "gift_card_service_redemption_id": "23603110", "error_code": null, "raw_error_code": null, "raw_error_message": null, "gift_card": { "type": "gift-card", "id": "5c3ba1d8-f418-416f-9493-5abf23e0d05d", "bin": "628000", "sub_bin": "556", "last4": "8599" } }, { "type": "gift-card-redemption", "id": "95de3b3a-db97-4361-9463-ee4c35b469e3", "status": "succeeded", "amount": 299, "refunded_amount": 0, "gift_card_service_redemption_id": "23603111", "error_code": null, "raw_error_code": null, "raw_error_message": null, "gift_card": { "type": "gift-card", "id": "4b74e655-bf33-4952-83a3-149cfff43817", "bin": "628000", "sub_bin": "556", "last4": "3923" } } ], "payment_service": { "type": "payment-service", "id": "2a931220-3d0e-484d-8b0f-91b9cd474c01", "payment_service_definition_id": "cybersource-card", "method": "card", "display_name": "Cybersource - wpay_gr4vy_test with 3DS" }, "gift_card_service": { "type": "gift-card-service", "id": "8b1242fb-2adf-4b72-8c15-6cbc1963cde4", "gift_card_service_definition_id": "qwikcilver-gift-card", "display_name": "Qwikcilver" }, "intent_outcome": "succeeded", "multi_tender": true ... } ``` ## Split Tender Transaction (Alternative Payment Method + Gift Cards) You can process a split tender using an alternative payment method (such as [PayPal](/guides/payments/direct-api/paypal), [Afterpay](/guides/payments/direct-api/afterpay), and [Zip Pay](/guides/payments/direct-api/zip)) via direct API integrations: 1. When [calling the Transaction API](/guides/payments/direct-api/quick-start/transaction), include gift card details in the `gift-cards` array. 2. Handle the redirect flow to complete the transaction. 3. Call [GET Transaction](/reference/transactions/get-transaction) API to get the latest status of the transaction. For a step-by-step integration guide, refer to the [Direct API guide](/guides/payments/direct-api/quick-start/overview). ## Handling Split Tender Failures In principle, the primary payment method is processed first before gift cards to prevent complications. There are scenarios where we will fail or decline such as insufficient funds or the transaction after a certain time period has lapsed (e.g. buyer authentication/approval abandonment). Here, charging gift cards first would mean that the gift card amount will not be refunded until the transaction is canceled at a later time. ### Scenario 1: Failed Gift Card If a gift card redemption fails while the primary payment method succeeds, the transaction is reversed: * `intent_outcome` = `failed`. * The primary payment method is voided (`status`: `authorization_voided`) * Successful gift card redemptions are refunded. * Failed gift cards display `status = failed` with an `error_code`. ```json theme={"system"} { "id": "9bd8581c-593e-463e-a25b-90e2022130fb", "amount": 1599, "status": "authorization_voided", "authorized_amount": 300, "payment_method": { "type": "payment-method", "details": { "bin": "462294", "card_type": "debit", "card_issuer_name": null }, "expiration_date": "03/30", ... }, "method": "card", "error_code": null, "raw_response_code": null, "raw_response_description": null, "gift_card_redemptions": [ { "type": "gift-card-redemption", "id": "6054f8ff-0edd-44fb-8c45-3bac2a099e21", "status": "failed", "amount": 1000, "refunded_amount": 0, "gift_card_service_redemption_id": null, "error_code": "operation_canceled", "raw_error_code": "0", "raw_error_message": null, "gift_card": { "type": "gift-card", "id": null, "bin": "628000", "sub_bin": "556", "last4": "8599" } }, { "type": "gift-card-redemption", "id": "e709c330-9c12-468f-a7b2-906b3a94532e", "status": "failed", "amount": 299, "refunded_amount": 0, "gift_card_service_redemption_id": null, "error_code": "expired_card", "raw_error_code": "10001", "raw_error_message": "Card expired.", "gift_card": { "type": "gift-card", "id": null, "bin": "628000", "sub_bin": "556", "last4": "7864" } } ], "authorized_at": "2025-03-16T06:39:45.100925+00:00", "voided_at": "2025-03-16T06:39:46.871247+00:00", "multi_tender": true, "intent_outcome": "failed", ... } ``` ### Scenario 2: Failed Primary Payment Method If the primary payment method fails or is declined:: * `intent_outcome` = `failed`. * The `status` of the primary payment method would either be `authorization_failed` or `authorization_declined`. * Gift card redemptions are not performed (`status = skipped`). ```json theme={"system"} { "type": "transaction", "id": "c33c0762-2300-4a9e-a897-8a418a42254a", "reconciliation_id": "5wOw5tz0J7evgOs6CdsSba", "merchant_account_id": "default", "currency": "AUD", "amount": 1499, "status": "authorization_declined", "authorized_amount": 0, "captured_amount": 0, "refunded_amount": 0, "gift_card_redemptions": [ { "type": "gift-card-redemption", "id": "0b7a4c58-dae8-4e87-be70-edfa834b0c84", "status": "skipped", "amount": 1000, "refunded_amount": 0, "gift_card_service_redemption_id": null, "error_code": null, "raw_error_code": null, "raw_error_message": null, "gift_card": { "type": "gift-card", "id": null, "bin": "628000", "sub_bin": "556", "last4": "8599" } }, { "type": "gift-card-redemption", "id": "5fda97d0-63ee-466c-9fa7-0e7635c9fff3", "status": "skipped", "amount": 299, "refunded_amount": 0, "gift_card_service_redemption_id": null, "error_code": null, "raw_error_code": null, "raw_error_message": null, "gift_card": { "type": "gift-card", "id": null, "bin": "628000", "sub_bin": "556", "last4": "3923" } } ], "intent_outcome": "failed", "multi_tender": true, ... } ``` ## Split Tender and Anti-Fraud Manual Review In scenarios where the anti-fraud engine returns a review decision in a split tender transaction: 1. The primary payment method is authorized but placed on hold for manual review. 2. The gift cards are charged successfully. 3. If the manual review later results in a rejection, the transaction outcome will be: * `intent_outcome`: `failed` * The primary payment method's `status`: `authorization_voided` * Gift cards will be refunded. ## Refunding back to gift cards *Important*: You should not refund back to gift cards for the following reasons: * The buyer may no longer be available to be notified of the refund, or * The buyer may not have the original gift card with them at the time of the refund. Instead, the recommendation is to issue a new gift card or store credit to compensate the buyer. ```json theme={"system"} { "type": "transaction", "id": "f82bbc79-c9be-4d2a-811c-2e38740ae230", "currency": "AUD", "amount": 1599, "status": "authorization_voided", "authorized_amount": 300, "captured_amount": 0, "refunded_amount": 0, "intent": "authorize", "voided_at": "2025-03-16T06:39:46.871247+00:00",, "payment_method": { "type": "payment-method", "approval_url": null, "country": "AU", "currency": null, "details": { "bin": "462294", "card_type": "debit", "card_issuer_name": null }, "expiration_date": "03/30", "fingerprint": "a8e201df83226560fa6d0904e2a8aba393ac22ecc6488b54939261b7f97bb9ba", "label": "3705", "last_replaced_at": null, "method": "card", "mode": "card", "scheme": "visa", "id": "25666304-81cc-46e3-b68b-5d60694a51d5", "approval_target": null, "external_identifier": null, "payment_account_reference": null }, "gift_card_redemptions": [ { "type": "gift-card-redemption", "id": "4d2c0c21-143e-455f-a1cb-f17f48bfd2cc", "status": "succeeded", "amount": 1000, "refunded_amount": 0, "gift_card_service_redemption_id": "23603110", "error_code": null, "raw_error_code": null, "raw_error_message": null, "gift_card": { "type": "gift-card", "id": "5c3ba1d8-f418-416f-9493-5abf23e0d05d", "bin": "628000", "sub_bin": "556", "last4": "8599" } }, { "type": "gift-card-redemption", "id": "95de3b3a-db97-4361-9463-ee4c35b469e3", "status": "succeeded", "amount": 299, "refunded_amount": 0, "gift_card_service_redemption_id": "23603111", "error_code": null, "raw_error_code": null, "raw_error_message": null, "gift_card": { "type": "gift-card", "id": "4b74e655-bf33-4952-83a3-149cfff43817", "bin": "628000", "sub_bin": "556", "last4": "3923" } } ], "instrument_type": "pan", "pending_review": false, "error_code": null, "raw_response_code": null, "raw_response_description": null, "auth_response_code": "00", "avs_response_code": "match", "cvv_response_code": "match", "payment_source": "ecommerce", "scheme_transaction_id": "016153570198200", "payment_service_transaction_id": "7419360017276878403814", "intent_outcome": "succeeded", "multi_tender": true ... } ``` ## Post-auth operations [Void](/reference/transactions/void-transaction) and [Capture](/reference/transactions/capture-transaction) APIs only apply to primary payment methods and do not affect gift card redemptions. # Decrypted Google Pay tokens Source: https://integration.wpay.com.au/guides/features/google-pay/decrypted-tokens Some merchants and platforms may have the option to decrypt a Google Pay token themselves. For this scenario, direct pass through of these decrypted Google Pay tokens to the API is supported using the [`network-token` method](/reference/transactions/new-transaction#body-payment-method). ```json Payment method theme={"system"} { "method": "network-token", "token": "4111111111111111", "expiration_date": "02/30", "cryptogram": "AAEBAwQjSQAAXX...", "eci": "05", "card_suffix": "1234", "card_scheme": "visa", "card_source": "google-pay", "cardholder_name": "John Smith" } ``` # Google Pay on Android with the SDK Source: https://integration.wpay.com.au/guides/features/google-pay/mobile The Android SDK supports native Google Pay out of the box. ## About this integration The Android SDK requires minimal configuration to get set up to process Google Pay. Unlike some other integrations it does not require you to sign up for a Google Developer account. ## Enable Google Pay To enable Google Pay, please reach out to your Wpay representative and provide them with the domain names where you want to use Google Pay. ## Test Google Pay To test Google Pay follow the [quick start guide](/guides/payments/android/quick-start/overview) to install the SDK into your app. Google Pay should automatically be displayed as a payment option. Please follow the Google Pay documentation with further guides on how to add [test cards](https://developers.google.com/pay/api/android/guides/resources/test-card-suite) for use in a sandbox environment. # Google Pay on Android without the SDK Source: https://integration.wpay.com.au/guides/features/google-pay/mobile-without-sdk It is possible to use Google Pay without the SDK. ## About this integration Even without the Android SDK, there is only minimal configuration required to get set up to process Google Pay. Unlike some other integrations it does not require you to sign up for a Google Developer account. ## Enable Google Pay To enable Google Pay, please reach out to your Wpay representative and provide them with the domain names where you want to use Google Pay. ## Integrate Google Pay **Java** To learn how to integrate Google Pay, following Google's [tutorial][tutorial] is recommended. The steps below highlight any differences and specifics for the API. ### Set gateway and merchant ID In step 2 of [Google's tutorial][tutorial] you are instructed to request a payment token for your provider. In this case, the value for `gateway` needs to be set to `gr4vy`, and the value of `gatewayMerchantId` needs to be set to `app.gr4vy.sandbox.[gr4vy_id].[merchant_account_id]` for your sandbox environment, and `app.gr4vy.[gr4vy_id].[merchant_account_id]` in production. ```java Java theme={"system"} private static JSONObject getGatewayTokenizationSpecification() throws JSONException { return new JSONObject() {{ put("type", "PAYMENT_GATEWAY"); put("parameters", new JSONObject() {{ put("gateway", "gr4vy"); put("gatewayMerchantId", "app.gr4vy.sandbox.wpay.merchant1"); }}); }}; } ``` ```kotlin Kotlin theme={"system"} private fun gatewayTokenizationSpecification(): JSONObject { return JSONObject().apply { put("type", "PAYMENT_GATEWAY") put("parameters", JSONObject(mapOf( "gateway" to "gr4vy", "gatewayMerchantId" to "app.gr4vy.sandbox.wpay.merchant1"))) } } ``` > [!NOTE] > > The instance ID is the unique identifier for the deployment of the system and is included in every API call. > Together with the environment (sandbox or production) it is used to connect to the right APIs, as well as dashboard. > > The possible values for your ID are one of `wpay`, `wpay1`, `wpay2`, or `wpay3`. You are informed > in which instance your account has been set up. ### Create a transaction In step 9 of Google's tutorial, you have a `paymentData` that needs to be sent to the gateway. You can send this object to the [`POST /transactions`](/reference/transactions/new-transaction#body-payment-method) endpoint either directly or via your server. The payment method used needs to contain the data from the Google Pay payload. ```json Payment method data theme={"system"} { "amount": 1299, "currency": "AUD", "country": "AU", "payment_method": { "method": "googlepay", "token": "[paymentData.paymentMethodData.tokenizationData.token]", "card_suffix": "[paymentData.paymentMethodData.info.cardDetails]", "card_scheme": "[paymentData.paymentMethodData.info.cardNetwork]", "redirect_url": "https://example.com/callback" } } ``` > [!NOTE] > > Always provide a `redirect_url`. When Google Pay returns a card saved in the buyer's Google Account > (`PAN_ONLY`), Gr4vy processes it like any other card, so > [3-D Secure can apply](/guides/features/google-pay/overview#3-d-secure). If the transaction returns the > status `buyer_approval_pending`, send the buyer to the `payment_method.approval_url`, even if no > challenge is shown. Otherwise, the transaction fails when the approval expires. > > After 3-D Secure, the `redirect_url` is appended with both a transaction ID and status (for example > `https://example.com/callback?gr4vy_transaction_id=123&gr4vy_transaction_status=capture_succeeded`). **React Native** In React Native you can use the [@google/react-native-make-payment](https://github.com/google-pay/react-native-make-payment) package to integrate Google Pay. This package uses the W3C Payment Request API. ### Install the package Install the package using npm: ```bash theme={"system"} npm install @google/react-native-make-payment ``` ### Configure the payment request Set up the Google Pay payment request with your merchant identifier and gateway ID. Set the `gateway` value to `gr4vy` and the `gatewayMerchantId` to your environment, for example `app.gr4vy.sandbox.[gr4vy_id].[merchant_account_id]`. ```js theme={"system"} import { PaymentRequest, type W3CGooglePayPaymentMethod, type GooglePayPaymentDataRequest } from '@google/react-native-make-payment'; // Configure Google Pay request const googlePayRequest: GooglePayPaymentDataRequest = { apiVersion: 2, apiVersionMinor: 0, allowedPaymentMethods: [ { type: 'CARD', parameters: { allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'], allowedCardNetworks: ['VISA', 'MASTERCARD'], }, tokenizationSpecification: { type: 'PAYMENT_GATEWAY', parameters: { gateway: 'gr4vy', gatewayMerchantId: 'app.wpay2.sandbox.example.default', }, }, }, ], merchantInfo: { merchantName: 'Example Merchant', }, }; // Configure payment details const paymentDetails = { total: { amount: { currency: 'AUD', value: '12.99', }, }, }; // Create payment methods array const paymentMethods: W3CGooglePayPaymentMethod[] = [ { supportedMethods: 'google_pay', data: googlePayRequest, }, ]; ``` > [!NOTE] > > The instance ID is the unique identifier for the deployment of the system and is included in every API call. > Together with the environment (sandbox or production) it is used to connect to the right APIs, as well as dashboard. > > The possible values for your ID are one of `wpay`, `wpay1`, `wpay2`, or `wpay3`. You are informed > in which instance your account has been set up. ### Process the payment Create a payment request and check if Google Pay is available before showing the payment sheet. ```js theme={"system"} const paymentRequest = new PaymentRequest(paymentMethods, paymentDetails); paymentRequest.canMakePayment() .then((canMakePayment) => { if (canMakePayment) { paymentRequest.show() .then((response) => { // Extract the token from the response const token = response.paymentMethodData.tokenizationData.token; // Send to Gr4vy API return fetch('https://api.example.gr4vy.app/transactions', { method: 'POST', headers: { 'Authorization': 'Bearer [TOKEN]', 'Content-Type': 'application/json', }, body: JSON.stringify({ amount: 1299, currency: 'AUD', payment_method: { method: 'googlepay', token: token, redirect_url: 'https://example.com/callback', }, }), }) .then(res => res.json()) .then((transaction) => { console.log('Payment successful', transaction); return transaction; }) .catch((error) => { console.error('Payment failed', error); throw error; }); }) .catch((error) => { // Handle payment sheet cancellation or errors console.error('Payment sheet error', error); }); } else { // Google Pay unavailable console.log('Google Pay is not available'); } }) .catch((error) => { // Handle canMakePayment errors console.error('Error checking Google Pay availability', error); }); ``` > [!NOTE] > > Always provide a `redirect_url`, which can be an application URL or URN. When Google Pay returns a > card saved in the buyer's Google Account (`PAN_ONLY`), [3-D Secure can apply](/guides/features/google-pay/overview#3-d-secure). > If the transaction returns the status `buyer_approval_pending`, send the buyer to the > `payment_method.approval_url`, even if no challenge is shown. Otherwise, the transaction fails when > the approval expires. > > After 3-D Secure, the `redirect_url` is appended with both a transaction ID and status (for example > `https://example.com/callback?gr4vy_transaction_id=123&gr4vy_transaction_status=capture_succeeded`). ## Test Google Pay Please follow the Google Pay documentation with further guides on how to add [test cards](https://developers.google.com/pay/api/android/guides/resources/test-card-suite) for use in a sandbox environment. ## Common issues > **Google Pay shows OR\_BIBED\_11 error in production** > > The OR\_BIBED\_11 error typically occurs due to configuration mismatches between your app and Google Pay settings. Check the following: > > ### SHA-256 certificate mismatch > > 1. In [Google Play Console](https://play.google.com/console), go to **App Integrity** → **App signing key certificate** > 2. Copy the SHA-256 certificate fingerprint > 3. In [Google Pay & Wallet Console](https://pay.google.com/business/console), go to **Your Merchant** → **Android apps** > 4. Verify the SHA-256 matches exactly > 5. Ensure the status shows as **Approved** > > ### Google Pay merchant approval > > Confirm your app entry in the [Google Pay & Wallet Console](https://pay.google.com/business/console) is approved (not pending). > > ### AndroidManifest metadata > > Ensure the required metadata is present in your AndroidManifest.xml: > > ```xml theme={"system"} > android:name="com.google.android.gms.wallet.api.enabled" > android:value="true" /> > ``` > > If any of these don't match (especially the SHA-256 certificate), updating the configuration typically resolves this error. [tutorial]: https://developers.google.com/pay/api/android/guides/tutorial # Google Pay Source: https://integration.wpay.com.au/guides/features/google-pay/overview ![Google Pay](https://mintcdn.com/wpay/c50rx3S48hHhNNxb/assets/images/google-pay/hero.png?fit=max\&auto=format\&n=c50rx3S48hHhNNxb\&q=85\&s=4b2ea055fbea7ab66a19287acdd2fbd0) Google Pay is supported both on the web and in Android apps. In both environments, Google Pay can be integrated using the SDK and without the SDK. Both `PAN_ONLY` and `CRYPTOGRAM_3DS` authentication methods are supported for Google Pay. * `PAN_ONLY` - the card is stored on file within your customer's Google Account and not bound to an Android device. * `CRYPTOGRAM_3DS` - the payment credentials are bound to an Android device. On a Google Pay transaction, `payment_method.method` describes how the buyer paid, while `payment_method.scheme` holds the brand of the underlying card, for example `amex`. Depending on the card, Google Pay returns either a device token with a cryptogram or the card's own number without a cryptogram. Google calls these `CRYPTOGRAM_3DS` and `PAN_ONLY`. A `CRYPTOGRAM_3DS` payment has a method of `googlepay`, and a `PAN_ONLY` payment has a method of `googlepay_pan_only`. The system passes either kind to your card connection, and the connection and issuer can treat them differently. ## Web (Embed) On the web, Google Pay can be easily integrated both with and without Embed. In both offerings, the need to sign up for a Google Developer account has been removed, reducing additional financial and maintenance costs. | | [Using SDK (Embed)][web] | [Without SDK][web-without] | | - | - | - | | Google Developer account required? | No | No | | Customization & Theming? | Limited | Full | | Integrated with Checkout Flow? | Yes | Optional | > [!TIP] > > An `OR_BIBED` error from Google Pay on the web usually means the domain passed to the API doesn't > match the page's domain. See [Set the merchant info](/guides/features/google-pay/web-without-sdk#set-the-merchant-info). ## Mobile (Android) On Android, Google Pay can be easily integrated both with and without the SDK. In both offerings, the need to sign up for a Google Developer account has been removed, reducing additional financial and maintenance costs. | | [Using SDK][mobile] | [Without SDK][mobile-without] | | - | - | - | | Google Developer account required? | No | No | | Customization & Theming? | Limited | Full | | Integrated with Checkout Flow? | Yes | Optional | ## 3-D Secure Google Pay can return two kinds of credentials: * A device token (`CRYPTOGRAM_3DS`), which carries a cryptogram. Gr4vy doesn't run 3-D Secure for these. * A card saved in the buyer's Google Account (`PAN_ONLY`), which is common on the web. Gr4vy processes these like any other card, so 3-D Secure can apply when it's turned on for the connection. When 3-D Secure applies, the transaction returns the status `buyer_approval_pending`. Send the buyer to the `payment_method.approval_url`, even if no challenge is shown, and include a `redirect_url` when you create the transaction. If the buyer isn't sent to the approval URL, the transaction fails when the approval expires. Embed handles this for you. For integrations without Embed, see [handle a redirect](/guides/payments/direct-api/quick-start/handle-redirect). ## Recurring payments > [!NOTE] > > This is in limited release and is being made generally available soon. Recurring payments are supported on Google Pay in a similar way to how [recurring payments][recurring-payments] are supported for regular card payments. 1. Take the first payment with the Google Pay token and `store: true`, with `merchant_initiated` and `is_subsequent_payment` set to `false` and a `payment_source` such as `recurring`. This stores the Google Pay payment method. 2. Charge later payments with the stored payment method `id`, the same `payment_source`, and `merchant_initiated` and `is_subsequent_payment` set to `true`. A stored Google Pay payment method has no cryptogram, because the cryptogram only exists for the original payment on the buyer's device. Later payments rely on the recurring flags and the scheme transaction ID from the first payment instead, so payment services may decline a later payment that's sent with the flags of a first payment. [web]: /guides/features/google-pay/web [web-without]: /guides/features/google-pay/web-without-sdk [mobile]: /guides/features/google-pay/mobile [mobile-without]: /guides/features/google-pay/mobile-without-sdk [recurring-payments]: /guides/features/recurring-payments # Google Pay on web with Embed Source: https://integration.wpay.com.au/guides/features/google-pay/web Google Pay is the faster, more secure way to pay online using cards saved in your customer's Google Account. Google Pay is supported out of the box in Embed with minimal configuration. ## About this integration Embed requires minimal configuration to get set up to process Google Pay. Unlike some other integrations it does not require you to sign up for a Google Developer account. ## Enable Google Pay To enable Google Pay, please reach out to your Wpay representative and provide them with the domain names where you want to use Google Pay. ## Test Google Pay To test Google Pay visit the site that has Embed loaded on it. Google Pay should automatically be displayed. When Embed runs with the `environment` option set to `sandbox`, Google Pay runs in Google's test environment. Embed defaults to `production`, so set this option when you test. Please follow the Google Pay documentation with further guides on how to add [test cards](https://developers.google.com/pay/api/web/guides/resources/test-card-suite) for use in a sandbox environment. ## Common issues > **Google Pay pop up shows a OR\_BIBID\_15 error** > > You may have set the `Cross-Origin-Opener-Policy` header on the page on which Google Pay is loaded. Please ensure you set this to allow popups, for example `same-origin-allow-popups`. # Google Pay on web without Embed Source: https://integration.wpay.com.au/guides/features/google-pay/web-without-sdk It is possible to use Google Pay on the web without the SDK. To learn how to integrate Google Pay, following Google's [tutorial][tutorial] is recommended. The steps below highlight any differences and specifics for the API. ## About this integration Even without Embed, there is only minimal configuration required to get set up to process Google Pay. There is no need to sign up for a Google Developer account with this integration. > [!NOTE] > > For Android apps which use this option with `WebViews`, please note that platform specific > Android Google Pay APIs must be invoked. > Please refer to [Google's FAQ](https://developers.google.com/pay/api/web/support/faq) for more > information. ## Enable Google Pay To enable Google Pay, please reach out to your Wpay representative and provide them with the domain names where you want to use Google Pay. ## Integrate Google Pay The following steps highlight the integration points with the system using Google's [tutorial][tutorial] for Google Pay. ### Fetch a Google Pay session First, before you can render a Google Pay button, you need to call the API to get a token and your gateway ID. Call the [`POST /digital-wallets/google/session`](/reference/digital-wallets/get-google-pay-session) API with the domain you want to host Google Pay on. This needs to be the full domain including any subdomains like `www.`. ```json Request theme={"system"} { "origin_domain": "www.example.com" } ``` This returns your gateway merchant ID as well as a JWT that authorizes running on your domain. ```json Response theme={"system"} { "gateway_merchant_id": "app.gr4vy.sandbox.example.default", "token": "eyJhbGciOiJFUzI1NiIsInR5cCI....NwRL9ZZg0N--ZyP6I0dDfHHoWhVkrHpQ" } ``` ### Set gateway and merchant ID Next, in step 2 of [Google's tutorial][tutorial] you are instructed to request a payment token for your provider. In our case, the value for `gateway` needs to be set to `gr4vy`, and the value of `gatewayMerchantId` needs to be set to the value you received from our session API in the previous step. ```js theme={"system"} const tokenizationSpecification = { type: "PAYMENT_GATEWAY", parameters: { gateway: "gr4vy", gatewayMerchantId: "app.gr4vy.sandbox.wpay.merchant1", }, }; ``` > [!NOTE] > > The instance ID is the unique identifier for the deployment of the system and is included in every API call. > Together with the environment (sandbox or production) it is used to connect to the right APIs, as well as dashboard. > > The possible values for your ID are one of `wpay`, `wpay1`, `wpay2`, or `wpay3`. You are informed > in which instance your account has been set up. ### Set the merchant info Next, in step 8, you need to set a `merchantInfo` object. This authorizes accepting Google Pay on your domain. When setting the `merchantInfo` object, make sure to set the `token` returned from the session API, your merchant name, the merchant ID, and the full domain you passed to the session API (including any subdomains like `www.`) ```js theme={"system"} const merchantInfo = { authJwt: "eyJhbGciOiJFUzI1NiIsInR5cCI....NwRL9ZZg0N--ZyP6I0dDfHHoWhVkrHpQ", merchantId: "BCR2DN4T7C3KX6DY", // This identifies our system merchantName: "Example App", merchantOrigin: "www.example.com", }; ``` > [!NOTE] > > `merchantId=BCR2DN4T7C3KX6DY` is the same for production and sandbox > [!WARNING] > > **`OR_BIBED_11`** / **`OR_BIBED_6`** - If you were to encounter any kind of > `OR_BIBED` error you likely did not pass the right domain to our API, or you > did not pass the same domain to our session API and the `merchantInfo` object. ### Create a transaction In step 10 of Google's tutorial, within the `onPaymentAuthorized` function you will have a `paymentData` that needs to be sent to the gateway. You can send this object to our [`POST /transactions`](/reference/transactions/new-transaction#body-payment-method) endpoint either directly or via your server. The payment method used needs to contain the data from the Google Pay payload. ```json Payment method data theme={"system"} { "amount": 1299, "currency": "AUD", "country": "AU", "payment_method": { "method": "googlepay", "token": "[paymentData.paymentMethodData.tokenizationData.token]", "card_suffix": "[paymentData.paymentMethodData.info.cardDetails]", "card_scheme": "[paymentData.paymentMethodData.info.cardNetwork]", "redirect_url": "https://example.com/callback" } } ``` ### Handle 3-D Secure Always provide a `redirect_url`. When Google Pay returns a card saved in the buyer's Google Account (`PAN_ONLY`), Gr4vy processes it like any other card, so [3-D Secure can apply](/guides/features/google-pay/overview#3-d-secure). If the transaction returns the status `buyer_approval_pending`, redirect the buyer to the `payment_method.approval_url`, even if no challenge is shown. Otherwise, the transaction fails when the approval expires. After 3-D Secure, the buyer returns to the `redirect_url` with a transaction ID and status appended (for example `https://example.com/callback?gr4vy_transaction_id=123&gr4vy_transaction_status=capture_succeeded`). See [handle a redirect](/guides/payments/direct-api/quick-start/handle-redirect). ## Test Google Pay To test Google Pay visit the site that has your integration loaded on it. Please follow the Google Pay documentation with further guides on how to add [test cards](https://developers.google.com/pay/api/web/guides/resources/test-card-suite) for use in a sandbox environment. [tutorial]: https://developers.google.com/pay/api/web/guides/tutorial # Linking guests buyer details Source: https://integration.wpay.com.au/guides/features/guest-checkout/linking Use guest buyer details in combination with stored buyer records and their payment methods In some situations you may want to use guest buyer details but also use a stored buyer's payment methods. For example, you may manage all the buyer billing and shipping details in your ecommerce platform and do not want to have to keep this in sync with the API. Yet, you also would like to offer a card-on-file experience at checkout where a buyer can use their stored payment methods. In these situations it makes sense to create a buyer record via the API with little to no details, passing all billing and shipping details as guest details, while using the buyer record just to link payment methods and transactions. Once created, you can use the buyer's ID or external identifier to link a payment to that buyer, while only the guest details are passed to the payment services. ```json API theme={"system"} { "amount": 1299, "currency": "AUD", "payment_method": { // This works with any payment method, // as well as checkout sessions ... }, // The buyer to link the payment (and any to-be-stored payment method) to "buyer_id": "e84fbc2a-3ee9-4ed5-9fc3-00c8e61fd8c5", // Alternatively use the buyer external identifier "buyer_external_identifier": "buyer-12345", // The buyer information used for this transaction. "buyer": {...} } ``` ```js Embed (Node) theme={"system"} setup({ ... // The buyer to link the payment (and any to-be-stored payment method) to. // This will also be used to load any stored payment methods. buyerId: "e84fbc2a-3ee9-4ed5-9fc3-00c8e61fd8c5", // Alternatively use the buyer external identifier buyerExternalIdentifier: "buyer-12345", // The buyer information used for this transaction. buyer: {...} }) ``` ```jsx Embed (React) theme={"system"} ``` ```swift iOS theme={"system"} Gr4vy.init( ... // The buyer to link the payment (and any to-be-stored payment method) to. // This will also be used to load any stored payment methods. buyerId: "e84fbc2a-3ee9-4ed5-9fc3-00c8e61fd8c5" // Alternatively use the buyer external identifier buyerExternalIdentifier: "buyer-12345" // The buyer information used for this transaction. buyer: Gr4vyBuyer(...) ) ``` ```java Android theme={"system"} gr4vySDK.launch( ... // The buyer to link the payment (and any to-be-stored payment method) to. // This will also be used to load any stored payment methods. buyerId="e84fbc2a-3ee9-4ed5-9fc3-00c8e61fd8c5" // Alternatively use the buyer external identifier buyerExternalIdentifier="buyer-12345" // The buyer information used for this transaction. buyer = Gr4vyBuyer(...) ) ``` ```js React Native theme={"system"} EmbedReactNative.showPaymentSheet({ ... // The buyer to link the payment (and any to-be-stored payment method) to. // This will also be used to load any stored payment methods. buyerId="e84fbc2a-3ee9-4ed5-9fc3-00c8e61fd8c5", // Alternatively use the buyer external identifier buyerExternalIdentifier="buyer-12345", // The buyer information used for this transaction. buyer: {...}, }) ``` ## Limitations There are a few limitations to linking in-line buyer details to a stored buyer. * Any buyer ID and buyer external identifier provided need to match the same buyer record or they may be ignored * Any stored buyer details (name, email, addresses, etc) are ignored when guest checkout is provided. Guest data is not merged into any existing data, instead the guest data is the only data used. # Guest checkout Source: https://integration.wpay.com.au/guides/features/guest-checkout/overview Create transactions without a creating a buyer first It is possible to provide buyer details, including the shipping and billing details, without the need to [create a buyer resource](/reference/buyers/new-buyer). You can send the buyer details directly when the transaction is requested, either to the API, Embed, or Secure Fields. ```json API theme={"system"} { "amount": 1299, "currency": "AUD", "payment_method": { // This works with any payment method, // as well as checkout sessions ... }, "buyer": { "external_identifier": "my-unique-identifier", "billing_details": { "first_name": "John", "last_name": "Smith", "address": { "postal_code": "789123" } }, "shipping_details": { "first_name": "John", "last_name": "Smith", "address": { "postal_code": "789123" } } } } ``` ```js Embed (Node) theme={"system"} setup({ ... buyer: { externalIdentifier: 'my-unique-identifier', billingDetails: { firstName: 'John', lastName: 'Smith', address: { postalCode: '789123', }, taxId: { value: '123456', kind: 'gb.vat', }, }, shippingDetails: { firstName: 'John', lastName: 'Smith', address: { postalCode: '789123', }, }, } }) ``` ```jsx Embed (React) theme={"system"} ``` ```swift iOS theme={"system"} Gr4vy.init( ... buyer: Gr4vyBuyer( externalIdentifier: "my-unique-identifier", billingDetails: Gr4vyBillingDetails( firstName: "John", lastName: "Smith", address: Gr4vyAddress( postalCode: "789123" ), taxId: Gr4vyTaxId( value: "123456", kind: "gb.vat" ) ), shippingDetails: Gr4vyShippingDetails( firstName: "John", lastName: "Smith", address: Gr4vyAddress( postalCode: "789123" ), ) ) ) ``` ```java Android theme={"system"} gr4vySDK.launch( ... buyer = Gr4vyBuyer( externalIdentifier = "my-unique-identifier", billingDetails = Gr4vyBillingDetails( firstName = "John", lastName = "Smith", address = Gr4vyAddress( postalCode = "789123" ), taxId = Gr4vyTaxId( value = "123456", kind = "gb.vat" ) ), shippingDetails = Gr4vyShippingDetails( firstName = "John", lastName = "Smith", address = Gr4vyAddress( postalCode = "789123" ), ) ) ) ``` ```js React Native theme={"system"} EmbedReactNative.showPaymentSheet({ ... buyer: { externalIdentifier: 'my-unique-identifier', billingDetails: { firstName: 'John', lastName: 'Smith', address: { postalCode: '789123', }, taxId: { value: '123456', kind: 'gb.vat', }, }, shippingDetails: { firstName: 'John', lastName: 'Smith', address: { postalCode: '789123', }, }, }, }) ``` ## Limitations There are a few limitations to using in-line buyer details like this, rather than using a stored buyer record. * A guest buyer cannot use a payment method which is assigned to another buyer resource. * A guest buyer cannot redeem gift cards which are assigned to another buyer resource. * A buyer resource can not be created when using in-line buyer data. Please use the [new buyers endpoint](/reference/buyers/new-buyer) instead. > [!NOTE] > > The buyer ID and buyer external identifier properties can be passed to the API and SDKs to apply guest buyer details while also using stored > payment methods. Please see the documentation on [linking](./linking) for more details. # Incremental authorization Source: https://integration.wpay.com.au/guides/features/incremental-authorization/overview Increase the amount of an existing card authorization when the final order total turns out higher than the amount you first authorized. Most card payments happen in two stages. First, an **authorization** reserves the funds with the customer's bank, but no money moves. Then, a **capture** requests those funds for settlement. Sometimes the final amount isn't known when you authorize. Orders with variable-weight items, product substitutions, or extra services often end up higher than the estimate. Incremental authorization lets you ask the customer's bank to raise the existing authorization before you capture, so the higher amount is reserved and you can capture all of it. ## Incremental authorization compared to over capture Over capture lets you capture more than the authorized amount without asking the bank again. Card schemes and acquirers cap how far over the authorized amount you can capture, often to a small percentage. Incremental authorization has no such fixed cap, because the bank approves each increase before you capture it. | | Over capture | Incremental authorization | | - | - | - | | Asks the bank again | No | Yes, for each increase | | How much you can add | Limited by the scheme and acquirer | Whatever the bank approves | | Can be declined | Only at capture | When you request the increase | ## How it works 1. **Authorize the payment** with `intent` set to `authorize`, for the amount you estimate. Set `is_amount_estimated` to `true` on [connectors that require it](#supported-connectors). 2. **Increase the authorization** when the final amount is known. Call [increment authorization](/reference/transactions/increment-transaction-authorization) with the amount to add. 3. **Capture the payment** for up to the new authorized amount using [capture transaction](/reference/transactions/capture-transaction). You can increase the same authorization more than once. Each request adds to the current authorized amount. ### Authorize with an estimated amount Some payment services only accept an increase if the original authorization was flagged as an estimate. Set `is_amount_estimated` to `true` when you [create the transaction](/reference/transactions/new-transaction). The examples leave out the payment method and other fields for brevity. ```csharp C# theme={"system"} using Gr4vy; using Gr4vy.Models.Components; var sdk = new Gr4vySDK( id: "example", server: SDKConfig.Server.Sandbox, bearerAuthSource: Auth.WithToken(privateKey), merchantAccountId: "default" ); var res = await sdk.Transactions.CreateAsync( transactionCreate: new TransactionCreate() { Amount = 10000, Currency = "AUD", Intent = "authorize", IsAmountEstimated = true, } ); // handle response ``` ```go Go theme={"system"} package main import( "context" "os" gr4vygo "github.com/gr4vy/gr4vy-go" "github.com/gr4vy/gr4vy-go/models/components" "log" ) func main() { ctx := context.Background() s := gr4vy.New( gr4vy.WithID("example"), gr4vy.WithServer(gr4vy.ServerSandbox), gr4vy.WithSecuritySource(withToken), gr4vy.WithMerchantAccountID("default"), ) res, err := s.Transactions.Create(ctx, components.TransactionCreate{ Amount: 10000, Currency: "AUD", Intent: components.TransactionIntentAuthorize.ToPointer(), IsAmountEstimated: gr4vy.Pointer(true), }, nil, nil, nil) if err != nil { log.Fatal(err) } if res != nil { // handle response } } ``` ```java Java theme={"system"} package hello.world; import com.gr4vy.sdk.Gr4vy; import com.gr4vy.sdk.models.components.TransactionCreate; import com.gr4vy.sdk.models.components.TransactionIntent; import com.gr4vy.sdk.models.errors.*; import com.gr4vy.sdk.models.operations.CreateTransactionResponse; import java.lang.Exception; public class Application { public static void main(String[] args) throws Exception { Gr4vy sdk = Gr4vy.builder() .id("example") .server(AvailableServers.SANDBOX) .merchantAccountId("default") .securitySource(new BearerSecuritySource.Builder(privateKey).build()) .build(); CreateTransactionResponse res = sdk.transactions().create() .transactionCreate(TransactionCreate.builder() .amount(10000L) .currency("AUD") .intent(TransactionIntent.AUTHORIZE) .isAmountEstimated(true) .build()) .call(); if (res.transaction().isPresent()) { System.out.println(res.transaction().get()); } } } ``` ```php PHP theme={"system"} declare(strict_types=1); require 'vendor/autoload.php'; use Gr4vy; $sdk = Gr4vy\SDK::builder() ->setId('example') ->setServer('sandbox') ->setSecuritySource(Auth::withToken($privateKey)) ->setMerchantAccountId('default') ->build(); $transactionCreate = new Gr4vy\TransactionCreate( amount: 10000, currency: 'AUD', intent: 'authorize', isAmountEstimated: true, ); $response = $sdk->transactions->create( transactionCreate: $transactionCreate ); if ($response->transaction !== null) { // handle response } ``` ```python Python theme={"system"} from gr4vy import Gr4vy import os with Gr4vy( id="example", server="sandbox", merchant_account_id="default", bearer_auth=auth.with_token(open("./private_key.pem").read()) ) as g_client: res = g_client.transactions.create(amount=10000, currency="AUD", intent="authorize", is_amount_estimated=True) # Handle response print(res) ``` ```typescript TypeScript theme={"system"} import { Gr4vy, withToken } from "@gr4vy/sdk"; import fs from "fs"; const gr4vy = new Gr4vy({ id: "example", server: "sandbox", merchantAccountId: "default", bearerAuth: withToken({ privateKey: fs.readFileSync("private_key.pem", "utf8"), }), }); async function run() { const result = await gr4vy.transactions.create({ amount: 10000, currency: "AUD", intent: "authorize", isAmountEstimated: true, }); console.log(result); } run(); ``` The flag can carry a cost at some payment services, so only set it on transactions you might increase. ### Increase the authorization Send the amount to add, in the smallest currency unit, to [increment authorization](/reference/transactions/increment-transaction-authorization). The `amount` is the increase, not the new total. An increase of `1299` on an authorization of `10000` asks the bank for `11299` in total. ```csharp C# theme={"system"} using Gr4vy; using Gr4vy.Models.Components; var sdk = new Gr4vySDK( id: "example", server: SDKConfig.Server.Sandbox, bearerAuthSource: Auth.WithToken(privateKey), merchantAccountId: "default" ); var res = await sdk.Transactions.IncrementAuthorizationAsync( transactionId: "7099948d-7286-47e4-aad8-b68f7eb44591", transactionAuthorizationIncrementCreate: new TransactionAuthorizationIncrementCreate() { Amount = 1299, } ); // handle response ``` ```go Go theme={"system"} package main import( "context" "os" gr4vygo "github.com/gr4vy/gr4vy-go" "github.com/gr4vy/gr4vy-go/models/components" "log" ) func main() { ctx := context.Background() s := gr4vy.New( gr4vy.WithID("example"), gr4vy.WithServer(gr4vy.ServerSandbox), gr4vy.WithSecuritySource(withToken), gr4vy.WithMerchantAccountID("default"), ) res, err := s.Transactions.IncrementAuthorization(ctx, "7099948d-7286-47e4-aad8-b68f7eb44591", components.TransactionAuthorizationIncrementCreate{ Amount: 1299, }, nil, nil) if err != nil { log.Fatal(err) } if res != nil { // handle response } } ``` ```java Java theme={"system"} package hello.world; import com.gr4vy.sdk.Gr4vy; import com.gr4vy.sdk.models.components.TransactionAuthorizationIncrementCreate; import com.gr4vy.sdk.models.errors.*; import com.gr4vy.sdk.models.operations.IncrementTransactionAuthorizationResponse; import java.lang.Exception; public class Application { public static void main(String[] args) throws Exception { Gr4vy sdk = Gr4vy.builder() .id("example") .server(AvailableServers.SANDBOX) .merchantAccountId("default") .securitySource(new BearerSecuritySource.Builder(privateKey).build()) .build(); IncrementTransactionAuthorizationResponse res = sdk.transactions().incrementAuthorization() .transactionId("7099948d-7286-47e4-aad8-b68f7eb44591") .transactionAuthorizationIncrementCreate(TransactionAuthorizationIncrementCreate.builder() .amount(1299L) .build()) .call(); if (res.transactionAuthorizationIncrement().isPresent()) { System.out.println(res.transactionAuthorizationIncrement().get()); } } } ``` ```php PHP theme={"system"} declare(strict_types=1); require 'vendor/autoload.php'; use Gr4vy; $sdk = Gr4vy\SDK::builder() ->setId('example') ->setServer('sandbox') ->setSecuritySource(Auth::withToken($privateKey)) ->setMerchantAccountId('default') ->build(); $transactionAuthorizationIncrementCreate = new Gr4vy\TransactionAuthorizationIncrementCreate( amount: 1299, ); $response = $sdk->transactions->incrementAuthorization( transactionId: '7099948d-7286-47e4-aad8-b68f7eb44591', transactionAuthorizationIncrementCreate: $transactionAuthorizationIncrementCreate ); if ($response->transactionAuthorizationIncrement !== null) { // handle response } ``` ```python Python theme={"system"} from gr4vy import Gr4vy import os with Gr4vy( id="example", server="sandbox", merchant_account_id="default", bearer_auth=auth.with_token(open("./private_key.pem").read()) ) as g_client: res = g_client.transactions.increment_authorization(transaction_id="7099948d-7286-47e4-aad8-b68f7eb44591", amount=1299) # Handle response print(res) ``` ```typescript TypeScript theme={"system"} import { Gr4vy, withToken } from "@gr4vy/sdk"; import fs from "fs"; const gr4vy = new Gr4vy({ id: "example", server: "sandbox", merchantAccountId: "default", bearerAuth: withToken({ privateKey: fs.readFileSync("private_key.pem", "utf8"), }), }); async function run() { const result = await gr4vy.transactions.incrementAuthorization({ amount: 1299, }, "7099948d-7286-47e4-aad8-b68f7eb44591"); console.log(result); } run(); ``` The response reports the outcome and returns the updated transaction. ```json theme={"system"} { "type": "transaction-authorization-increment", "status": "succeeded", "code": null, "raw_response_code": null, "raw_response_description": null, "transaction": { "type": "transaction", "id": "7099948d-7286-47e4-aad8-b68f7eb44591", "status": "authorization_succeeded", "amount": 10000, "authorized_amount": 11299, "captured_amount": 0 } } ``` The transaction's `amount` stays at the original value. The `authorized_amount` reflects the new total, and you can capture up to that amount. ## Outcomes The `status` in the response is one of the following: | Status | Meaning | `authorized_amount` | | - | - | - | | `succeeded` | The bank approved the increase. | Raised to the new total. | | `failed` | The bank declined the increase, or the payment service returned an error. `code`, `raw_response_code`, and `raw_response_description` explain why. | Unchanged. | | `pending` | The payment service accepted the request, but the outcome isn't known yet. | Unchanged until the payment service confirms the increase. | A failed or pending increase doesn't affect the original authorization. It stays valid, and you can still capture up to the previous authorized amount. > [!WARNING] > > Treat `pending` as not yet approved. Don't capture the increased amount until the `authorized_amount` on the transaction reflects it. ## Requirements An increase is only sent to the payment service when all of the following are true: * The transaction status is `authorization_succeeded`. A transaction that is captured, voided, or in any other status returns a `400` `bad_request` error with a detail of type `not_valid_status`. * The transaction was processed by a payment service that supports incremental authorization. Any other payment service returns a `400` error saying the `incremental_authorization` feature is not supported. 3-D Secure is never performed on an increase. The endpoint accepts an `Idempotency-Key` header, so you can retry a request safely. See [idempotent requests](/guides/api/idempotent-requests). ## Supported connectors | Connector | Requires `is_amount_estimated` | Notes | | - | - | - | | [Adyen](/connections/payments/adyen-card) | Yes | An increase always returns `pending` first. Adyen confirms the outcome later. | | [Chase Orbital](/connections/payments/chaseorbital-card) | Yes | | | [Checkout.com](/connections/payments/checkoutcom-card) | Yes | | | [Cybersource](/connections/payments/cybersource-card) | No | Not supported on American Express. An increase on an American Express card fails with the `unsupported_scheme` code. | | [Mastercard Gateway](/connections/payments/mastercard-card) | Yes | | | [Shift4](/connections/payments/shift4-card) | Yes | Availability varies by payment method and region. See the Shift4 page for details. | | [Card simulator](/connections/payments/mock-card) | No | Sandbox only. See [Testing](#testing). | Each connector page lists incremental authorization under its capabilities. ## Webhooks and transaction events When an increase succeeds, Gr4vy sends a `transaction.modified` webhook with the updated transaction. No webhook is sent for a failed or pending increase. See [webhook events](/guides/features/webhooks/events). Each increase is recorded on the transaction's history in the dashboard and in [list transaction events](/reference/transactions/list-transaction-events): * The increment request and response. * The request Gr4vy sent to the payment service, and its response. * An **authorization increment succeeded** event, with the previous and new authorized amounts, or an **authorization increment failed** event, with the error code and the payment service's response code and description. When a pending increase is later confirmed by the payment service, the succeeded or failed event is added at that point. ## Testing In sandbox, the [card simulator](/connections/payments/mock-card) uses the increase `amount` to simulate each outcome: | Increase `amount` | Outcome | | - | - | | `200` | `succeeded` | | `250` | `pending` | | `300` | `failed`, declined | | Any other amount | `failed`, service error | # Multi captures Source: https://integration.wpay.com.au/guides/features/multi-captures/overview Capture a single authorization in more than one step, drawing down the balance as you fulfill an order over time. Most card payments happen in two stages. First, an **authorization** reserves the funds with the customer's bank, but no money moves. Then, a **capture** requests those funds for settlement. With a single capture, you capture the authorization once. If you capture less than the authorized amount, the remainder is released back to the customer and can't be claimed again. Multi captures let you claim a single authorization across more than one capture, over time. The authorization behaves like a balance you draw down until the order is complete. This suits orders fulfilled in parts, for example, when items ship separately or when an out-of-stock product becomes available later. ## Single capture compared to multi captures | | Single capture | Multi captures | | - | - | - | | Captures per payment | One | Many | | Capturing part of the amount | Supported, but the rest is released | Supported, and the rest stays available | | Best suited to | Orders shipped in full, in one go | Orders fulfilled in parts or over time | ## How it works 1. **Authorize the payment** for the full expected amount. 2. **Capture as you fulfil.** Each time you're ready to claim part of the balance, submit a [capture](/reference/transactions/capture-transaction) for that amount. You can attach the relevant order line items and [shipment tracking](/guides/features/shipment-tracking/overview) details to each capture, and add your own identifier, so each claim reflects what was fulfilled and reconciles against your order systems. 3. **Close the payment.** Mark the last capture as `final`. After the final capture, no further captures can be taken against the authorization, and any remaining authorized amount is released back to the customer where possible. Each capture is recorded individually, so you keep a clear breakdown of what was claimed and when. ## Tracking captures Each capture has its own status as it progresses, and a webhook with a matching `name` is sent each time a capture reaches that status: * **Pending** (`capture.pending`) — the capture has been submitted and is awaiting confirmation. * **Succeeded** (`capture.succeeded`) — the funds were claimed successfully. * **Declined** (`capture.declined`) — the bank or payment provider declined the capture. * **Failed** (`capture.failed`) — the capture couldn't be completed because of an error. These `capture.*` events are sent per individual capture, so you receive one for each capture against a transaction. They're distinct from the transaction-level `transaction.captured` and `transaction.capture-pending` events, which reflect the transaction as a whole. See [Webhook events](/guides/features/webhooks/events) for the full list. You can view every capture associated with a payment, each with its amount, status, and timing, for a complete history of how the authorization was drawn down. ## Limitations * Multiple captures must be enabled for your account, and your payment provider must support the feature. * Across all captures, you can't claim more than the authorized amount, unless capturing more than authorized is enabled for your account, in which case a configured limit applies. Pending captures count toward what you've claimed, so they're reserved while in progress. * Once you mark a capture as `final`, the authorization is complete and no further captures are possible. * No further capture requests are allowed once a previous capture has been declined. If a capture fails because of an error, you can attempt another capture. * Sync transaction doesn't work when a transaction has more than one capture. * Refunds can't be applied to an individual capture. * The transaction status isn't downgraded from `capture_succeeded` if a later capture fails or is declined. # Network token implementation Source: https://integration.wpay.com.au/guides/features/network-tokens/implementation How to use scheme tokens with or without payment orchestration To process payments with network tokens you can choose between either of the following options. * The **automatic provisioning of tokens** for use with the orchestration platform. * The **standalone tokenization endpoints** for using the network tokens directly with the PSPs. > [!NOTE] > > Before you start processing payments with network tokens, make sure you have > enabled processing with network tokens. Please contact the support team to get set up. ## Automatic provisioning of network tokens Although network tokens are agnostic tokens, not every payment service or acquirer support the usage of network tokens that were provisioned outside of their own environment. When you use the system to orchestrate your payment, the routing behavior is optimized to take into consideration the limitations of the payment services you're connecting to. A payment service that supports externally obtained data is generally considered an **open loop connection**, while one that doesn't is considered **closed loop**. In the orchestration platform, network tokens can be supported across any open-loop payment services. You don't have to perform any additional integrations to use network tokens besides storing the original card data (PAN) in the vault. You can follow the regular flow of card integrations, and the original card details are automatically swapped out with the network token when possible. Gr4vy only provisions and uses its own network tokens for stored cards. It provisions a network token when a transaction stores the card with `store: true`, or when a transaction uses a card that's already stored. A transaction that sends card details without storing them is processed with the PAN. Gr4vy doesn't match new card details against cards that are already stored. Each transaction that sends card details with `store: true` stores a new payment method, which gets its own network token. To reuse a network token, store the card once and charge later payments with the payment method `id`. You can use [card fingerprints](/guides/features/cards/fingerprints) to find cards that are stored more than once. ### Cryptograms A cryptogram authenticates a single use of the network token. Gr4vy requests a cryptogram in the following cases: * The transaction is the first one on a newly provisioned network token, including a merchant-initiated transaction (MIT). For example, when you move an existing billing sequence to Gr4vy, the first payment on each new token has a cryptogram. * The transaction is a customer-initiated transaction (CIT), including the first payment of a recurring or installment series. * The transaction is an MIT with `payment_source` set to `card_on_file` on a Mastercard card. * The transaction is an MIT on a network token that the card network has replaced. Other MITs, such as later payments in a recurring or installment series, use the network token without a cryptogram. Gr4vy requests a new cryptogram for each authorization attempt that needs one, and doesn't store cryptograms. If Gr4vy can't get a cryptogram, it skips the network token outcome and tries the next outcome in the routing rule. The token and cryptogram are provisioned as "on file" and therefore the buyer does not have to be present for the provisioning of either the token or the cryptogram. The usage of network tokens has been optimized by implementing scheme best practices regarding cryptogram usage, as well as the correct payment flagging towards the payment services. Using Flow, it is possible to choose the instrument (PAN or Network token) in your routing rules, allowing you to optimize your routing behavior. For best practices and advice on routing optimizations, please reach out to the support team. If Gr4vy can't provision a network token, for example because the card isn't eligible or the issuer declines tokenization, it skips the network token outcome without contacting the payment service and tries the next outcome. Gr4vy doesn't switch to the PAN by itself, so add a PAN outcome after each network token outcome. See [Instruments](/guides/dashboard/flow/card-transactions#instruments) in the Flow guide. To see whether provisioning worked, [list the transaction's events](/reference/transactions/list-transaction-events). Gr4vy adds a `network-token-provision-succeeded` or `network-token-provision-failed` event to the transaction that triggered provisioning. A failed event includes the reason, such as the issuer declining tokenization. ### Already stored cards You don't need to migrate cards that are already stored in the vault. Keep charging them with the payment method `id`. * Don't send `store: true` when you charge a stored payment method by `id`. The API rejects the request with a validation error, because the card is already stored. * Gr4vy provisions the network token during the next transaction that Flow routes to a network token outcome. With synchronous provisioning, that same transaction is authorized with the new token and its cryptogram, even when it's an MIT. * Keep a PAN outcome after the network token outcome, so that cards that can't be tokenized are still processed. * To see which instrument processed a transaction, check its `instrument_type`, which is `network_token` or `pan` for card transactions. ### Network token provisioning modes Network token provisioning can be configured in two different modes: **synchronous** (default) and **asynchronous**. The mode you use depends on your performance requirements and tolerance for processing latency. #### Synchronous provisioning In synchronous mode, the network token is created before the first transaction is processed. This ensures the token is ready for immediate use: * **Initial Customer Initiated Transaction (CIT):** The network token and cryptogram are created first, then the transaction is processed with the token and cryptogram. * **Subsequent CIT:** The transaction is processed with the network token and cryptogram. * **Subsequent MIT:** The transaction is processed with the network token only (cryptogram optional). This is the default mode and ensures tokens are used from the start. #### Asynchronous provisioning In asynchronous mode, network token creation is deferred to a background process, allowing faster initial transaction processing. This mode is useful when network token/cryptogram generation latency would negatively impact the customer experience: * **Initial CIT:** Gr4vy skips the network token outcome and creates the network token in the background. The transaction is processed with the PAN and CVV only if a PAN outcome follows in the routing rule. * **Subsequent CIT:** The transaction is processed with the network token and cryptogram (which were created in the background). * **Subsequent MIT:** The transaction is processed with the network token only. This mode provides the best user experience for initial transactions at the cost of a slightly delayed token creation window. In this mode, Gr4vy skips the network token outcome for the transaction that triggers provisioning, so that transaction uses the next outcome in the routing rule. Add a PAN outcome after the network token outcome, as described in [Instruments](/guides/dashboard/flow/card-transactions#instruments). Provisioning starts once that transaction succeeds: * If the transaction is still in progress, for example while the buyer completes 3-D Secure, Gr4vy retries provisioning automatically for a limited time. * If the transaction is declined or fails, Gr4vy doesn't provision a network token. * If the payment method already has a network token, Gr4vy doesn't provision another one. > [!NOTE] > > Asynchronous network tokenization is an opt-in feature that is currently configured per merchant by the support team. Contact support to enable asynchronous provisioning for your account if you require faster initial transaction processing. ## Standalone tokenization endpoints The API allows you to provision network tokens and cryptograms for use in your own system. Please note that network tokens can be considered PCI sensitive data depending on your interpretation of PCI guidelines, and therefore these API's are not enabled in production by default. You can use the [network token endpoints][endpoints] to obtain network tokens and their cryptograms for use in your own system. > [!WARNING] > > Network token provisioning via the API is not enabled by default in production. > Please contact support for further guidance. [endpoints]: /reference/network-tokens/ # Network token lifecycle management Source: https://integration.wpay.com.au/guides/features/network-tokens/lifecycle Managing the full lifecycle of network tokens The system has been set up to automatically keep your network tokens up to date. Updates are automatically received when the underlying card receives any updates, and these updates are applied to the network token, such as its status and expiration date. > [!NOTE] > > Network token updates don't change the stored card details, such as the last four digits or the card's > expiration date. The network token keeps working for payments after the card is replaced. To refresh the > stored card details, use the [account updater](/guides/features/account-updater/overview). The card holder can interact with their card in multiple ways which can result in these lifecycle events. * The contract with the issuer is terminated rendering all cards and related tokens invalid. In this case, all the associated network tokens are terminated and deleted. * A user reports a card lost or stolen or damaged. In this case, the issuer issues a new card and all the associated tokens stay active. The last four digits of the new card may differ from the stored card details. * The card expires. In this case, the issuer issues a new card and all the associated tokens stay active. The expiration date of the new card may differ from the stored card details. * The user triggers a token revocation from the issuer side, for example from the issuer's mobile banking app. In this case, any of the associated network tokens may be terminated and deleted. All of these events are automatically managed when you use the vault. ## Usage data Using different instruments per transaction attempt is possible. Insight is provided as to which instrument was used per transaction attempt via the **retry event report**. This report can be fetched via the API or via the dashboard. In the report, the used instrument is listed per transaction attempt. If the first attempt was done using the original card, and a retry was done using a network token, then the transaction shows as having used a "network token" in the dashboard. ## Manual lifecycle management Using the following endpoints it is possible to manually manage the token lifecycle for a stored token. Using these endpoints changes the status at the respective scheme token service and notifies the issuer. * [Suspend token endpoint][suspend] - Used to temporarily turn off a provisioned token. The token is temporarily suspended. The token can be resumed by using the `/resume` API. This API is only supported for Visa cards. * [Resume token endpoint][resume] - Used to re-enable a provisioned token. The token status changes from its previously suspended state into the active state. This API is only supported for Visa cards. * [Delete token endpoint][delete] - Used to permanently remove a provisioned token. The token is deleted and needs to be re-provisioned. [suspend]: /reference/network-tokens/suspend-network-token [resume]: /reference/network-tokens/resume-network-token [delete]: /reference/network-tokens/delete-network-token # Network tokens Source: https://integration.wpay.com.au/guides/features/network-tokens/overview Using scheme tokens with payment orchestration Network tokens are a technology created by [EMVCo][emvco] to make the payment ecosystem safer and more robust. ## EMV payment tokenization The **primary account number:** (PAN) the number printed on your card, which is a particularly sensitive piece of data. EMV Payment Tokenization (or network tokenization) provides a technology solution for protecting the PAN and securing digital and online payments. Tokenization is achieved by taking the PAN and replacing it with a unique alternative value, a payment token. This token is limited to the merchant it's been created for, which limits the use of the token in case it's ever compromised. Unlike a PAN, a network token cannot be used to process a transaction with another merchant. Network tokens comply with industry-standardized EMVCo specifications that not only adhere to PCI standards but are also identifiable across the entire transaction lifecycle. This enables significant ecosystem benefits for merchants using network tokens. * **Higher authorization rates:** Once a stored PAN has been securely replaced with a network token, the card network keeps the token current when the underlying card changes. For example, if a customer's card expires, the token remains usable for payments. The stored card details, such as the expiration date, aren't updated by the token. Use the [account updater](/guides/features/account-updater/overview) to refresh them. This reduces the number of charges declined due to outdated data, and increases the authorization rates - particularly cards used for recurring and subscription payments. * **Lower costs:** In general the card schemes maintain a lower fee for tokenized transactions. The exact value of this depends on your PSP, but this can be up to 10 bps per transaction. * **Improved security:** Network tokens can only be unlocked by the associated payment network. Therefore if it is lost or stolen, criminals cannot access the underlying card data at any point in the transaction lifecycle.\ In addition, each token is bound to a specific merchant and each transaction is protected with a one-time use cryptogram. This means that the token by itself cannot be used to initiate a transaction. * **Flexible provisioning:** The system supports both synchronous and asynchronous network token provisioning. Asynchronous provisioning allows merchants to process initial transactions quickly with the PAN while tokens are created in the background, optimizing the customer experience without sacrificing security or subsequent transaction efficiency. ## Network tokens versus Acquirer/PSP tokens Network tokenization is very different from acquirer tokenization or PSP tokenization, especially when you consider the data portability benefits. * **An acquirer or PSP token** swaps a customer's card details with a token that can only be used by the service that generated the token. This restricts the re-use and introduces considerable friction when using another service for failover, redundancy, or migration. * **A network token** swaps a customer's card details with a token that is tied to the scheme of the card (also called the networks) and can be used by any acquirer or PSP that can transact for that scheme. This provides ultimate flexibility to use this token with any service, whether that is for failover or redundancy purposes, or when migrating to another service. Using the full range of tokenization features with the system is recommended. You can store the original card number (PAN) in the vault and additionally use Network Tokens for transactions. With this, you can limit your PCI scope and optimize your payments using network tokens in an agnostic way across the payment ecosystem. Gr4vy only provisions and uses its own network tokens for stored cards. Digital wallet payments, such as Apple Pay, Google Pay, and Paze, use the token that the wallet provides, so Gr4vy doesn't provision a network token for them. A Google Pay `PAN_ONLY` payment has no wallet token and is sent with the card number instead. See [implementation](/guides/features/network-tokens/implementation) for when a network token is provisioned and used. If you provision network tokens yourself, you can [pass them through](/guides/features/network-tokens/passthrough) when you create a transaction, without storing them. ## Card details in your payment service's dashboard When a transaction uses a network token, your payment service receives the network token instead of the card number. The payment service's dashboard and reports can therefore show digits of the network token, such as a different BIN and last four digits, while Gr4vy shows the details of the buyer's card. To check which instrument a transaction used, look at its `instrument_type`, which is `network_token` or `pan` for card transactions. [emvco]: https://www.emvco.com/ # Network token pass through Source: https://integration.wpay.com.au/guides/features/network-tokens/passthrough Pass network tokens that you provision yourself through to a connection when you create a transaction. Fully PCI certified customers may want to provision their own network tokens rather than relying on the network token provisioning. To allow for this, support for network token pass through via the API has been enabled, either with or without cryptogram, straight through to a connector. * Any network token can be passed through, though acceptance (especially in sandbox) varies by connector used * Network tokens can benefit from BIN insights, though in the sandbox environment the number of cards that result in a valid BIN insights is limited * Storing externally provisioned network tokens in the vault is currently not supported ## Requirements To process a network token that you pass through, the following must apply: * The connection supports network tokens and has them turned on. To find connections that support network tokens, filter the [connection catalog](/connections/catalog?supports=network-tokens). Check the connector page for any extra steps. For example, the [Stripe](/connections/payments/stripe#recurring-payments) network tokens setting only appears after you turn on open loop. * If you use Flow routing rules, the rule that matches the transaction has an outcome with the **Network Token** instrument. For passed-through network tokens, Gr4vy ignores outcomes that use the PAN instrument. See [Instruments](/guides/dashboard/flow/card-transactions#instruments). Gr4vy looks up the card scheme from the BIN of the network token. If the BIN isn't recognized, the card scheme is `other`, and connections that need a known card scheme may reject the transaction. Not every connection accepts network tokens for every card scheme, so use a card scheme condition in Flow to route each scheme to a connection that accepts it. ## Usage To pass through a network token, set the `method` value to `network-token` in the [`payment_method`](/reference/transactions/new-transaction#body-payment-method) when creating a transaction, and set `token` to the network token. The `cryptogram` and `eci` values can be set as well, but are optional in the API. Acceptance of network tokens downstream varies per connector. ```json Payment method theme={"system"} { "method": "network-token", "token": "4111111111111111", "expiration_date": "12/30", "cryptogram": "AAEBAwQjSQAAXX...", "eci": "05" } ``` # Network Token Testing Source: https://integration.wpay.com.au/guides/features/network-tokens/testing To test Network Tokens in `sandbox` please make sure the following applies. * Network Tokens have been configured for your merchant account by the team. * The connection supports network tokens and has them turned on. * Transactions are sent with `store: true` or with a stored card. * A test card number from the list below is sent in the request. ### Successful network token | Test PAN | Scheme | | - | - | | `5204731600014784` | `mastercard` | | `5204731600013711` | `mastercard` | | `5204731600014636` | `mastercard` | | `5204731600012804` | `mastercard` | | `5204731600014792` | `mastercard` | | `5204731600013729` | `mastercard` | | `5204731600014644` | `mastercard` | | `5204731600012812` | `mastercard` | | `5204731600013737` | `mastercard` | | `5186009200935811` | `mastercard` | | `2222690420064590` | `mastercard` | | `2222690420064574` | `mastercard` | | `2222690420064582` | `mastercard` | | `5120342233150747` | `mastercard` | | `5120343287499758` | `mastercard` | | `4622943120000600` | `visa` | | `4895379980001778` | `visa` | | `4622943120000618` | `visa` | | `4622943120000626` | `visa` | | `4895379980001802` | `visa` | | `4622943120000634` | `visa` | | `4622943120000642` | `visa` | ### Testing error scenarios | Test PAN | Scheme | Error | | - | - | - | | `2223001850064564` | `mastercard` | `PAN_INELIGIBLE` | | `5462970394839348` | `mastercard` | `PAN_INELIGIBLE` | | `2223520127577835` | `mastercard` | `PAN_INELIGIBLE` | | `5460126128200800` | `mastercard` | `PAN_INELIGIBLE` | | `5207980000000102` | `mastercard` | `PAN_INELIGIBLE` | | `5204731601950473` | `mastercard` | `INVALID_PAN` | | `5204731601950473` | `mastercard` | `INVALID_PAN` | | `5204731940398145` | `mastercard` | Declined by Issuer | | `5204731600000239` | `mastercard` | Declined by Issuer | | `5120340000000392` | `mastercard` | Declined by Issuer | | `5120340000000293` | `mastercard` | Declined by Issuer | > [!WARNING] > > Any other card number is processed as ineligible and therefore > is not processed as a network token transaction. ### Testing network token pass-through The test card numbers on this page are for network tokens that Gr4vy provisions. To test a network token that you [pass through](/guides/features/network-tokens/passthrough), check the requirements on that page first. Acceptance of passed-through network tokens in sandbox varies by connector. # Payment facilitators Source: https://integration.wpay.com.au/guides/features/payment-facilitators/overview Process card payments as a payment facilitator (PayFac) by sending sub-merchant details to supported payment services. A payment facilitator (PayFac), also called an aggregator, processes payments for its sub-merchants under its own account with an acquirer. In each authorization, a PayFac identifies both itself and the sub-merchant the payment is for. This lets the acquirer attribute funds, reporting, and the buyer's statement descriptor to the right sub-merchant. Gr4vy supports the PayFac model on selected card connectors. On those connectors, Gr4vy sends the PayFac and sub-merchant details that the payment service expects with each transaction. ## How it works PayFac support is enabled per connection, by request only. It isn't available by default. Contact Gr4vy to have it configured on a connection. * **Sub-merchant details are set on the connection.** You don't pass them per transaction. Each PayFac connection carries the details of one sub-merchant, so set up a connection for each sub-merchant you process for. * **Your payment service account must allow it.** The payment service must enable PayFac processing on your account before it accepts sub-merchant details. Gr4vy can't enable this for you. ## Supported connectors | Connector | Details | | - | - | | [Fiserv IPG - Card](/connections/payments/fiserv-card#payment-facilitator-omnipay) | Uses Fiserv's Omnipay platform. Sends the sub-merchant's merchant category code (MCC), legal name, email, merchant ID, and address. | | [Cybersource - Card](/connections/payments/cybersource-card#payment-facilitators) | Sends your aggregator ID for the card scheme, your PayFac name, and the sub-merchant's ID, name, email, address, phone number, and MCC. Also sets a merchant descriptor that combines your PayFac name with the sub-merchant's details. | Other connectors don't have built-in PayFac support. Some accept sub-merchant fields per transaction through connection options instead. For example, the Adyen connector passes its `additionalData` connection option through to Adyen. Check the connector's page and the [new transaction](/reference/transactions/new-transaction) reference for the connection options each connector supports. ## Request support on another connector Gr4vy adds PayFac support to connectors on request. Before you ask, check with the payment service that it supports the PayFac model for your account, and in the countries you need. Payment services differ in whether they offer it and in which fields they require, and Gr4vy can't confirm this on their behalf. Once the payment service confirms its support and the fields it needs, contact Gr4vy to discuss adding them to the connector. # Payment links Source: https://integration.wpay.com.au/guides/features/payment-links/overview Set up payment links to accept payments from your customers. ![Payment Links](https://mintcdn.com/wpay/RxgiaZJ07iWfuxu1/assets/images/payment-links/hero.png?fit=max\&auto=format\&n=RxgiaZJ07iWfuxu1\&q=85\&s=0a7e66b16ceaa460e613cf3f6cbeb9a2) Payment links allows generation of a link, to send to a customer via email/sms/etc, and then have the customer pay without the need to host a custom checkout. ## Creating a payment link Use the payment links endpoint to create a payment link with the data displayed on the hosted page. The [payment links API endpoint](/reference/payment-links/new-payment-link) makes it easy to get create a new payment link. ```json Request theme={"system"} { "currency":"AUD", "country":"AU", "amount":1000, "metadata":{ "TypeOfPayment":"purchase", "Carbon_FootPrint":"10" }, "cart_items":[ { "name":"Aloe", "unit_amount":1000, "quantity":1, "sku":"aloe" } ], "merchant_banner_url":"https://susies.store/logo.svg", "merchant_color":"#cf6b6b", "merchant_name":"Susie's Store", "merchant_url":"https://susies.store", "merchant_message":"Thanks for your purchase at Susie's Store!", "merchant_terms_and_conditions_url":"https://susies.store/terms-and-conditions", "return_url":"https://susies.store/success" } ``` The payment link includes the `expires_at`, `status`, and other useful information used by the hosted page. Make note of the returned `id` as it is used in the next step. ```json Response theme={"system"} { "id": "09e90ace-a746-41f5-88d2-8b16335ded97", "type": "payment-link", "expires_at": "2025-01-28T14:45:45.929102+00:00", "amount": 1000, "currency": "AUD", "country": "AU", "status": "active", ... } ``` The hosted page passes these fields to [Embed](/guides/payments/embed/options#options), which uses them to create the transaction: * `amount`, `currency`, and `country` (required) * `buyer` * `buyer_id` * `cart_items` * `connection_options` * `external_identifier` * `installment_count` * `intent` * `metadata` * `payment_source` * `statement_descriptor` * `store` These fields control the hosted page itself: * `expires_at` sets when the link expires. See [expiration](/guides/features/payment-links/statuses#expiration). * `locale` sets the language of the page. * `return_url` is where the page sends the customer once the transaction is created, whatever its outcome. Gr4vy appends `gr4vy_transaction_id` and `gr4vy_transaction_status`. Without it, the page shows its own confirmation screen. * `merchant_name`, `merchant_url`, `merchant_banner_url`, `merchant_color`, `merchant_message`, `merchant_terms_and_conditions_url`, and `merchant_favicon_url` brand the page. When you set both `merchant_name` and `merchant_url`, the success and expired screens show a **Return to** link with your merchant name that opens `merchant_url`. Payment links don't support airline data. The API rejects a request that includes an `airline` field. See the [API reference](/reference/payment-links/new-payment-link) for the full list of fields. By creating a payment link, send it to the customer to complete the payment. ## Limit the payment methods on a link Payment links don't take a list of payment methods to show or hide. To control which methods appear, set a `metadata` key on the payment link, for example `{"link_type": "invoice"}`, and add a rule to the [Checkout Flow](/guides/dashboard/flow/checkout#action-select-payment-options) with a **Metadata** condition on that key. The rule then selects the payment options for those links. If no rule matches, the page shows all configured and active payment options. Checkout Flow rules apply to the whole merchant account, not just to payment links. ## Storing a payment method A payment link can be used to securely store a payment method against a buyer's profile. To do this, you need to pass both the `buyer_id` and `store` parameters. When `store` is set to `true`, the `buyer_id` of an existing buyer must also be provided. After the payment is completed, the payment method used is stored and associated with that buyer. When `store` is `true`, the payment link page only shows payment methods that can be stored, so some of the options you've configured might not appear. Currently, it is not possible to use a buyer's existing stored payment methods to complete a payment link. > [!WARNING] > > When `buyer_id` is provided, the URL of the payment link should be treated as a secret as > it allows anyone the ability to manage payment methods for the associated buyer. ## Find a payment link's transactions To list the transactions created through a payment link, call [list transactions](/reference/transactions/list-transactions) with the `payment_link_id` query parameter set to the `id` of the payment link. ```http theme={"system"} GET /transactions?payment_link_id=09e90ace-a746-41f5-88d2-8b16335ded97 ``` In the dashboard, go to **Transactions** > **Payment links** and open the payment link to see its transactions. # Sharing the payment link Source: https://integration.wpay.com.au/guides/features/payment-links/sharing With the payment link created, share it with the customer. By default a payment link page is hosted with the data you created at the following URL. ```sh theme={"system"} https://pay.{environment}.{instance_id}.gr4vy.app/link/{payment_link_id} ``` * `{payment_link_id}` is the `id` of the payment link you created in the previous step * `{instance_id}` is your `gr4vy_id` * `{environment}` is the environment, either `sandbox` or nothing for production. For example, in production. ```sh theme={"system"} https://pay.example.gr4vy.app/link/954289d7-d701-45ff-a67c-445e60f6e5af ``` Or in sandbox. ```sh theme={"system"} https://pay.sandbox.example.gr4vy.app/link/daf08adb-39bb-4038-b1f1-c52c791e75b6 ``` > [!NOTE] > > A custom domain for your payment links can be configured by contacting the support team. # Payment link expiration and statuses Source: https://integration.wpay.com.au/guides/features/payment-links/statuses Your customer uses the payment link to create the payment. Once the transaction is created, the customer is redirected to the `return_url` you specified, or shown the page's own confirmation screen if you didn't set one. Gr4vy appends `gr4vy_transaction_id` and `gr4vy_transaction_status` to the `return_url`. The redirect happens for every transaction outcome, including declines, so check the status before you treat the order as paid. To confirm the result, [get the transaction](/reference/transactions/get-transaction) or listen for [transaction webhooks](/guides/features/webhooks/overview). ## Statuses The page displays different content based on the status of the payment link. ### Generic error Either the payment link id provided is invalid or some other error occurred when loading the page (for example network error). ![Generic Error](https://mintcdn.com/wpay/c50rx3S48hHhNNxb/assets/images/payment-links/generic-error.png?fit=max\&auto=format\&n=c50rx3S48hHhNNxb\&q=85\&s=09a647d14921aa36639d803d33a0e011) ### Payment link not found The payment link id provided is valid but the resource associated with it could not be found. ![Not Found](https://mintcdn.com/wpay/RxgiaZJ07iWfuxu1/assets/images/payment-links/not-found.png?fit=max\&auto=format\&n=RxgiaZJ07iWfuxu1\&q=85\&s=19846f472bc584014ddb933c00578809) ### Redirecting Embed has created the transaction and the page is redirecting the customer to the `return_url` or the internal confirmation screen. ![Redirecting](https://mintcdn.com/wpay/RxgiaZJ07iWfuxu1/assets/images/payment-links/redirecting.png?fit=max\&auto=format\&n=RxgiaZJ07iWfuxu1\&q=85\&s=4662cf250ba384482d00a5ef8c6d22a9) ### Payment successful (internal) The payment was successful and a `return_url` was not specified. Also displays if the payment link has already been used. ![Payment Successful](https://mintcdn.com/wpay/RxgiaZJ07iWfuxu1/assets/images/payment-links/payment-successful.png?fit=max\&auto=format\&n=RxgiaZJ07iWfuxu1\&q=85\&s=e7a450ddffac0a92d33fc393b74be55c) ## Expiration By default, payment link expires in 24 hours. This can be configured separately by setting the `expires_at` field in the API. Once expired, a payment link no longer loads and instead shows the following screen. ![Expired](https://mintcdn.com/wpay/c50rx3S48hHhNNxb/assets/images/payment-links/expired.png?fit=max\&auto=format\&n=c50rx3S48hHhNNxb\&q=85\&s=5ee0288b318a935d56d621be325e334b) # Paze on mobile Source: https://integration.wpay.com.au/guides/features/paze/mobile Add Paze to native iOS and Android apps using the Paze mobile SDK and the Gr4vy server SDK. This guide shows how to add Paze to a native iOS or Android app. The shopper completes the Paze flow in your app via the Paze mobile SDK, and your server uses the Gr4vy server SDK to orchestrate the session and create the resulting transaction. For background on how Paze works and how Gr4vy fits in, read the [overview](/guides/features/paze/overview). A runnable web simulation of this flow is available in the [`sample-standalone-paze`](https://github.com/gr4vy/sample-standalone-paze) repository, alongside the web version. The repository shows the server-side calls end-to-end against the Gr4vy API; pair it with the Paze iOS or Android SDK in your own app for the native launch step. ## Before you begin Each merchant that wants to use Paze needs to be approved by Early Warning Services (EWS), and Gr4vy then enables Paze on a per-merchant basis. To start onboarding, see [Set up Paze](/connections/digital-wallets/paze#set-up-paze) on the connector page. The native Paze iOS and Android SDKs are distributed by Paze. Your Paze onboarding contact provides the SDK, the integration documentation, and the credentials your app needs. > [!WARNING] > > The server-side examples assume you have already created a Gr4vy client authenticated with your private API key. See [Authentication](/guides/api/authentication) to create the client. Keep your API key and the Gr4vy client on the server — never expose them in your mobile app. ## Overview of the flow The mobile integration has a server side and an app side that coordinate through a callback URL: 1. **Server-side** — create a Paze mobile session with Gr4vy. The session returns the Paze merchant data `id`, `name`, `profileId`, an `accessToken`, and a `sessionId`. 2. **Server-side** — create a Paze checkout URL with the session values, your callback URL scheme, and the intent. 3. **App-side** — launch the Paze checkout URL using the Paze iOS or Android SDK. After the shopper finishes, Paze invokes your callback URL scheme with an opaque response code. 4. **App-side** — forward the response code to your server. 5. **Server-side** — (optional) review the session. Required for `REVIEW_AND_PAY` if you want Gr4vy to add shipping details to the transaction; skipped for `EXPRESS_CHECKOUT`. 6. **Server-side** — complete the session to get the opaque token. 7. **Server-side** — create a Gr4vy transaction with the token. ## Step 1: Create a Paze mobile session On your server, create a Paze session with `source` set to `"mobile"`. Unlike the web flow, `domain_name` is not required for mobile sessions. The response includes the merchant `id`, `name`, `profileId`, plus the `accessToken` and `sessionId` you pass to the next step. ```csharp C# theme={"system"} var res = await client.DigitalWallets.Sessions.PazeAsync( pazeSessionRequest: new PazeSessionRequest() { Source = "mobile", } ); PazeMobileSession? session = null; if (res?.Type == ResponseCreatePazeDigitalWalletSessionType.PazeMobileSession) { session = res.PazeMobileSession; } ``` ```go Go theme={"system"} source := components.SourceMobile res, err := client.DigitalWallets.Sessions.Paze(ctx, components.PazeSessionRequest{ Source: &source, }) if err != nil { log.Fatal(err) } var session *components.PazeMobileSession if res != nil && res.Type == operations.ResponseCreatePazeDigitalWalletSessionTypePazeMobileSession { session = res.PazeMobileSession } ``` ```java Java theme={"system"} CreatePazeDigitalWalletSessionResponse res = gr4vyClient.digitalWallets().sessions().paze() .pazeSessionRequest(PazeSessionRequest.builder() .source(Source.MOBILE) .build()) .call(); PazeMobileSession session = null; if (res.responseCreatePazeDigitalWalletSession().isPresent()) { Object raw = res.responseCreatePazeDigitalWalletSession().get().value(); if (raw instanceof PazeMobileSession) { session = (PazeMobileSession) raw; } } ``` ```php PHP theme={"system"} $pazeSessionRequest = new PazeSessionRequest(source: 'mobile'); $response = $sdk->digitalWallets->sessions->paze(pazeSessionRequest: $pazeSessionRequest); $session = $response->responseCreatePazeDigitalWalletSession; ``` ```python Python theme={"system"} session = client.digital_wallets.sessions.paze( source="mobile", ) ``` ```ts TypeScript theme={"system"} const session = await gr4vy.digitalWallets.sessions.paze({ source: "mobile", }); ``` > [!WARNING] > > Use the same `accessToken` and `sessionId` for every server call in this checkout (create, review, complete). Do not reuse them for a different checkout — request a new session each time. ```json Response theme={"system"} { "id": "W8GT9RLCNME754Z7025613H3PDM2T4HF2CSAOi9w2kkP3D4S0", "name": "ACME", "profileId": "550e8400-e29b-41d4-a716-446655440000", "accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "sessionId": "7c1cba03-d20e-4a3f-9d77-e5dc23a39ac2" } ``` ## Step 2: Create the Paze checkout URL Call `paze_mobile_session_create` with the values from Step 1, the callback URL scheme your app handles, the intent, and the transaction value. The response includes a `pazeCheckoutUrl` to open in the Paze SDK. The supported intents are: * `REVIEW_AND_PAY` — the shopper reviews shipping and card details before paying. Your app and server then call the review step before completing. * `EXPRESS_CHECKOUT` — the shopper pays immediately without a review step. ```csharp C# theme={"system"} var create = await client.DigitalWallets.Sessions.PazeMobileSessionCreateAsync( pazeMobileSessionCreateRequest: new PazeMobileSessionCreateRequest() { Client = new PazeClient() { Id = session.Id, Name = session.Name, ProfileId = session.ProfileId }, SessionId = session.SessionId, AccessToken = session.AccessToken, CallbackURLScheme = "myapp://paze/return", Intent = "REVIEW_AND_PAY", TransactionValue = new PazeTransactionValue() { TransactionAmount = "50.21", TransactionCurrency = "AUD", }, TransactionType = "PURCHASE", } ); ``` ```go Go theme={"system"} create, err := client.DigitalWallets.Sessions.PazeMobileSessionCreate(ctx, components.PazeMobileSessionCreateRequest{ Client: components.PazeClient{ ID: session.ID, Name: &session.Name, ProfileID: &session.ProfileID, }, SessionID: session.SessionID, AccessToken: session.AccessToken, CallbackURLScheme: "myapp://paze/return", Intent: "REVIEW_AND_PAY", TransactionValue: &components.PazeTransactionValue{ TransactionAmount: "50.21", TransactionCurrency: "AUD", }, TransactionType: gr4vy.String("PURCHASE"), }) ``` ```java Java theme={"system"} CreatePazeMobileSessionResponse createRes = gr4vyClient.digitalWallets().sessions().pazeMobileSessionCreate() .pazeMobileSessionCreateRequest(PazeMobileSessionCreateRequest.builder() .client(PazeClient.builder() .id(session.id()) .name(session.name()) .profileId(session.profileId()) .build()) .sessionId(session.sessionId()) .accessToken(session.accessToken()) .callbackURLScheme("myapp://paze/return") .intent("REVIEW_AND_PAY") .transactionValue(PazeTransactionValue.builder() .transactionAmount("50.21") .transactionCurrency("AUD") .build()) .transactionType("PURCHASE") .build()) .call(); PazeMobileSessionCreate create = createRes.pazeMobileSessionCreate().orElse(null); ``` ```php PHP theme={"system"} $create = $sdk->digitalWallets->sessions->pazeMobileSessionCreate( pazeMobileSessionCreateRequest: new PazeMobileSessionCreateRequest( client: new PazeClient( id: $session->id, name: $session->name, profileId: $session->profileId, ), sessionId: $session->sessionId, accessToken: $session->accessToken, callbackURLScheme: 'myapp://paze/return', intent: 'REVIEW_AND_PAY', transactionValue: new PazeTransactionValue( transactionAmount: '50.21', transactionCurrency: 'AUD', ), transactionType: 'PURCHASE', ) ); ``` ```python Python theme={"system"} create = client.digital_wallets.sessions.paze_mobile_session_create( client={ "id": session.id, "name": session.name, "profile_id": session.profile_id, }, session_id=session.session_id, access_token=session.access_token, callback_url_scheme="myapp://paze/return", intent="REVIEW_AND_PAY", transaction_value={ "transaction_amount": "50.21", "transaction_currency": "AUD", }, transaction_type="PURCHASE", ) ``` ```ts TypeScript theme={"system"} const create = await gr4vy.digitalWallets.sessions.pazeMobileSessionCreate({ client: { id: session.id, name: session.name, profileId: session.profileId }, sessionId: session.sessionId, accessToken: session.accessToken, callbackURLScheme: "myapp://paze/return", intent: "REVIEW_AND_PAY", transactionValue: { transactionAmount: "50.21", transactionCurrency: "AUD", }, transactionType: "PURCHASE", }); ``` Return `create.pazeCheckoutUrl`, `session.sessionId`, and `session.accessToken` to your app, along with the chosen `intent`. Your app needs all three to handle the callback and call your server again in Step 4. ## Step 3: Launch Paze in your app Use the Paze iOS or Android SDK to open `pazeCheckoutUrl`. The exact native call is specific to each platform — refer to the SDK documentation your Paze onboarding contact provides. When the shopper finishes, Paze invokes your callback URL scheme (the one you passed in Step 2) with a `response` parameter containing a base64-encoded JWE. Your app decodes that value and sends it to your server, together with the `sessionId`, `accessToken`, and `intent` from Step 2. See [`sample-standalone-paze`](https://github.com/gr4vy/sample-standalone-paze) for an end-to-end reference of the callback decode and the server-side review/complete/transaction calls. > [!WARNING] > > The callback `response` is sensitive material that authorizes the transaction. Forward it directly to your server over a trusted channel — do not persist it client-side or send it to other destinations. ## Step 4: Review the session This step is optional. Call `paze_mobile_session_review` when you want Gr4vy to add the shipping address (and the rest of the review data) to the transaction in Step 6. Skip it for `EXPRESS_CHECKOUT`, or for `REVIEW_AND_PAY` checkouts where you do not need those details — and pass the original callback `code` directly to Step 5. Call `paze_mobile_session_review` with the JWE `code` from the callback. The response includes the selected card (masked), the consumer, and the shipping address. When you call review, use `review.code` as the input to Step 5. ```csharp C# theme={"system"} var review = await client.DigitalWallets.Sessions.PazeMobileSessionReviewAsync( pazeSessionReviewRequest: new PazeSessionReviewRequest() { SessionId = sessionId, Code = code, AccessToken = accessToken, } ); ``` ```go Go theme={"system"} review, err := client.DigitalWallets.Sessions.PazeMobileSessionReview(ctx, components.PazeSessionReviewRequest{ SessionID: sessionId, Code: code, AccessToken: accessToken, }) ``` ```java Java theme={"system"} ReviewPazeMobileSessionResponse reviewRes = gr4vyClient.digitalWallets().sessions().pazeMobileSessionReview() .pazeSessionReviewRequest(PazeSessionReviewRequest.builder() .sessionId(sessionId) .code(code) .accessToken(accessToken) .build()) .call(); PazeSessionReview review = reviewRes.pazeSessionReview().orElse(null); ``` ```php PHP theme={"system"} $review = $sdk->digitalWallets->sessions->pazeMobileSessionReview( pazeSessionReviewRequest: new PazeSessionReviewRequest( sessionId: $sessionId, code: $code, accessToken: $accessToken, ) ); ``` ```python Python theme={"system"} review = client.digital_wallets.sessions.paze_mobile_session_review( session_id=session_id, code=code, access_token=access_token, ) ``` ```ts TypeScript theme={"system"} const review = await gr4vy.digitalWallets.sessions.pazeMobileSessionReview({ sessionId, code, accessToken, }); ``` ## Step 5: Complete the session Call `paze_mobile_session_complete` with the code from the previous step (`review.code` for `REVIEW_AND_PAY`, or the original callback `code` for `EXPRESS_CHECKOUT`). The response includes `completeResponse`, the opaque token you forward to Gr4vy as the transaction token. ```csharp C# theme={"system"} var complete = await client.DigitalWallets.Sessions.PazeMobileSessionCompleteAsync( pazeSessionCompleteRequest: new PazeSessionCompleteRequest() { SessionId = sessionId, Code = completeCode, AccessToken = accessToken, TransactionType = "PURCHASE", } ); ``` ```go Go theme={"system"} complete, err := client.DigitalWallets.Sessions.PazeMobileSessionComplete(ctx, components.PazeSessionCompleteRequest{ SessionID: sessionId, Code: completeCode, AccessToken: accessToken, TransactionType: "PURCHASE", }) ``` ```java Java theme={"system"} CompletePazeMobileSessionResponse completeRes = gr4vyClient.digitalWallets().sessions().pazeMobileSessionComplete() .pazeSessionCompleteRequest(PazeSessionCompleteRequest.builder() .sessionId(sessionId) .code(completeCode) .accessToken(accessToken) .transactionType("PURCHASE") .build()) .call(); PazeSessionComplete complete = completeRes.pazeSessionComplete().orElse(null); ``` ```php PHP theme={"system"} $complete = $sdk->digitalWallets->sessions->pazeMobileSessionComplete( pazeSessionCompleteRequest: new PazeSessionCompleteRequest( sessionId: $sessionId, code: $completeCode, accessToken: $accessToken, transactionType: 'PURCHASE', ) ); ``` ```python Python theme={"system"} complete = client.digital_wallets.sessions.paze_mobile_session_complete( session_id=session_id, code=complete_code, access_token=access_token, transaction_type="PURCHASE", ) ``` ```ts TypeScript theme={"system"} const complete = await gr4vy.digitalWallets.sessions.pazeMobileSessionComplete({ sessionId, code: completeCode, accessToken, transactionType: "PURCHASE", }); ``` ## Step 6: Create a transaction Create the transaction with the opaque `completeResponse` from Step 5 as `token`. Set `checkout_token` to the **original** JWE callback `code` from Step 3 (not `review.code`) so Gr4vy can populate the masked card details and shipping address on the transaction. > [!WARNING] > > The Paze mobile SDK and `transactionValue` use decimal-string major units (for example `"50.21"`), while Gr4vy's transaction `amount` is an integer in the smallest currency unit (for example `5021` for \$50.21). Convert between the two when bridging the values, and make sure they match for the same purchase. ```csharp C# theme={"system"} var transaction = await client.Transactions.CreateAsync( transactionCreate: new TransactionCreate() { Amount = 5021, Currency = "AUD", Country = "AU", PaymentMethod = TransactionCreatePaymentMethod.CreatePazePaymentMethodCreate( new PazePaymentMethodCreate() { Method = "paze", Token = complete.CompleteResponse, CheckoutToken = code, } ), } ); ``` ```go Go theme={"system"} pazePaymentMethodCreate := components.PazePaymentMethodCreate{ Token: *complete.CompleteResponse, CheckoutToken: &code, } paymentMethod := components.CreateTransactionCreatePaymentMethodPazePaymentMethodCreate(pazePaymentMethodCreate) transaction, err := client.Transactions.Create(ctx, components.TransactionCreate{ Amount: 5021, Currency: "AUD", Country: gr4vy.String("AU"), PaymentMethod: &paymentMethod, }, nil, nil) ``` ```java Java theme={"system"} CreateTransactionResponse transactionResponse = gr4vyClient.transactions().create() .transactionCreate(TransactionCreate.builder() .amount(5021L) .currency("AUD") .country("AU") .paymentMethod(TransactionCreatePaymentMethod.of(PazePaymentMethodCreate.builder() .token(complete.completeResponse().orElseThrow()) .checkoutToken(code) .build())) .build()) .call(); Transaction transaction = transactionResponse.transaction().orElse(null); ``` ```php PHP theme={"system"} $transactionCreate = new TransactionCreate( amount: 5021, currency: 'AUD', country: 'AU', paymentMethod: new PazePaymentMethodCreate( method: 'paze', token: $complete->completeResponse, checkoutToken: $code, ), ); $response = $sdk->transactions->create($transactionCreate); $transaction = $response->transaction; ``` ```python Python theme={"system"} transaction = client.transactions.create( amount=5021, currency="AUD", country="AU", payment_method={ "method": "paze", "token": complete.complete_response, "checkout_token": code, }, ) ``` ```ts TypeScript theme={"system"} const transaction = await gr4vy.transactions.create({ amount: 5021, currency: "AUD", country: "AU", paymentMethod: { method: "paze", token: complete.completeResponse, checkoutToken: code, }, }); ``` The response is a standard Gr4vy transaction. Handle its `status` as you would for any other payment method. ## Test Paze To test Paze on mobile, drive your integration in a simulator or device build with a Paze test wallet. > [!NOTE] > > Test wallet credentials are issued by Paze, not by Gr4vy. To obtain a test wallet for your merchant, work with Paze directly through your Paze onboarding contact. For general sandbox environment information, see Paze's [Environments guide](https://developer.paze.com/design-sandbox/docs/environments). # Paze Source: https://integration.wpay.com.au/guides/features/paze/overview Understand how Paze works and how to add the bank-led US digital wallet to your checkout with Gr4vy. ![Paze](https://mintcdn.com/wpay/eKmZTvDmeu_K9Ctt/assets/images/paze/hero.png?fit=max\&auto=format\&n=eKmZTvDmeu_K9Ctt\&q=85\&s=eabb23f7cb63df60cf40482cd0af209d) Paze is a bank-led digital wallet from Early Warning Services (EWS), the consortium behind Zelle. Major US banks provision eligible credit and debit cards directly into the wallet, so shoppers can check out without entering card details or creating a separate wallet account. This overview explains how Paze works and how Gr4vy fits in. To build an integration, follow the [web setup guide](/guides/features/paze/web). > [!NOTE] > > Paze is available to US-based businesses only. Transactions settle in US dollars and require a US acquiring connection that [supports network tokens](/guides/features/network-tokens/passthrough). ## How Paze works Paze does not require shoppers to install an app or enroll cards. Participating banks pre-provision eligible cards into the wallet, and shoppers authenticate with their existing bank credentials. When a shopper checks out: 1. Your checkout renders the Paze button and launches the Paze experience using the Paze SDK. 2. The shopper selects one of their bank-provisioned cards and authenticates. 3. Paze returns an encrypted payload that contains a network token. 4. Your server sends that payload to Gr4vy, which decrypts it and creates a transaction. Because Paze returns a network token rather than a card number, transactions can benefit from higher authorization rates and lower fraud. ### Paze compared with Apple Pay and Google Pay Paze works like Apple Pay and Google Pay in most ways. It's a wallet rather than a payment processor, so one of your card connections authorizes each payment, and Gr4vy decrypts the wallet payload for you. Like Apple Pay and Google Pay device tokens, Paze payments don't go through 3-D Secure. There are two main differences. Banks pre-provision eligible cards into Paze, so shoppers don't add their cards to a device first. And where Gr4vy sends Apple Pay and Google Pay payments to your payment service as decrypted wallet tokens, it sends Paze payments as a network token with a cryptogram, in the same way as a [network token pass-through](/guides/features/network-tokens/passthrough). Your payment service needs to accept network tokens in this form. ## How Gr4vy fits in Gr4vy acts as the Paze Technical Integrator. This means Gr4vy: * Enrolls your merchant account with Paze and manages the Paze credentials. * Generates and rotates the encryption keys and certificates that Paze requires. * Decrypts the Paze payload, extracts the network token, and routes the transaction to your payment service through [Flow](/guides/dashboard/flow/overview). * Ingests the cardholder, billing, and shipping details from the Paze payload and populates them on the transaction. See the per-platform setup guide for the exact mechanics on web and mobile. You host the Paze button and forward the Paze checkout responses to Gr4vy. You do not manage Paze certificates or private keys yourself. ## Integration paths You add the Paze button to your checkout using the Paze SDK for your platform. Gr4vy provides a session endpoint that returns the values the Paze SDK needs to initialize. > **[Web (JavaScript)](/guides/features/paze/web)** > > Add Paze to a web checkout using the Paze JavaScript SDK and the Gr4vy server SDK. > **[Mobile (native)](/guides/features/paze/mobile)** > > Add Paze to iOS and Android apps using the native Paze SDK and the Gr4vy server SDK. ## Reference * [Paze for Business](https://www.paze.com/for-business) — overview for merchants. * [`sample-standalone-paze`](https://github.com/gr4vy/sample-standalone-paze) — a runnable web sample app. # Set up Paze Source: https://integration.wpay.com.au/guides/features/paze/setup Activate Paze in the Gr4vy dashboard and configure your merchant details and domains. This guide walks through activating the Paze digital wallet in the Gr4vy dashboard, the fields it asks for, and how to manage the configuration afterwards. For background on how Paze works and how Gr4vy fits in, read the [overview](/guides/features/paze/overview). When you are ready to add Paze to your checkout, follow the [web setup](/guides/features/paze/web) or [mobile setup](/guides/features/paze/mobile) guide. ## Before you begin Each merchant that wants to use Paze needs to be approved by Early Warning Services (EWS) before Gr4vy can register it with Paze. See [Set up Paze](/connections/digital-wallets/paze#set-up-paze) on the connector page for the onboarding flow. Submit the dashboard activation form below only after your merchant is approved. ## Activate Paze In the Gr4vy dashboard, open **Connections** → **Catalog** → **Paze** to start the activation form. The form has two sections — **Merchant details** and **Merchant profile & domain configuration** — and submits a `POST /digital-wallets` request with `provider` set to `paze`. ### Merchant details These fields identify your business and are used when Gr4vy registers your merchant with Paze. After activation, the legal name, merchant URL, and trading name become read-only. | Field | Required | Description | | - | - | - | | Legal name | Yes | The legal name of your company. Maps to `merchant_name` on the API. | | Merchant URL | Yes | The main URL of your business. Maps to `merchant_url`. | | Trading name | No | The name shown to shoppers in the Paze experience. Defaults to the legal name if left empty. Maps to `merchant_display_name`. | | Address line 1 | Yes | Street address. Maps to `address.line1`. | | Address line 2 | No | Suite, floor, or unit. Maps to `address.line2`. | | City | Yes | City of the registered address. Maps to `address.city`. | | State | Yes | The ISO 3166-2 subdivision code for the state or region, such as `US-CA`. Maps to `address.state_code`. | | Zip code / postal code | Yes | Postal code of the registered address. Maps to `address.postal_code`. | ### Merchant profile & domain configuration These fields control how Paze categorizes your transactions and where the Paze button can run. | Field | Required | Description | | - | - | - | | Merchant category code (MCC) | Yes | The four-digit MCC that best describes your business. Maps to `merchant_category_code`. | | Domain names | Yes | One or more fully qualified domain names where the Paze button is hosted (for example, `www.example.com`). Maps to `domain_names`. | When you save the form, Gr4vy registers your merchant with Paze on your behalf. ## Manage domains You can add domains where the Paze button runs after activation. On the Paze connection page, open the **Domains** tab to add a new fully qualified domain name. Removing domains from a Paze merchant is not currently supported. ## Edit the configuration After activation, the **Overview** tab of the Paze connection lets you edit the address and the MCC. The legal name, merchant URL, and trading name are fixed once Gr4vy registers your merchant with Paze. Some additional configuration, such as the network name and the MID list, can be updated only through the API and is not exposed in the dashboard. ## Next steps * [Add Paze to your web checkout](/guides/features/paze/web). * [Add Paze to your iOS or Android app](/guides/features/paze/mobile). # Paze on web Source: https://integration.wpay.com.au/guides/features/paze/web Add Paze to a web checkout using the Paze JavaScript SDK and the Gr4vy server SDK. This guide shows how to add Paze to a web checkout. The shopper completes the Paze flow in the browser with the Paze JavaScript SDK, and your server uses the Gr4vy server SDK to create a Paze session and the resulting transaction. For background on how Paze works and how Gr4vy fits in, read the [overview](/guides/features/paze/overview). ## Before you begin Each merchant that wants to use Paze needs to be approved by Early Warning Services (EWS), and Gr4vy then enables Paze on a per-merchant basis. To start onboarding, see [Set up Paze](/connections/digital-wallets/paze#set-up-paze) on the connector page. A runnable version of this integration is available in the [`sample-standalone-paze`](https://github.com/gr4vy/sample-standalone-paze) repository. > [!WARNING] > > The server-side examples assume you have already created a Gr4vy client authenticated with your private API key. See [Authentication](/guides/api/authentication) to create the client. Keep your API key and the Gr4vy client on the server — never expose them in browser code. ## Overview of the flow The integration has server-side and browser-side steps: 1. **Server-side** — create a Paze session with Gr4vy. The session returns the Paze merchant data `id`, merchant `name`, and `profileId` that the Paze JavaScript SDK needs. 2. **Browser-side** — load the Paze JavaScript SDK, initialize it with the session, render the Paze button, and run the checkout. 3. **Server-side** — create a Gr4vy transaction with the opaque token that the Paze checkout returns. ## Step 1: Create a Paze session On your server, create a Paze session with the domain that hosts the Paze button. Pass the full domain, including any subdomains such as `www.`. ```csharp C# theme={"system"} var res = await client.DigitalWallets.Sessions.PazeAsync( pazeSessionRequest: new PazeSessionRequest() { DomainName = "www.example.com", } ); PazeWebSession? session = null; if (res?.Type == ResponseCreatePazeDigitalWalletSessionType.PazeWebSession) { session = res.PazeWebSession; } ``` ```go Go theme={"system"} domainName := "www.example.com" pazeSessionRequest := components.PazeSessionRequest{ DomainName: &domainName, } res, err := client.DigitalWallets.Sessions.Paze(ctx, pazeSessionRequest) if err != nil { log.Fatal(err) } var session *components.PazeWebSession if res != nil { switch res.Type { case operations.ResponseCreatePazeDigitalWalletSessionTypePazeWebSession: session = res.PazeWebSession case operations.ResponseCreatePazeDigitalWalletSessionTypePazeMobileSession: // res.PazeMobileSession is populated for mobile sessions } } ``` ```java Java theme={"system"} CreatePazeDigitalWalletSessionResponse res = gr4vyClient.digitalWallets().sessions().paze() .pazeSessionRequest(PazeSessionRequest.builder() .domainName("www.example.com") .build()) .call(); PazeWebSession session = null; if (res.responseCreatePazeDigitalWalletSession().isPresent()) { Object raw = res.responseCreatePazeDigitalWalletSession().get().value(); if (raw instanceof PazeWebSession) { session = (PazeWebSession) raw; } } ``` ```php PHP theme={"system"} $pazeSessionRequest = new PazeSessionRequest(domainName: 'www.example.com'); $response = $sdk->digitalWallets->sessions->paze(pazeSessionRequest: $pazeSessionRequest); $session = $response->responseCreatePazeDigitalWalletSession; ``` ```python Python theme={"system"} session = client.digital_wallets.sessions.paze( domain_name="www.example.com", ) ``` ```ts TypeScript theme={"system"} const session = await gr4vy.digitalWallets.sessions.paze({ domainName: "www.example.com", }); ``` The session contains the values that the Paze JavaScript SDK needs. Return it to the browser. ```json Response theme={"system"} { "id": "W8GT9RLCNME754Z7025613H3PDM2T4HF2CSAOi9w2kkP3D4S0", "name": "ACME", "profileId": "550e8400-e29b-41d4-a716-446655440000" } ``` ## Step 2: Load and initialize the Paze SDK In the browser, add the Paze JavaScript SDK script for your environment. Use the sandbox script while testing, and the production script when you go live. ```html Sandbox theme={"system"} ``` ```html Production theme={"system"} ``` Once the script loads, the SDK is available as `window.DIGITAL_WALLET_SDK`. Initialize it with the session from Step 1, then render the Paze button. The initialization is asynchronous, so call it from inside an `async` function. ```js theme={"system"} const paze = window.DIGITAL_WALLET_SDK; async function setupPaze(session) { await paze.initialize({ client: session }); } ``` ```html theme={"system"} ``` ## Step 3: Run the checkout When the shopper clicks the button, call `paze.checkout()` followed by `paze.complete()`. Generate a unique `sessionId` to correlate the two calls. The `complete()` response is the opaque token that your server sends to Gr4vy as `token`, and the `checkout()` response is sent as the optional `checkout_token` (named per your SDK's casing convention). ```js theme={"system"} const button = document.getElementById("paze-button"); button.addEventListener("click", async () => { const sessionId = crypto.randomUUID(); const { checkoutResponse, result } = await paze.checkout({ sessionId, transactionValue: { transactionAmount: "50.21", transactionCurrencyCode: "AUD", }, }); if (result === "COMPLETE") { const { completeResponse } = await paze.complete({ sessionId, transactionType: "PURCHASE", transactionValue: { transactionAmount: "50.21", transactionCurrencyCode: "AUD", }, }); // Send completeResponse and checkoutResponse to your server. } }); ``` > [!NOTE] > > The preceding example shows a minimal set of Paze SDK options. See the [`sample-standalone-paze`](https://github.com/gr4vy/sample-standalone-paze) repository for the full option set. ## Step 4: Create a transaction On your server, create the transaction with the token returned by `paze.complete()`. Set the payment method `method` to `paze` and `token` to the `complete()` response. The `checkout_token` is optional but recommended. Set it to the `checkout()` response to add the card last four digits, card type, expiration, and shipping address to the transaction. > [!WARNING] > > The Paze JavaScript SDK takes `transactionAmount` as a decimal string in major units (for example `"50.21"`), while Gr4vy's transaction `amount` is an integer in the smallest currency unit (for example `5021` for \$50.21). Convert between the two when bridging the values, and make sure they match for the same purchase. ```csharp C# theme={"system"} var transaction = await client.Transactions.CreateAsync( transactionCreate: new TransactionCreate() { Amount = 5021, Currency = "AUD", Country = "AU", PaymentMethod = TransactionCreatePaymentMethod.CreatePazePaymentMethodCreate( new PazePaymentMethodCreate() { Method = "paze", Token = completeResponse, CheckoutToken = checkoutResponse, } ), } ); ``` ```go Go theme={"system"} pazePaymentMethodCreate := components.PazePaymentMethodCreate{ Token: completeResponse, CheckoutToken: &checkoutResponse, } paymentMethod := components.CreateTransactionCreatePaymentMethodPazePaymentMethodCreate(pazePaymentMethodCreate) transactionCreate := components.TransactionCreate{ Amount: 5021, Currency: "AUD", Country: gr4vy.String("AU"), PaymentMethod: &paymentMethod, } transaction, err := client.Transactions.Create(ctx, transactionCreate, nil, nil) // handle response ``` ```java Java theme={"system"} CreateTransactionResponse transactionResponse = gr4vyClient.transactions().create() .transactionCreate(TransactionCreate.builder() .amount(5021L) .currency("AUD") .country("AU") .paymentMethod(TransactionCreatePaymentMethod.of(PazePaymentMethodCreate.builder() .token(completeResponse) .checkoutToken(checkoutResponse) .build())) .build()) .call(); Transaction transaction = transactionResponse.transaction().orElse(null); // handle response ``` ```php PHP theme={"system"} $transactionCreate = new TransactionCreate( amount: 5021, currency: 'AUD', country: 'AU', paymentMethod: new PazePaymentMethodCreate( method: 'paze', token: $completeResponse, checkoutToken: $checkoutResponse, ), ); $response = $sdk->transactions->create($transactionCreate); $transaction = $response->transaction; ``` ```python Python theme={"system"} transaction = client.transactions.create( amount=5021, currency="AUD", country="AU", payment_method={ "method": "paze", "token": complete_response, "checkout_token": checkout_response, }, ) ``` ```ts TypeScript theme={"system"} const transaction = await gr4vy.transactions.create({ amount: 5021, currency: "AUD", country: "AU", paymentMethod: { method: "paze", token: completeResponse, checkoutToken: checkoutResponse, }, }); ``` The response is a standard Gr4vy transaction. Handle its `status` as you would for any other payment method. ## Test Paze To test Paze, load your integration in the browser and complete a checkout with a Paze test wallet. Make sure your environment matches where Paze is configured, so that the browser loads the matching Paze SDK script: the sandbox script for `sandbox`, and the production script for `production`. > [!NOTE] > > Test wallet credentials are issued by Paze, not by Gr4vy. To obtain a test wallet for your merchant, work with Paze directly through your Paze onboarding contact. For general sandbox environment information, see Paze's [Environments guide](https://developer.paze.com/design-sandbox/docs/environments). # Recurring transactions with Embed Source: https://integration.wpay.com.au/guides/features/recurring-payments/embed Embed automatically tries to apply the right flags based on the situation, mainly using the provided `paymentSource` property and whether a stored card is used for the transaction. By default, the flags from the [E-commerce](./scenarios#e-commerce) scenario are used for a regular card payment. For transactions where the card is stored, Embed applies the properties from the [Fast checkout](./scenarios#fast-checkout) scenario. A `paymentSource` prop can be set when initializing Embed and it can be set to either `installment` or `recurring`. In those cases, the card is always stored (forcing `store=true`) and every transaction is marked as being the first in a series (`is_subsequent_payment=false`). As a result, with `paymentSource` set to `installment` or `recurring` you are provided with a payment method that can be re-used to create subsequent merchant-initiated subsequent transactions. # Mastercard Transaction Link Identifier (TLID) Source: https://integration.wpay.com.au/guides/features/recurring-payments/mastercard-tlid How Gr4vy captures and sends the Mastercard Transaction Link Identifier on recurring and other merchant-initiated card payments, and which connectors support it. > [!NOTE] > > Last updated: October 26, 2026. ## What the TLID is The Mastercard Transaction Link Identifier (TLID) links every payment in a stored credential series back to the customer-initiated payment that started it. Mastercard generates the TLID on that first customer-initiated transaction (CIT) and returns it in the authorization response. Mastercard began returning the TLID on June 2, 2026. From October 23, 2026, Mastercard requires the TLID on every later merchant-initiated transaction (MIT) in a stored credential series, such as a subscription renewal or an unscheduled card-on-file charge. The TLID runs alongside the existing scheme transaction ID, also known as the Mastercard trace ID. It doesn't replace it, and Gr4vy continues to send both. > [!NOTE] > > Some acquirers communicate their own compliance dates for the TLID. Check with your acquirer for the dates that apply to you. ## How Gr4vy handles the TLID On a [supported connector](#connector-coverage), Gr4vy handles the TLID for you: 1. Gr4vy reads the TLID from the response to a successful customer-initiated payment with `payment_source` set to `recurring`, `installment`, or `card_on_file`, starting with the first payment in a series. 2. Gr4vy stores the TLID with the stored payment method. A later qualifying customer-initiated payment replaces it, and a merchant-initiated payment only stores a TLID when the payment method has none. 3. On each later merchant-initiated payment with that payment method, Gr4vy sends the TLID to the connector automatically. These are the same rules Gr4vy uses for the scheme transaction ID. See [Scheme transaction IDs on stored payment methods](/guides/features/recurring-payments/overview#scheme-transaction-ids-on-stored-payment-methods). No integration changes are needed, unless your payment service provider (PSP) has to switch the TLID on for your account. See [enabling the TLID with your PSP](#enabling-the-tlid-with-your-psp). This relies on each transaction being flagged correctly as customer-initiated or merchant-initiated. Set `payment_source`, `merchant_initiated`, and `is_subsequent_payment` as described in the [recurring payment flags](/guides/features/recurring-payments/overview#recurring-payment-flags) and [common scenarios](/guides/features/recurring-payments/scenarios). ### API fields The TLID appears in the API as follows: | Field | Resource | Description | | - | - | - | | `transaction_link_id` | [Transaction](/reference/transactions/get-transaction) | The TLID returned by the scheme for this transaction, when available. | | `transaction_link_id` | [Payment method](/reference/payment-methods/get-payment-method) | The TLID stored against the payment method. | | `previous_transaction_link_id` | [New transaction](/reference/transactions/new-transaction) request | The TLID to send on a merchant-initiated transaction. | You can supply your own TLID in two ways: * **Per transaction** - Set `previous_transaction_link_id` when you create a merchant-initiated transaction. A value in the request takes precedence. If you leave it out, Gr4vy fills it in with the TLID stored on the payment method. * **On the payment method** - Set `transaction_link_id` when you [update a payment method](/reference/payment-methods/update-payment-method). Gr4vy then sends that value on later merchant-initiated transactions with the payment method, until a later qualifying customer-initiated transaction replaces it. If you process merchant-initiated transactions without a stored payment method, pass the `transaction_link_id` from the first transaction as `previous_transaction_link_id` on each later transaction. ## Series that started before June 2, 2026 A stored credential series that started before June 2, 2026 has no TLID on file, because Mastercard didn't return one at the time. For these series, Gr4vy only sends a TLID when one is stored on the payment method or supplied in the request. If you hold a TLID for such a series, for example from your acquirer, store it on the payment method or pass it as `previous_transaction_link_id`. The same applies to cards you import from another provider. To import a TLID with the card, include it in the token import file as `payment_method.transaction_link_id`. See [Token import file](/guides/features/token-import/token-import-file). If the import file has no TLID, set one on the payment method yourself if you have it. ## Connector coverage The following table shows TLID support by connector. ### Supported | Connector | TLID support | PSP enablement needed | Notes | | - | - | - | - | | [Adyen](/connections/payments/adyen-card) | Yes | No | | | [Airwallex](/connections/payments/airwallex-card) | Yes | No | | | [Chase Orbital](/connections/payments/chaseorbital-card) | Yes | No | | | [Checkout.com](/connections/payments/checkoutcom-card) | Yes | No | | | [Cybersource](/connections/payments/cybersource-card) | Yes | No | | | [dLocal](/connections/payments/dlocal-card) | Yes | No | dLocal returns the TLID subject to acquirer coverage, so it may be missing for some acquirers. | | [Fiserv IPG](/connections/payments/fiserv-card) | Yes | No | | | [Mastercard Gateway](/connections/payments/mastercard-card) | Yes | No | | | Ozpay | Yes | No | | | [Shift4](/connections/payments/shift4-card) | Yes | No | | | [Stripe](/connections/payments/stripe-card) | Yes | No | Applies to open-loop. In the default closed-loop configuration, Stripe keeps and replays the TLID itself. | | [Trust Payments](/connections/payments/trustpayments-card) | Yes | No | Closed-loop connector. Gr4vy stores the TLID and sends it along with the Trust Payments token. | | [Worldline Connect](/connections/payments/worldlineconnect-card) | Yes | No | Worldline is rolling out the TLID in its responses in phases, by acquiring partner. | ### Supported with PSP action | Connector | TLID support | PSP enablement needed | Notes | | - | - | - | - | | [Ecommpay](/connections/payments/ecommpay-card) | Yes | Yes | Ecommpay must turn on the TLID callback parameters for your production project. | | [Nuvei](/connections/payments/nuvei-card) | Yes | Yes | Nuvei must enable the TLID on each merchant account. | | [Worldline TravelHub](/connections/payments/travelhub-card) | Yes | Confirm with Worldline | Worldline is still rolling out the field. | | [Worldpay WPG](/connections/payments/worldpaywpg-card) | Yes | Yes | Worldpay must enable the TLID on your merchant code. | | Wpay | Yes | Confirm with Wpay | Wpay forwards the TLID to the scheme once its switch upgrade is in place. | ### Handled by the provider For these closed-loop connectors, cards are stored with the provider, which keeps and replays the TLID itself. Nothing is needed from you or from Gr4vy. | Connector | TLID support | PSP enablement needed | Notes | | - | - | - | - | | Adyen Vault | Handled by provider | No | | | [Buckaroo](/connections/payments/buckaroo-card) | Handled by provider | No | | | [CardPointe](/connections/payments/cardpointe-card) | Handled by provider | No | | | Eckoh | Handled by provider | No | | | [Finix](/connections/payments/finix-card) | Handled by provider | No | | | FreedomPay | Handled by provider | No | | | [Moneris](/connections/payments/moneris-card) | Handled by provider | No | | | [NMI](/connections/payments/nmi-card) | Handled by provider | No | | | [Planet](/connections/payments/planet-card) | Handled by provider | No | | | [PowerTranz](/connections/payments/powertranz-card) | Handled by provider | No | | | [Repay](/connections/payments/repay-card) | Handled by provider | No | | | [Ryft](/connections/payments/ryft-card) | Handled by provider | No | | | [Shift4 i4Go](/connections/payments/shift4i4go-card) | Handled by provider | No | Tokenization only. You make payments through your own Shift4 integration, so confirm TLID handling with Shift4. | | Stripe Token | Handled by provider | No | | | [USAePay](/connections/payments/usaepay-card) | Handled by provider | No | | | [Windcave](/connections/payments/windcave-card) | Handled by provider | No | | ### Not yet supported | Connector | TLID support | PSP enablement needed | Notes | | - | - | - | - | | [ACI Worldwide](/connections/payments/aci-card) | No | Not applicable | | | [Authorize.net](/connections/payments/authorizenet-card) | No | Not applicable | The Authorize.net API has no TLID field. | | [BlueSnap](/connections/payments/bluesnap-card) | No | Not applicable | | | [Braintree](/connections/payments/braintree-card) | No | Not applicable | Braintree returns the TLID and Gr4vy stores it, but Braintree doesn't yet accept it on merchant-initiated payments for cards stored outside Braintree. | | [Merchant Warrior](/connections/payments/merchantwarrior-card) | No | Not applicable | | | [PayPal](/connections/payments/paypal-card) (cards) | No | Not applicable | | | [Unlimit](/connections/payments/unlimit-card) | No | Not applicable | | | [Worldpay VAP](/connections/payments/worldpayvap-card) | No | Not applicable | Worldpay plans to add support in its April 2027 release. | ## Enabling the TLID with your PSP For the following connectors, Gr4vy supports the TLID, but your PSP has to act before it reaches Mastercard. * **Ecommpay** - For Gr4vy to receive the TLID, Ecommpay must turn on the TLID callback parameters for your production project. Ask your Ecommpay contact to enable them. * **Nuvei** - Nuvei must enable the TLID on each merchant account. Ask your Nuvei account manager. Until it's enabled, Nuvei accepts the TLID without an error but doesn't pass it on to Mastercard. * **Worldline TravelHub** - Worldline is still rolling out the field, so confirm availability with Worldline. If you process through Navitaire, Sabre, or Outpayce, take it up with that partner instead. * **Worldpay WPG** - Worldpay must enable the TLID on your merchant code. Ask your Worldpay account manager. * **Wpay** - Wpay forwards the TLID to the scheme only once its own switch upgrade is in place. Confirm the timing with Wpay. # Recurring payments Source: https://integration.wpay.com.au/guides/features/recurring-payments/overview Flag recurring card payments correctly, and learn how Gr4vy stores and sends scheme transaction IDs for stored cards. To support recurring billing some connections (mainly card processors) require transactions to be marked accordingly to properly indicate if a transaction is part of a recurring sequence of transactions. It is important to set these indicators correctly to ensure a successful authorization for subsequent transactions. Recurring transactions can generally be categorized as scheduled payments, unscheduled payments, subscriptions, or installments. ## Recurring payment flags To facilitate recurring payments, three properties are supported that can be set [when creating a new card transaction](/reference/transactions/new-transaction). * `payment_source` - The source of the transaction. This defaults to `ecommerce` and can be set to `recurring`, `installment`, or `card_on_file`. * `merchant_initiated` - Indicates whether the transaction was initiated by the merchant (`true`) or if the customer was present (`false`). * `is_subsequent_payment` - Indicates whether the transaction represents a subsequent payment. Please note this flag is only compatible when the `payment_source` is set to `recurring`, `installment`, or `card_on_file` and is ignored for other values or if `payment_source` is not present. These 3 properties are automatically mapped to your downstream payment service's properties, where they apply. Currently, these mostly only apply to card processors. > [!WARNING] > > When you charge card details that aren't stored with Gr4vy, provide `previous_scheme_transaction_id` and > `previous_transaction_link_id` on each subsequent merchant-initiated transaction (MIT) where possible. Use the > `scheme_transaction_id` and `transaction_link_id` returned on the customer-initiated transaction that set up the series. > > If you use a stored payment method, this is handled for you automatically. A stored payment method can only be used > for one recurring series. See > [Scheme transaction IDs on stored payment methods](#scheme-transaction-ids-on-stored-payment-methods) for the rules, > and [Mastercard Transaction Link Identifier (TLID)](/guides/features/recurring-payments/mastercard-tlid) for how the > `transaction_link_id` is captured and which connectors support it. ## Scheme transaction IDs on stored payment methods Card schemes link a merchant-initiated transaction back to the customer-initiated transaction that set up the stored credential. The link is the scheme transaction ID, also known as the Visa Transaction Identifier or Mastercard trace ID, and for Mastercard also the [Transaction Link Identifier (TLID)](/guides/features/recurring-payments/mastercard-tlid). Gr4vy stores both on the payment method, as `scheme_transaction_id` and `transaction_link_id`, and you can read them with [Get payment method](/reference/payment-methods/get-payment-method). ### Which transactions store an ID Gr4vy saves the IDs that the payment service returns on a successful transaction, following these rules. * Only transactions with `payment_source` set to `recurring`, `installment`, or `card_on_file` store an ID. A transaction with any other `payment_source`, such as `ecommerce` or `moto`, stores nothing, even when it stores the card with `store: true`. * A customer-initiated transaction (`merchant_initiated: false`) replaces the stored IDs with the ones it returns. * A merchant-initiated transaction only stores an ID when the payment method doesn't have one yet. * When the payment service doesn't return an ID, the stored value stays as it is. After the transaction that sets up a series, check `scheme_transaction_id` on the transaction to confirm that the payment service returned one. > [!NOTE] > > A payment service may not return a scheme transaction ID for a payment processed on a domestic co-badged network. > A card stored with such a payment has no reference for later merchant-initiated transactions. ### When Gr4vy sends the stored ID Gr4vy sends the stored IDs as the previous scheme transaction ID and previous transaction link ID on: * Every merchant-initiated transaction with the payment method. * Subsequent card-on-file customer-initiated transactions, with `payment_source` set to `card_on_file`, `merchant_initiated` set to `false`, and `is_subsequent_payment` set to `true`. A `previous_scheme_transaction_id` or `previous_transaction_link_id` in the transaction request takes precedence over the stored value. Gr4vy doesn't save these request values on the payment method. An ID belongs to the card network of the transaction that returned it. Gr4vy leaves the scheme transaction ID out of an attempt that's processed on a different network, with one exception for domestic co-badged networks. ## Best practices Follow these best practices when creating recurring payments. * When creating a customer-initiated transaction (CIT), with `merchant_initiated=false`, providing the `security_code` (CVV) is highly recommended. Not doing so could result in declined transactions. * Before creating a merchant-initiated transaction (MIT), with `merchant_initiated=true`, it's highly recommended to make an initial CIT first so the customer gives their authorization. Any subsequent transaction is then identified as being authorized by this first transaction. * Before each merchant-initiated transaction with a stored payment method, check the payment method's `status` with [Get payment method](/reference/payment-methods/get-payment-method). Only charge payment methods with the status `succeeded`. A transaction with a payment method in any other status fails with a validation error. See [payment method statuses](/guides/api/statuses/payment-methods). * When performing a subsequent MIT transaction without storing the card data in the vault, provide the `previous_scheme_transaction_id` and `previous_transaction_link_id` where possible. With a stored payment method, Gr4vy sends these for you, as described in [Scheme transaction IDs on stored payment methods](#scheme-transaction-ids-on-stored-payment-methods). # Common scenarios Source: https://integration.wpay.com.au/guides/features/recurring-payments/scenarios Set the recurring payment flags for common checkout scenarios, from one-off payments to subscriptions and installments. The following is a list of common scenarios that merchants use when creating transactions. Your use case depends on how your checkout is handled. > [!NOTE] > > Some payment services set how a stored payment method can be used from the first transaction that > stores it, based on its `payment_source`. On these services, a payment method first stored with > `card_on_file` may not support later merchant-initiated recurring or installment payments. Pick the > scenario that matches every way you plan to charge the payment method, and check the > [connection](/connections/payments/overview) for your payment service. ## E-commerce A customer provides their payment details for a one-off payment and the payment method is not stored. The transaction is created directly after the payment details are provided. * `payment_source=ecommerce` * `merchant_initiated=false` * `is_subsequent_payment=false` This is the end of this scenario. There is no merchant-initiated part. ## Fast checkout A customer at checkout agrees to store their card for quicker checkouts in the future. * `payment_source=card_on_file` * `merchant_initiated=false` * `is_subsequent_payment=false` The same customer then returns to the checkout at a later point and decides to checkout with this stored card. * `payment_source=card_on_file` * `merchant_initiated=false` * `is_subsequent_payment=true` > [!NOTE] > > Embed automatically applies the properties in this scenario for new and stored > payment methods when the `paymentSource` prop is left to its default value. ## Unscheduled payments A customer at checkout agrees to store their card for future transactions, and allows the merchant to initiate an unscheduled transaction in the case that there are any additional charges they incur on their account. * `payment_source=card_on_file` * `merchant_initiated=false` * `is_subsequent_payment=false` At a later point, this same customer might incur some extra charges on their account. The merchant can then charge the card while the customer is not present. * `payment_source=card_on_file` * `merchant_initiated=true` * `is_subsequent_payment=true` The key difference between this scenario and a subscription is that a subscription is generally handled as a scheduled payment with a predictable amount and recurrence, where an unscheduled payment can occur outside of a schedule and for any amount. > [!WARNING] > > If you don't store the card with Gr4vy, provide `previous_scheme_transaction_id` and `previous_transaction_link_id` > on each subsequent MIT transaction where possible. Use the `scheme_transaction_id` and `transaction_link_id` returned > on the customer-initiated transaction that set up the series. > > If you use a stored payment method, this is handled for you automatically. A stored payment method can only be used > for one recurring series. See > [Scheme transaction IDs on stored payment methods](./overview#scheme-transaction-ids-on-stored-payment-methods). ## Installments A customer at checkout agrees to pay for goods over a certain amount of installments. * `payment_source=installment` * `merchant_initiated=false` * `is_subsequent_payment=false` A customer is then charged for each subsequent installment by the merchant when the payment is due. The customer is not present for this transaction. * `payment_source=installment` * `merchant_initiated=true` * `is_subsequent_payment=true` The key difference between this scenario and a subscription is that an installment has a finite number of transactions, where a subscription is generally recurring until the subscription is canceled. > [!WARNING] > > If you don't store the card with Gr4vy, provide `previous_scheme_transaction_id` and `previous_transaction_link_id` > on each subsequent MIT transaction where possible. Use the `scheme_transaction_id` and `transaction_link_id` returned > on the customer-initiated transaction that set up the series. > > If you use a stored payment method, this is handled for you automatically. A stored payment method can only be used > for one recurring series. See > [Scheme transaction IDs on stored payment methods](./overview#scheme-transaction-ids-on-stored-payment-methods). ## Installments split by the issuer An installment series, as in the [Installments](#installments) scenario, is a set of separate transactions that you charge over time. In some markets, the card issuer or acquirer can instead split one payment into installments. You create a single customer-initiated payment, and the buyer repays it in installments. To request this, set `installment_count` to the number of installments. The payment itself is a one-off customer-initiated payment, so don't set `payment_source` to `installment`, which marks a series of merchant-initiated payments. * `installment_count=3` * `payment_source=ecommerce` * `merchant_initiated=false` * `is_subsequent_payment=false` The value can be from 1 to 100 and has no default. Gr4vy passes it to the payment service on connections that support issuer-based installments, such as [Adyen](/connections/payments/adyen-card), [dLocal](/connections/payments/dlocal-card), [Fiserv](/connections/payments/fiserv-card), [Stripe](/connections/payments/stripe-card), and [Worldpay WPG](/connections/payments/worldpaywpg-card). ## Subscriptions A customer at checkout agrees to pay for a regular recurring subscription for a certain amount, paid at a fixed interval. * `payment_source=recurring` * `merchant_initiated=false` * `is_subsequent_payment=false` This customer is then charged for each subsequent payment when the payment is due. The customer is not present for this transaction. * `payment_source=recurring` * `merchant_initiated=true` * `is_subsequent_payment=true` > [!WARNING] > > If you don't store the card with Gr4vy, provide `previous_scheme_transaction_id` and `previous_transaction_link_id` > on each subsequent MIT transaction where possible. Use the `scheme_transaction_id` and `transaction_link_id` returned > on the customer-initiated transaction that set up the series. > > If you use a stored payment method, this is handled for you automatically. A stored payment method can only be used > for one recurring series. See > [Scheme transaction IDs on stored payment methods](./overview#scheme-transaction-ids-on-stored-payment-methods). ## MOTO Customer's card data is collected via mail (Mail Order) or telephone (Telephone Order) and typed in by a support agent or an automated scanning process. * `payment_source=moto` * `merchant_initiated=false` * `is_subsequent_payment=false` Optionally, a merchant can then charge the customer when a recurring payment is due. The customer is not present for this and so the transaction should be created as a MIT transaction. * `payment_source=recurring`, `installment`, or `card_on_file` * `merchant_initiated=true` * `is_subsequent_payment=true` > [!WARNING] > > Provide `previous_scheme_transaction_id` and `previous_transaction_link_id` on each subsequent MIT > transaction where possible, using the `scheme_transaction_id` and `transaction_link_id` returned on the MOTO transaction. > > If you use a stored payment method, this is handled for you automatically. A stored payment method can only be used > for one recurring series. See > [Scheme transaction IDs on stored payment methods](./overview#scheme-transaction-ids-on-stored-payment-methods). # Shipment tracking Source: https://integration.wpay.com.au/guides/features/shipment-tracking/overview Attach shipment tracking details to a capture and forward them to payment services that support them. When you capture a payment after the goods have shipped, you can attach the shipment tracking details to the capture. Gr4vy stores them with the capture and forwards them to payment services that support them. Tracking details work with any delayed capture, whether you capture a payment once or in [multiple captures](/guides/features/multi-captures/overview). ## Add tracking details to a capture Include a `tracking` array when you [capture a transaction](/reference/transactions/capture-transaction). Each entry describes one shipment, and a capture can include up to 5 entries. | Field | Required | Description | | - | - | - | | `number` | Yes | The tracking number for the shipment. | | `carrier` | Yes | The carrier handling the shipment, for example `ups`. Use `other` if the carrier isn't in the list of supported values. | | `url` | No | A URL where the buyer can track the shipment. | See the [capture transaction](/reference/transactions/capture-transaction) reference for the full list of supported carriers. ```http theme={"system"} POST /transactions/{transaction_id}/capture ``` ```json theme={"system"} { "amount": 1299, "tracking": [ { "number": "1Z999AA10123456784", "carrier": "ups", "url": "https://www.ups.com/track?tracknum=1Z999AA10123456784" } ] } ``` ## Payment service support Gr4vy accepts tracking details on captures for every connection. They're only forwarded to payment services that support them. To see which connectors support shipment tracking, filter the [connector catalog](/connections/catalog?supports=shipment-tracking) or check the **Capabilities** section of each connector's page. Each carrier is mapped to the equivalent value that the payment service expects. If a payment service has no matching value, Gr4vy uses its generic "other" or "unknown" value where one exists, and otherwise sends no carrier value. Some payment services don't accept every field. Check the connector's page for any differences in how tracking details are handled. ## Retrieve tracking details Gr4vy always stores tracking details on the capture record. This is the case even when they weren't forwarded to the payment service, or when the capture or tracking request to the payment service failed. You can retrieve them in the following ways: * Call [list transaction captures](/reference/transactions/list-transaction-captures) or [get transaction capture](/reference/transactions/get-transaction-capture). Each capture includes its `tracking` details. * Set the `Prefer: resource=transaction-capture` header when you capture the transaction. The response is then a `transaction-capture` resource that includes `tracking`, rather than the default `transaction` resource. See [Prefer header](/guides/api/prefer-header) for details. # Transaction simulators Source: https://integration.wpay.com.au/guides/features/simulators/overview Payment simulators are provided for card, PayPal, and SPEI payments, allowing you to test card payments, redirect payments, and push payments respectively. These simulators do not connect to any PSP and instead provide mocked responses based on the amounts or other values passed in. > [!NOTE] > > An [anti-fraud > simulator](/guides/features/anti-fraud/simulator), [gift card > simulator](/guides/features/gift-cards/simulator) and a way to [simulate > real-time account > updates](/guides/features/account-updater/real-time#simulator) are also provided. ## Setup To set up a simulator, head over to your sandbox dashboard and go to the connections catalog. Once there, each simulator is available to set up with a custom merchant ID. This ID does not serve any purpose. Please note that simulators are not available in production environments. ## Test values > [!NOTE] > > A card's type, such as credit, debit, or prepaid, comes from a lookup of the card's bank > identification number (BIN), not from how a payment service labels its test cards. The type > used in the **Card type** condition of the [card transactions Flow](/guides/dashboard/flow/card-transactions) > is the result of this lookup. ### Payments When creating a payment, the following test values can be used to simulate various error codes. > [!NOTE] > > Simulator amounts are in the smallest currency unit, the same as the `amount` > on a transaction. For example, `200001` is 2000.01 in a currency with two > decimal places. Send the integer value exactly as listed. | Simulator amount | `error_code` | | :- | :- | | 200001 | `canceled_payment_method` | | 200002 | `disputed_transaction` | | 200003 | `duplicate_transaction` | | 200004 | `expired_authorization` | | 200005 | `expired_payment_method` | | 200006 | `incorrect_billing_address` | | 200007 | `incorrect_country` | | 200008 | `incorrect_currency` | | 200009 | `incorrect_cvv` | | 200010 | `incorrect_expiry_date` | | 200011 | `insufficient_funds` | | 200012 | `issuer_decline` | | 200013 | `other_decline` | | 200014 | `requires_buyer_authentication` | | 200015 | `refused_transaction` | | 200016 | `service_decline` | | 200017 | `suspected_fraud` | | 200018 | `unavailable_payment_method` | | 200019 | `unknown_payment_method` | | 200020 | `unsupported_transaction` | | 200021 | `unsupported_payment_method` | | 200022 | `cancelled_buyer_approval` | | 300001 | `insufficient_service_permissions` | | 300002 | `invalid_amount` | | 300003 | `invalid_payment_method` | | 300004 | `invalid_service_configuration` | | 300005 | `invalid_service_credentials` | | 300006 | `invalid_service_response` | | 300007 | `invalid_tax_identifier` | | 300008 | `missing_billing_address` | | 300009 | `missing_cvv` | | 300010 | `missing_shipping_address` | | 300011 | `missing_tax_identifier` | | 300012 | `refund_period_expired` | | 300013 | `service_error` | | 300014 | `service_network_error` | | 300015 | `service_rate_limit` | | 400001 | `internal_error` | | 400002 | `invalid_billing_address` | | 400003 | `invalid_operation` | | 400004 | `invalid_request_parameters` | | 400005 | `invalid_service_request` | | 400006 | `invalid_shipping_address` | | 400007 | `service_resource_conflict` | | 400008 | `unexpected_state` | | 400009 | `unknown_error` | | 400010 | `unknown_service_resource` | | 400011 | `unsupported_country` | | 400012 | `unsupported_currency` | | 400013 | `unrecognised_country` | | 400014 | `unrecognised_currency` | | 400015 | `unrecognised_payment_method` | | 400016 | `unrecognised_scheme` | | 400017 | `unsupported_scheme` | The following amounts can be used to test pending outcomes: | Simulator amount | Outcome | New transaction `status` | | :- | :- | - | | 408 | Connector request times out. An asynchronous retry if scheduled idempotent retries are supported. | `processing` or `authorization_failed` | | 102 | Connector returns a pending status. | `processing` | ### 3-D Secure The card simulator supports PSP-hosted [3-D Secure](/guides/features/3ds/overview), where the payment service performs the authentication as part of the transaction. To simulate an authentication outcome, include a `metadata` value in the request with `"require_three_d_secure": "true"` and prefix the simulator amount with one of the following values: | Prefix | Outcome | Redirect | | :- | :- | :- | | 90 | Challenge success | Yes | | 91 | Frictionless success | Yes | | 92 | Challenge failed | Yes | | 93 | Authentication failed | Yes | | 94 | Frictionless success | No | | 95 | Authentication failed | No | Prefixes `90` to `93` return a `buyer_approval_pending` transaction with a redirect URL, and the authentication outcome is applied when the buyer returns from it. Prefixes `94` and `95` complete the authentication as part of the create request, so the 3-D Secure data is returned in the initial response without a redirect. You can combine one of these prefixes with another simulator amount to control both the authentication outcome and the payment result. For example, an amount of `90200001` returns the challenge success data together with a `canceled_payment_method` error code. ### Captures When capturing an authorized payment, the following responses can be simulated. When capturing a payment, the following test values can be used to simulate various error codes. | Simulator amount | New transaction `status` | `code` | | :- | :- | - | | 10 | `authorization_declined` | `issuer_decline` | | 20 | `capture_pending` | - | | 30 | `authorization_succeeded` | `service_error` | | 40 | `authorization_succeeded` | `internal_error` | Additionally, the `408` amount can be used to simulate a connector request that times out. An asynchronous retry is scheduled if idempotent retries are supported. > [!WARNING] > > Transactions in a `capture_pending` state can be moved to the next state using > [webhooks](./webhooks). ### Void When voiding an authorized payment, the following responses can be simulated. When creating a payment, the following test values can be used to simulate various error codes. | Simulator amount | New transaction `status` | `code` | | :- | :- | - | | 10 | `authorization_declined` | `issuer_decline` | | 20 | `authorization_void_pending` | - | | 30 | `authorization_succeeded` | `service_error` | | 40 | `authorization_succeeded` | `internal_error` | > [!WARNING] > > Transactions in a `authorization_void_pending` state can be moved to the next > state using [webhooks](./webhooks). ### Refunds When refunding a card payment, the following responses can be simulated. When creating a payment, the following test values can be used to simulate various error codes. | Simulator amount | `status` | | :- | :- | | 1 | `declined` | | 2 | `processing` | | 3 | `failed` | | 4 | `failed` | ### Scheme transactions When creating a card payment, random `scheme_transaction_id` and `transaction_link_id` values are generated. An amount of 10001 can be used to simulate a missing `scheme_transaction_id` and `transaction_link_id`. # Transaction simulator webhooks Source: https://integration.wpay.com.au/guides/features/simulators/webhooks Post webhook payloads to move simulated transactions, payouts, and refunds to their next state. Some simulator responses leave a resource in a pending state, such as a transaction marked as `capture_pending` or a payout marked as `processing`. You can move these resources to their next state by posting a webhook payload to the connector's webhook URL. To send a webhook, grab the webhook URL for the connector from your dashboard, and then post a JSON payload with the expected values. The simulator routes each payload using its `event` field, so every payload must include an `event` value that matches one of the supported events below. The `payment_service_transaction_id` needs to be set to the ID of the transaction. The rest of the fields are optional. ## Authorizations ### Authorization succeeded Example payload: ```json theme={"system"} { "event": "authorization.succeeded", "transaction_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "payment_service_transaction_id": "tx-12345", "scheme_transaction_id": "123456789012345", "payment_service_authorization_id": "auth-12345", "authorized_amount": 1299, "transaction_link_id": "123456789012345", "iso_response_code": "00", "raw_response_code": "00", "raw_response_description": "Approved", "merchant_advice_code": "01" } ``` ### Authorization failed Example payload: ```json theme={"system"} { "event": "authorization.failed", "transaction_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "payment_service_transaction_id": "tx-12345", "scheme_transaction_id": "123456789012345", "payment_service_authorization_id": "auth-12345", "authorized_amount": 0, "transaction_link_id": "123456789012345", "iso_response_code": "07", "raw_response_code": "E104", "raw_response_description": "System error", "merchant_advice_code": "01" } ``` ## Captures ### Capture succeeded Example payload: ```json theme={"system"} { "event": "capture.succeeded", "transaction_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "payment_service_transaction_id": "tx-12345", "scheme_transaction_id": "123456789012345", "payment_service_capture_id": "capture-12345", "capture_id": "b2c3d4e5-f6a7-8901-bcde-f21234567890", "capture_amount": 1299, "transaction_link_id": "123456789012345", "iso_response_code": "00", "raw_response_code": "00", "raw_response_description": "Approved", "merchant_advice_code": "01" } ``` ### Capture failed Example payload: ```json theme={"system"} { "event": "capture.failed", "transaction_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "payment_service_transaction_id": "tx-12345", "payment_service_capture_id": "capture-12345", "capture_id": "b2c3d4e5-f6a7-8901-bcde-f21234567890", "iso_response_code": "96", "raw_response_code": "E104", "raw_response_description": "System error" } ``` ## Refunds ### Refund succeeded Example payload: ```json theme={"system"} { "event": "refund.succeeded", "refund_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "payment_service_refund_id": "REFUND1234567890", "amount": 1299 } ``` ## Payouts ### Payout succeeded Example payload: ```json theme={"system"} { "event": "payout.succeeded", "payout_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "xid": "PAYOUT1234567890", "raw_response_code": "00", "raw_response_description": "Approved" } ``` ### Payout declined The simulator uses a `service_decline` code by default. Example payload: ```json theme={"system"} { "event": "payout.declined", "payout_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "xid": "PAYOUT1234567890", "code": "service_decline", "raw_response_code": "E104", "raw_response_description": "Declined" } ``` ### Payout failed The simulator uses a `service_error` code by default. Example payload: ```json theme={"system"} { "event": "payout.failed", "payout_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "xid": "PAYOUT1234567890", "code": "service_error", "raw_response_code": "E104", "raw_response_description": "System error" } ``` > [!WARNING] > > Payout and refund webhooks are only available for the card simulator. # Creating buyers & payment methods on import Source: https://integration.wpay.com.au/guides/features/token-import/buyer-payment-creation For each unique buyer ID in the token import file, a new buyer record is created in your instance. All fields that are passed in for the buyer are linked to the respective fields of a [buyer record](/reference/buyers). Similarly, for each payment method in the token import file, a new payment method record is created in your instance. Additionally, if a buyer is present the payment method is linked to the created buyer. The buyer's `external_identifier` field retains the value you passed in the import file and a unique internal ID field is produced as a part of the import process. ## Merchant advice file Once all tokens, buyer, and payment methods were imported, a **Merchant Advice File** ([example](https://gist.githubusercontent.com/phillipgr4vy/f9ca91d6d4163842bd52e5fb1a3858d9/raw/b37d10c95215be9cf07d4afa82004abcfea202c0/merchant_advice_file.csv)) is provided which logs the successful and unsuccessful imports of all resources as part of this process. * For each row attempted, a corresponding log item is shown with an audit reference tag * The ID for each created buyer record created is outputted * The ID for each created payment method created is outputted * A list of failures and validation errors is outputted The file has one row per imported row, with the following columns. | Column | Description | | - | - | | `audit_reference_id` | The `audit_reference_id` of the row in your import file. | | `status` | Either `successful` or `failed`. | | `buyer_external_identifier` | The buyer's `external_identifier` from your import file. | | `payment_method_external_identifier` | The payment method's `external_identifier` from your import file, for example the token your previous payment service used for it. | | `buyer_id` | The ID of the buyer created in Gr4vy. | | `payment_method_id` | The ID of the payment method created in Gr4vy. Use this ID to charge the payment method. | | `card_bin` | The card's BIN, for card imports. | | `card_last4` | The last four digits of the card, for card imports. | | `card_expiration_date` | The card's expiration date, for card imports. | | `card_fingerprint` | The card's [fingerprint](/guides/features/cards/fingerprints), which helps you find duplicate cards. | | `notes` | The reason a row failed. | For each `successful` row, store at least the `buyer_id` and `payment_method_id` against your own records. Rows marked `failed` aren't imported. Correct them using the `notes` column and send them again in a new file. # Vault token import Source: https://integration.wpay.com.au/guides/features/token-import/overview Migrate stored cards, digital wallets, and other payment data from your current provider into the vault. A token import allows you to migrate your payment data from your current payment provider into the vault. Cards (raw PAN), digital wallets (Apple Pay and Google Pay), non-card tokens, bank accounts, and gift cards are all supported. To start this process, contact the team. This guide helps you understand the basic principles of the token import process. This process may not be supported by all providers in which case a custom process can be established. The token import process is comprised of the following steps and involves you (the merchant), your original payment service (the PSP), either of which can be the **Token holder**. * Token holder creates a token file using the file template and definitions. * Token holder encrypts the token file created using the public PGP encryption key that the team shares with you for the import. * Token holder uploads the encrypted token file to a cloud-signed storage URL provided. * The tokens are processed and imported into your instance. Additionally, new buyers and payment methods are created from the token import file. * A **Merchant Advice file** is provided that provides you with a log of all the succeeded and failed imports. ## Before you migrate * Ask the token holder for an export of the full card numbers. Card tokens issued by your current payment service can't be imported, as they only work with that payment service. * Ask the token holder to include the latest network transaction ID for each card in the token file, as `payment_method.scheme_transaction_id`. Without it, merchant-initiated transactions on imported cards can't reference the previous transaction. See [Token import file](/guides/features/token-import/token-import-file). * For Mastercard cards in a recurring or card-on-file series, also ask for the [Mastercard Transaction Link Identifier (TLID)](/guides/features/recurring-payments/mastercard-tlid), as `payment_method.transaction_link_id`. Gr4vy then sends it on merchant-initiated transactions with the imported card. * Gr4vy only records refunds, chargebacks, and other updates for transactions that it processed. Updates from your payment service about transactions made before the migration aren't recorded, so keep handling those at your payment service. ## Migrating through the API As an alternative to a token file, the vault that holds the cards can forward each card's details to the [Create payment method](/reference/payment-methods/new-payment-method) endpoint. This creates a stored card for each request. Digital wallet tokens, such as Apple Pay and Google Pay, can't be created through the API, so import them with a token file. # Token import file upload Source: https://integration.wpay.com.au/guides/features/token-import/token-file-transmission To successfully import your tokens, please encrypt the import file using the PGP public key that the team shares with you and send it to the provided GCP signed URL. ## Encryption Merchants **must** encrypt the token file before sending it. A suggested approach to encryption is using [GnuPG](https://www.gnupg.org) interface that can be used to encrypt files using PGP public keys. Please refer to the GnuPG documentation to learn how to [add the public key to your keyring](https://www.gnupg.org/gph/en/manual/x56.html) and how to [encrypt your token file](https://www.gnupg.org/gph/en/manual/x110.html) with the key. ## File transfer When you request a token import, a Google Cloud Storage signed URL is provided. This allows you to upload files directly via HTTPS. Sending to this Google Cloud Storage signed URL can be accomplished in a variety of ways described in [this Google Cloud storage guide](https://cloud.google.com/storage/docs/uploading-objects). For example, using cURL this request would look something like this. ```sh theme={"system"} curl -X PUT -H 'Content-Type: text/plain' --upload-file tokens.csv.gpg google_signed_cloud_storage_url ``` > [!NOTE] > > In this example, `google_signed_cloud_storage_url` is the URL provided, and `tokens.csv.gpg` is the name of the encrypted token file. ## Files sent by your payment service When your previous payment service creates the token file, it must also encrypt it with the Gr4vy public PGP encryption key that the team shares with you, not with a key of its own. If your payment service can't upload to a signed URL, contact the team to arrange an SFTP account instead. # Token import file creation Source: https://integration.wpay.com.au/guides/features/token-import/token-import-file Create a token import file with the column definitions for cards, digital wallets, non-card tokens, bank accounts, and gift cards. To allow for a smooth import, a template has been created that can be used for a generic token import from your current payment service to the system, These templates: [card template](https://raw.githubusercontent.com/gr4vy/token-import-templates/main/token_import_template.csv) [digital wallets](https://raw.githubusercontent.com/gr4vy/token-import-templates/main/token_import_template_digital_wallet.csv) [non-card tokens](https://raw.githubusercontent.com/gr4vy/token-import-templates/main/token_import_template_non_card.csv) [bank accounts](https://raw.githubusercontent.com/gr4vy/token-import-templates/main/token_import_template_bank.csv) [gift cards](https://raw.githubusercontent.com/gr4vy/token-import-templates/main/token_import_template_gift_card.csv) can be downloaded and saved as a `.csv` file. Not all fields are required for minimum import, but sending as many fields as are available is recommended. These sample files show how a file can be populated: [card sample](https://raw.githubusercontent.com/gr4vy/token-import-templates/main/token_import_sample.csv) [bank sample](https://raw.githubusercontent.com/gr4vy/token-import-templates/main/token_import_sample_bank.csv). > [!NOTE] > > The import process does not modify your token file, if column data is not > truncated the record is treated as a failure. ### Column definitions | Column | Restrictions | Minimum Length | Maximum Length | Definition | | - | - | - | - | - | | `audit_reference_id` | Required, unique | | 200 | An ID for this row that can be used for logging and audit purposes. | | `merchant_account_id` | | | 22 | The merchant account ID that resources should be created for. This defaults to `default`. | | `buyer.external_identifier` | Required, unique | | 200 | An external identifier that can be used to match the buyer against your own records. This value needs to be unique for all buyers. See [Buyer creation](./buyer-payment-creation) for more details. Must be unique. | | `buyer.display_name` | | | 200 | A unique name for this buyer which is used in the dashboard to give a buyer a human-readable name. | | `buyer.billing_details.email_address` | | | 320 | The email address for the buyer. | | `buyer.billing_details.first_name` | | | 255 | The first names or given names for the buyer. | | `buyer.billing_details.last_name` | | | 255 | The last name, or family name, of the buyer. | | `buyer.billing_details.phone_number` | | | 320 | The phone number for the buyer should be formatted according to the [E164 number standard](https://www.twilio.com/docs/glossary/what-e164). | | `buyer.billing_details.address.city` | | | 100 | The city for the address. | | `buyer.billing_details.address.country` | | | 2 | The country for the address in ISO-3166 format. | | `buyer.billing_details.address.line1` | | | 255 | The first line of the address. | | `buyer.billing_details.address.line2` | | | 255 | The second line of the address. | | `buyer.billing_details.address.postal_code` | | | 50 | The postal code or zip code for the address. | | `buyer.billing_details.address.state` | | | 255 | The state, county, or province for the address. | | `buyer.billing_details.address.state_code` | | | | The code of state, county, or province for the address in ISO 3166-2 format, for example `US-UT`. This value isn't derived from `state`, and some connectors, such as Adyen and Repay, send only `state_code` to the payment service. Include it for US and Canadian addresses. | | `buyer.billing_details.address.house_number_or_name` | | | 255 | The house number or name for the address. Not all payment services use this field but some do. | | `buyer.billing_details.address.organization` | | | 255 | The optional name of the company or organization to add to the address. | | `buyer.billing_details.tax_id.kind` | | | | The kind of tax ID. See [API Docs](https://docs.gr4vy.com/reference/buyers/new-buyer) for this field's accepted values. | | `buyer.billing_details.tax_id.value ` | | | 50 | The tax ID for the buyer. | | `payment_method.number` | Required (card, digital wallet) | 13 | 19 | The card number (PAN), without spaces or any other formatting. For digital wallets, this is the device primary account number (DPAN) for `applepay` and `googlepay`, or the card number for `googlepay_pan_only`. | | `payment_method.expiration_date` | Required (card, digital wallet) | | | The credit card expiration date, formatted as a string in the `MM/YY` format. | | `payment_method.cardholder_name` | Optional (digital wallet) | | | The cardholder name for the wallet. Defaults to the buyer's billing first and last name when omitted. | | `payment_method.card_scheme` | Optional (digital wallet) | | | The card scheme for the wallet. Overrides the value derived from the number. Must be a valid card scheme. | | `payment_method.card_type` | Optional (digital wallet) | | | The underlying card type for the wallet, one of `credit`, `debit`, or `prepaid`. Overrides the value derived from the number. | | `payment_method.card_suffix` | Optional (digital wallet) | | 4 | The last four digits of the underlying card. Overrides the value derived from the number. | | `payment_method.scheme_transaction_id` | Recommended | | 100 | The latest scheme or network transaction ID for the card or wallet. This is required to enable subsequent MIT transactions without requiring the buyer to be present. | | `payment_method.transaction_link_id` | Optional (card, digital wallet) | | 100 | The [Mastercard Transaction Link Identifier (TLID)](/guides/features/recurring-payments/mastercard-tlid) for the card or wallet. Gr4vy sends it on later merchant-initiated transactions with the payment method. Leave empty if you don't have one. | | `payment_method.external_identifier` | Required | | 200 | An external identifier that can be used to match the card against your own records. | | `payment_method.token` | Required (non-card) | | | The non-card payment method's token. | | `payment_method.token_xid` | Optional (non-card) | | | The token ID on the provider. Defaults to `payment_method.token` if not provided. | | `payment_method.payment_service_id` | Required (non-card or gift card) | | | The `payment_service_id` for the token or `gift_card_service_id` for the gift card. | | `payment_method.method` | Required (non-card, digital wallet) | | | The payment method's method. For digital wallets, use `applepay`, `googlepay`, or `googlepay_pan_only`. | | `payment_method.scheme` | Required (bank) | | | The bank account's scheme, one of `ach`, `bacs`, or `sepa`. | | `payment_method.account_number` | Required (bank) | | | The bank account number, or the IBAN when the scheme is `sepa`. An `ach` account number is at most 17 digits, a `bacs` one is 8 digits. Leading zeros are significant and must be preserved. | | `payment_method.routing_number` | Required (`ach`, `bacs`) | | | The ABA routing number for `ach`, 9 digits, or the sort code for `bacs`, 6 digits. Optional for `sepa`, where it is the BIC. Leading zeros are significant and must be preserved. | | `payment_method.account_type` | Optional (`ach`) | | | The type of bank account, either `checking` or `savings`. Defaults to `checking` when empty. Only used by `ach`. | | `payment_method.account_holder.first_name` | Optional (bank) | | | The account holder's first name, for an account held by an individual. | | `payment_method.account_holder.last_name` | Optional (bank) | | | The account holder's last name, for an account held by an individual. | | `payment_method.account_holder.company_name` | Optional (bank) | | | The account holder's company name, for an account held by a business. | | `payment_method.label` | Ignored for card, bank, and gift cards | | | The label to display for non-card payment methods. | | `payment_method.last_used_at` | Ignored for gift cards | | | The timestamp in ISO-8601 format when this payment method was last used in a transaction. | | `payment_method.cit_last_used_at` | Ignored for gift cards | | | The timestamp in ISO-8601 format when this payment method was last used in a transaction for client initiated transactions. | | `gift_card.number` | Required (gift card) | 16 | 19 | The gift card number, without spaces or any other formatting. | | `gift_card.pin` | Required (gift card) | 1 | | The gift card pin. | | `gift_card.currency` | Required (gift card) | | | The gift card currency in ISO-4217 format. | | `gift_card.expiration_date` | Required (gift card) | | | The gift card expiration date in ISO-8601 format. Leave empty if gift card doesn't expire. | > [!NOTE] > > The import service treats a file as a digital wallet import when it includes `payment_method.number`, `payment_method.expiration_date`, and `payment_method.method`. > The digital wallet template includes all card columns, so don't include `payment_method.method` in a card import file. > [!NOTE] > > The import service treats a file as a bank import when it includes `payment_method.scheme` and `payment_method.account_number`. > These columns can't be combined with the columns of another template type in the same file. > > A bank account holder is either an individual or a company, never both. Supply either > `payment_method.account_holder.first_name` and `payment_method.account_holder.last_name`, or > `payment_method.account_holder.company_name`. When all three are empty the buyer's billing name is used, and > failing that the buyer's billing address organization. A partially filled holder is used as given, so the > fallback only applies when all three columns are empty. > > Imported bank accounts are stored with the `bank` method. You can only charge them through a connection that accepts > stored bank account details, such as [Adyen bank account](/connections/payments/adyen-bank). Redirect connections, such > as Adyen SEPA or Adyen ACH Direct Debit, can't charge them. See [Supported connectors](/connections/payments/bank#supported-connectors). > Set up this connection before the first charge, otherwise the transaction fails with the error > `There is no active payment service for this transaction.` > > An import file is expected to carry raw account numbers. Account numbers already encrypted or tokenized by another > provider can't be imported. > > Take care when producing the file with a spreadsheet application, as these commonly treat account and routing > numbers as numeric and silently drop their leading zeros. A routing number that arrives with too few digits is > rejected (see the `payment_method.routing_number` column in the [Column definitions](#column-definitions) > table above). Format both columns as text before entering any values. # Advanced authentication for Vault Forward Source: https://integration.wpay.com.au/guides/features/vault-forwarding/authentication When using Vault Forward, the authentication to the downstream API is done by passing in the authorization header, you need to pass your own API keys and credentials through the body or headers. For example, when using a bearer token, it needs to be passed in the Vault Forward request as a `x-vault-forward-header-authorization` header. ```http theme={"system"} POST /vault-forward host: api.acme.gr4vy.app content-type: application/json x-vault-forward-url: https://example.com/endpoint x-vault-forward-http-method: PUT x-vault-forward-header-authorization: Bearer 123 ``` In some situations, passing through the authentication is not technically possible. * The downstream service requires the request to be signed * The downstream service requires two-way SSL / mutual TLS authentication * The downstream service requires the entire payload to be encrypted For these services, support for advanced authentication has been added. ## Setup To set up advanced authentication, head over to the dashboard, switch to **Settings** -> **Manage merchants** then edit a merchant using the actions menu, and then **Edit merchant**. In a single merchant environment you may be able to go to this screen directly via the **Settings** -> **Configuration** menu. Next, switch to the **Vault Forwarding** tab and you should see the endpoints enabled for your environment. ![Vault Forward](https://mintcdn.com/wpay/RxgiaZJ07iWfuxu1/assets/images/vault-forward/config.png?fit=max\&auto=format\&n=RxgiaZJ07iWfuxu1\&q=85\&s=36eda47165147b4dc94425e2317c05f6) When an endpoints supports authentication, for example Visa, the actions menu should have an option to edit the **Authentication methods**. Select this menu, and it takes you to a new screen to set up an authentication. To set up an authentication method, click the **Add authentication**, and select the method to add for that endpoint. Fill in any of the required fields, and finally click the **Add** button. ![Vault Forward authentication](https://mintcdn.com/wpay/RxgiaZJ07iWfuxu1/assets/images/vault-forward/add-authentication.png?fit=max\&auto=format\&n=RxgiaZJ07iWfuxu1\&q=85\&s=aa4444ebb134ae17c62807bf235f7388) Once set up, please note the UUID of each authentication method as you need it in the next step. > [!WARNING] > > **Validation**: credentials provided are not currently validated or verified. Please ensure the right values are passed, and no extra > new lines, spaces, or other characters are filled in. ## Usage To make a Vault Forward request with an authentication method use the `x-vault-forward-authentications` header with a list of one or more authentication methods UUIDs (comma separated) to use on that request. Each authentication method has its unique UUID that is displayed in the dashboard. ```http theme={"system"} POST /vault-forward host: api.acme.gr4vy.app content-type: application/json x-vault-forward-url: https://example.com/endpoint x-vault-forward-http-method: PUT x-vault-forward-payment-methods: b77fef6d-c360-4b42-8f70-d884f4a6852a x-valud-forward-authentications: 0a71cd3f-021e-4528-bcd7-38772594634b,2168f3c2-e327-47bd-9d5a-e9cfaeb53e96 { "cards": [ { "number": "{{ CARD_NUMBER_1 }}", "expiry": "{{ CARD_EXPIRATION_DATE_1 }}", "cvv": "123" } ] } ``` Each authentication method is applied to a request before it is sent on to the target endpoint. # Vault Forward card data from a checkout session Source: https://integration.wpay.com.au/guides/features/vault-forwarding/checkout-sessions Use a Vault Forward with a checkout session and Secure Fields when you want to collect a CVV for a stored card, or forward card data without first creating a payment method. ## Forward CVV for a stored card To forward a security code (CVV) for a stored card, create a checkout session and use it with Secure Fields to collect the security code. See the [Secure Fields guide for stored cards](/guides/payments/secure-fields/stored-cards) for the collection flow. Once the security code has been collected, forward the card details using the checkout session as shown below. ## Forward a checkout session To forward a checkout session, set the `x-vault-forward-checkout-session` header instead of `x-vault-forward-payment-methods`. The `CARD_SECURITY_CODE_1` placeholder is then available in the request body. ```http Original request theme={"system"} POST /vault-forward host: api.acme.gr4vy.app content-type: application/json x-vault-forward-url: https://example.com/endpoint x-vault-forward-http-method: PUT x-vault-forward-header-authorization: Bearer 123 x-vault-forward-checkout-session: b77fef6d-c360-4b42-8f70-d884f4a6852a x-vault-forward-timeout: 10 { "cards": [ { "number": "{{ CARD_NUMBER_1 }}", "expiry": "{{ CARD_EXPIRATION_DATE_1 }}", "cvv": "{{ CARD_SECURITY_CODE_1 }}" } ] } ``` ```http Forwarded request theme={"system"} PUT /endpoint host: example.com content-type: application/json authorization: Bearer 123 { "cards": [ { "number": "4111111111111111", "expiry": "12/25", "cvv": "123" } ] } ``` > [!WARNING] > > Only one checkout session can be provided per Vault Forward request, so all placeholders use the `_1` index. For the full list of supported headers and card placeholders, see [Forward to endpoint](./forward). > **[3DS forwarding](/guides/features/vault-forwarding/3ds)** > > Forward 3DS authentication data alongside card details using checkout sessions # Enable a Vault Forward endpoint Source: https://integration.wpay.com.au/guides/features/vault-forwarding/enable To use Vault forward the host name of the API you want to make an API request to needs to be explicitly enabled in both sandbox and production environments. There is a list of hosts to which raw vaulted PCI data can be forwarded. To enable a host in an environment please put in a request with the support team. > [!NOTE] > > **Sandbox:** Vault forward rejects requests to a host that isn't enabled in the sandbox environment, the same as in production. > The API returns a validation error on the `x-vault-forward-url` header. ## Supported API endpoints The following PCI endpoints are currently supported in production. Additional API endpoints can be enabled on demand. Please contact the support team for next steps. * ACI Worldwide * Adyen * Amadeus * Authorize.net * Bank of Ireland Payment Acceptance * Braintree * Chase Orbital * Checkout.com * Cybersource * dLocal * EBANX * Fiserv * Nuvei * Mastercard * Razorpay * Sabre * Stripe * USAePay * Visa * Worldpay # Vault Forward card data to API endpoint Source: https://integration.wpay.com.au/guides/features/vault-forwarding/forward Once an API endpoint has been [enabled](/guides/features/vault-forwarding/enable), any vaulted card data can be forwarded to it using the Vault Forward API. The card must already be stored in the vault as a payment method — see [Tokenize a card](./tokenize) for how to store one. ## Request Vault Forward acts as a proxy to your third-party endpoint. You send the request to the Gr4vy API instead of directly to the downstream service, with a few adjustments: 1. Send the request to `POST /vault-forward` instead of the downstream URL. 2. Replace any PCI data in the body with template placeholders, for example `{{ CARD_NUMBER_1 }}`. 3. Add control headers to specify which cards to forward and where to send the request. 4. Prefix any downstream headers with `x-vault-forward-header-`. ```http Original request theme={"system"} POST /vault-forward host: api.acme.gr4vy.app content-type: application/json x-vault-forward-url: https://example.com/endpoint x-vault-forward-http-method: PUT x-vault-forward-header-authorization: Bearer 123 x-vault-forward-payment-methods: b77fef6d-c360-4b42-8f70-d884f4a6852a,1ca3fe71-7782-4f00-9b0e-c250b365bef1 x-vault-forward-timeout: 10 { "cards": [ { "number": "{{ CARD_NUMBER_1 }}", "expiry": "{{ CARD_EXPIRATION_DATE_1 }}", "cvv": "123" }, { "number": "{{ CARD_NUMBER_2 }}", "expiry": "{{ CARD_EXPIRATION_DATE_2 }}", "cvv": "234" } ] } ``` ```http Forwarded request theme={"system"} PUT /endpoint host: example.com content-type: application/json authorization: Bearer 123 { "cards": [ { "number": "4111111111111111", "expiry": "12/25", "cvv": "123" }, { "number": "4242424242424242", "expiry": "03/26", "cvv": "234" } ] } ``` ### Request headers | Header | Required | Description | | - | - | - | | `x-vault-forward-url` | Yes | HTTPS URL to forward the request to. The host must be [enabled](/guides/features/vault-forwarding/enable) in the environment. | | `x-vault-forward-http-method` | Yes | HTTP method to use: `POST`, `PUT`, or `PATCH`. | | `x-vault-forward-payment-methods` | Yes\* | Comma-separated list of payment method IDs. At least 1, no duplicates. Only card payment methods are accepted. | | `x-vault-forward-checkout-session` | Yes\* | Checkout session ID. If set, `x-vault-forward-payment-methods` can be omitted. Use to [forward a CVV](/guides/features/vault-forwarding/checkout-sessions). | | `x-vault-forward-header-{HEADER_NAME}` | No | A header to forward to the downstream endpoint. For example, set `x-vault-forward-header-authorization` to forward an `authorization` header. | | `x-vault-forward-timeout` | No | Timeout for the forwarded request in seconds. Defaults to 30. | \*Provide either `x-vault-forward-payment-methods` or `x-vault-forward-checkout-session` — at least one is required. ### Payload placeholders Replace PCI data in the request body with placeholders using `{{ PLACEHOLDER }}` syntax. The `X` suffix is the 1-based index of the payment method in the `x-vault-forward-payment-methods` list. | Placeholder | Description | | - | - | | `CARD_NUMBER_X` | The card number (PAN). | | `CARD_EXPIRATION_DATE_X` | Expiration date in `MM/YY` format. | | `CARD_EXPIRATION_DATE_M_X` | Expiration month, non-zero-padded (`1`–`12`). | | `CARD_EXPIRATION_DATE_MM_X` | Expiration month, zero-padded (`01`–`12`). | | `CARD_EXPIRATION_DATE_YY_X` | Expiration year without century (`00`–`99`). | | `CARD_EXPIRATION_DATE_YYYY_X` | Expiration year with century (for example `2025`). | | `CARD_SECURITY_CODE_X` | Security code (CVV). Only available via [Secure Fields and checkout sessions](/guides/features/vault-forwarding/checkout-sessions). | ## Authentication Pass your downstream API credentials through headers using the `x-vault-forward-header-` prefix: ```http theme={"system"} POST /vault-forward host: api.acme.gr4vy.app content-type: application/json x-vault-forward-url: https://example.com/endpoint x-vault-forward-http-method: PUT x-vault-forward-header-authorization: Bearer 123 ... ``` For services that require request signing, mutual TLS, or payload encryption — where passing credentials through headers is not possible — see [Advanced authentication](/guides/features/vault-forwarding/authentication). ## Response ### HTTP status code Any validation error before forwarding returns a `4XX` status. In all other cases — including when the downstream service returns an error — the status is `200`. ### Response headers | Header | Description | | - | - | | `content-type` | Content type of the downstream response, forwarded as-is. | | `x-vault-forward-http-status-code` | HTTP status code returned by the downstream endpoint. | | `x-vault-forward-header-{HEADER_NAME}` | Response headers from the downstream endpoint (except `content-type`). For example, `x-vault-forward-header-x-frame-options`. | | `x-vault-forward-error` | Error string if no response was received, for example on timeout or network failure. | ### Payload The raw downstream response body is returned as-is. Empty if no response was received. ### Examples ```http Successful forward theme={"system"} HTTP 200 content-type: application/json x-vault-forward-header-request-id: 123abc123abc { "status": "OK", "transactionId": "123456789" } ``` ```json Invalid forward request theme={"system"} HTTP 400 { "type": "error", "code": "bad_request", "status": 400, "message": "Request failed validation", "details": [ { "type": "invalid", "pointer": "x-vault-forward-http-method", "message": "x-vault-forward-http-method is not a valid value (PUT/POST/PATCH)", "location": "header" } ] } ``` ```http Successful forward, error response theme={"system"} HTTP 200 x-vault-forward-http-status-code: 401 content-type: application/json x-vault-forward-header-request-id: 123abc123abc { "status": "FAILURE", "error": "Invalid authentication" } ``` ## Explore further > **[CVV & Secure Fields](/guides/features/vault-forwarding/checkout-sessions)** > > Forward a security code collected via Secure Fields or a checkout session > **[3DS forwarding](/guides/features/vault-forwarding/3ds)** > > Forward 3DS authentication data alongside card details > **[Network token forwarding](/guides/features/vault-forwarding/network-tokens)** > > Forward network tokens and cryptograms to third-party endpoints > **[Advanced authentication](/guides/features/vault-forwarding/authentication)** > > Configure request signing, mutual TLS, or payload encryption > **[Enable an endpoint](/guides/features/vault-forwarding/enable)** > > Allow a specific host to receive forwarded PCI data in production # Forward network tokens via Vault Forward Source: https://integration.wpay.com.au/guides/features/vault-forwarding/network-tokens Vault Forward supports forwarding [network tokens](/guides/features/network-tokens/overview) and their associated cryptograms to third-party endpoints. If a network token has not yet been provisioned for a payment method, it is automatically provisioned before forwarding. Network token placeholders work with both `x-vault-forward-payment-methods` and `x-vault-forward-checkout-session`. When using a checkout session, it must reference a stored payment method (not inline card data). The CVV collected in that session is used to provision the network token. Cryptograms are generated for network token requests. To control whether a cryptogram is generated as a customer-initiated transaction (`false`) or a merchant-initiated transaction (`true`, the default), set the `x-vault-forward-merchant-initiated` header. ## Placeholders The following placeholders are available for forwarding network token data. * `CARD_NETWORK_TOKEN_X`: The network token for a card. Auto-provisioned if not yet available. * `CARD_NETWORK_TOKEN_EXPIRATION_DATE_X`: The network token's expiration date. * `CARD_NETWORK_TOKEN_EXPIRATION_DATE_M_X`: The expiration month as a non-zero-padded number. Example: `1, 2, ..., 12`. * `CARD_NETWORK_TOKEN_EXPIRATION_DATE_MM_X`: The expiration month as a zero-padded number. Example: `01, 02, ..., 12`. * `CARD_NETWORK_TOKEN_EXPIRATION_DATE_YY_X`: The expiration year without century. Example: `00, 01, ..., 99`. * `CARD_NETWORK_TOKEN_EXPIRATION_DATE_YYYY_X`: The expiration year with century. Example: `2025, 2026`. * `CARD_NETWORK_TOKEN_EXPIRATION_DATE_MMYY_X`: The expiration month and year combined. Example: `1225`. * `CARD_CRYPTOGRAM_X`: The cryptogram for the network token. A new cryptogram is generated for each request. > [!NOTE] > > The value `X` references the index of the payment method in the list of `x-vault-forward-payment-methods` (starting with 1). ## Request Use these placeholders in the request body the same way as other card data placeholders. ```http Original request theme={"system"} POST /vault-forward host: api.acme.gr4vy.app content-type: application/json x-vault-forward-url: https://example.com/endpoint x-vault-forward-http-method: POST x-vault-forward-header-authorization: Bearer 123 x-vault-forward-payment-methods: b77fef6d-c360-4b42-8f70-d884f4a6852a x-vault-forward-timeout: 10 { "card": { "number": "{{ CARD_NETWORK_TOKEN_1 }}", "expiry": "{{ CARD_NETWORK_TOKEN_EXPIRATION_DATE_MM_1 }}/{{ CARD_NETWORK_TOKEN_EXPIRATION_DATE_YY_1 }}", "cryptogram": "{{ CARD_CRYPTOGRAM_1 }}" } } ``` ```http Forwarded request theme={"system"} POST /endpoint host: example.com content-type: application/json authorization: Bearer 123 { "card": { "number": "4895370015293468", "expiry": "12/25", "cryptogram": "AgAAAAAAAAAAAAAQEAAAAAAAAAA=" } } ``` ## Fallback placeholders If you use a standard network token placeholder and provisioning fails, the request returns a `400` error. Each placeholder above has an `_OR_EMPTY_STRING` variant that returns an empty string (`""`) instead of failing. Use these placeholders when the downstream endpoint accepts—but does not require—a network token. * `CARD_NETWORK_TOKEN_OR_EMPTY_STRING_X` * `CARD_NETWORK_TOKEN_EXPIRATION_DATE_OR_EMPTY_STRING_X` * `CARD_NETWORK_TOKEN_EXPIRATION_DATE_M_OR_EMPTY_STRING_X` * `CARD_NETWORK_TOKEN_EXPIRATION_DATE_MM_OR_EMPTY_STRING_X` * `CARD_NETWORK_TOKEN_EXPIRATION_DATE_YY_OR_EMPTY_STRING_X` * `CARD_NETWORK_TOKEN_EXPIRATION_DATE_YYYY_OR_EMPTY_STRING_X` * `CARD_NETWORK_TOKEN_EXPIRATION_DATE_MMYY_OR_EMPTY_STRING_X` * `CARD_CRYPTOGRAM_OR_EMPTY_STRING_X` The `_OR_EMPTY_STRING` variants can also be combined with the `or` template operator to fall back to a specific value, such as the raw PAN: ```json theme={"system"} { "number": "{{ CARD_NETWORK_TOKEN_OR_EMPTY_STRING_1 or CARD_NUMBER_1 }}" } ``` See [or-logic](./3ds#or) for more details on the `or` operator. ```http With empty string fallback theme={"system"} POST /vault-forward host: api.acme.gr4vy.app content-type: application/json x-vault-forward-url: https://example.com/endpoint x-vault-forward-http-method: POST x-vault-forward-header-authorization: Bearer 123 x-vault-forward-payment-methods: b77fef6d-c360-4b42-8f70-d884f4a6852a { "card": { "number": "{{ CARD_NETWORK_TOKEN_OR_EMPTY_STRING_1 }}", "expiry": "{{ CARD_NETWORK_TOKEN_EXPIRATION_DATE_MM_OR_EMPTY_STRING_1 }}/{{ CARD_NETWORK_TOKEN_EXPIRATION_DATE_YY_OR_EMPTY_STRING_1 }}", "cryptogram": "{{ CARD_CRYPTOGRAM_OR_EMPTY_STRING_1 }}" } } ``` ## Errors If you use a standard network token placeholder that does not include the `_OR_EMPTY_STRING` fallback and the network token cannot be provisioned, the API returns a `400` error before the request is forwarded to the third-party endpoint. ```json Network token provisioning failure theme={"system"} HTTP 400 { "type": "error", "code": "network_token_provisioning_failed", "status": 400, "message": "Could not provision a network token for the provided payment method", "details": [] } ``` # Vaulting Source: https://integration.wpay.com.au/guides/features/vault-forwarding/overview Vaulting is Gr4vy's secure card storage and forwarding capability. It covers three related workflows: 1. **Tokenization** — capture and store card details in the Gr4vy vault. Using Secure Fields or the mobile SDK keeps raw PAN off your servers; direct API tokenization requires a PCI-compliant environment. 2. **PSP tokenization** — provision a provider-specific token for a vaulted card on a payment service. Use this when a downstream service requires its own token format rather than raw card data. 3. **Vault Forwarding** — proxy stored card data directly to a third-party PCI-compliant endpoint, such as a booking system, loyalty platform, or order management service. Together these capabilities let you handle card data securely across your entire stack — from first capture through to downstream partners — while helping to reduce your PCI scope. ## Get started 1. [Tokenize a card](./tokenize) to store it in the vault 2. [Provision a PSP token](./psp-tokens) if a downstream service requires a provider-specific token 3. [Forward vaulted data](./forward) to a third-party API endpoint ## Explore further Once the basics are in place, the feature guides cover the rest of what Vaulting can do. > **[PSP Tokenization](/guides/features/psp-tokenization/overview)** > > Provision, list, and delete provider-specific card tokens > **[Enable an endpoint](/guides/features/vault-forwarding/enable)** > > Allow a host to receive forwarded PCI data in production > **[CVV & Secure Fields](/guides/features/vault-forwarding/checkout-sessions)** > > Forward a security code collected via Secure Fields or a checkout session > **[3DS forwarding](/guides/features/vault-forwarding/3ds)** > > Forward 3DS authentication data alongside card details > **[Network token forwarding](/guides/features/vault-forwarding/network-tokens)** > > Forward network tokens and cryptograms to third-party endpoints > **[Advanced authentication](/guides/features/vault-forwarding/authentication)** > > Configure request signing, mutual TLS, or payload encryption # Advanced authentication for Visa Source: https://integration.wpay.com.au/guides/features/vault-forwarding/visa ## Prerequisites Please ensure the Visa endpoints have been enabled for your instance by the support team. ## Setup Once the Visa endpoint has been enabled, head over to the dashboard, switch to **Settings** -> **Manage merchants** then edit your merchant account using the actions menu, and then **Edit merchant**. In a single merchant environment you may be able to go to this screen directly via the **Settings** -> **Configuration** menu. Next, switch to the **Vault Forwarding** tab and you should see the Visa endpoint enabled for your environment. In the actions menu an option to edit the **Authentication methods** should be available. Select this menu, and it takes you to a new screen to set up an authentication. Visa has 2 authentication methods that can be set up: message level encryption (MLE) and mutual TLS (mTLS). ### Mutual TLS (mTLS) Mutual TLS ensures that all data between the system and Visa is encrypted at the TLS (SSL) level. This is generally a required authentication method for Visa endpoints and is the same for any merchant account. To set up mTLS, head over to the **Visa Developer Center** and switch to your merchant account, **Credentials**, and register a new certificate. Once set up, a private key and 2 certificates should be available. You can always redownload your certificate from the Visa dashboard by going to **Credentials** -> **Actions** -> **Download Certificate**. Similarly, you can download the DigiCert Global Root CA' certificate from the Visa dashboard as well. This root certificate may require conversion to the `pem` format. ```sh theme={"system"} openssl x509 -inform der -in Downloads/DigiCertGlobalRootCA.crt -outform pem -out DigiCertGlobalRootCA.pem ``` You can now upload this certificate (`cert`), private key (`cert_key`) and root certificate (`ca_cert`) via the dashboard by selecting the Mutual TLS authentication method. Alternatively, you can use the [`POST /vault-forward/configs/{instance_id}/authentications`](/reference/vault-forwarding/add-vault-forward-authentication) API as follows. ```json Request theme={"system"} { "kind": "mtls", "display_name": "mTLS", "fields": [ {"key": "cert", "value": "{cert}"}, {"key": "cert_key", "value": "{cert_key}"}, {"key": "ca_cert", "value": "{ca_cert}"} ] } ``` Note the created `id` of the authentication for later. > [!WARNING] > > Please ensure keys and certificates are uploaded in a multiline format, without any extra spaces, new lines, or other characters. ### Message Level Encryption (MLE) Message Level Encryption encrypts the payload sent to Visa, and this is generally recommended for most Visa endpoints that send sensitive PCI data. This can be configured on a per merchant basis. To set up MLE, create a new key via the **Visa Developer Center** -> **Merchant name** -> **Credentials** -> **Encryption/Decryption**. Note the key ID and key value. You can then send this key (`key`) and key ID (`key_id`) via the dashboard by selecting the Message Level Encryption authentication method. Alternatively, you can use the [`POST /vault-forward/configs/{instance_id}/authentications`](/reference/vault-forwarding/add-vault-forward-authentication) API as follows. ```json Request theme={"system"} { "kind": "mle", "display_name": "MLE", "fields": [ {"key": "key_id", "value": "{key_id}"}, {"key": "key", "value": "{key}"} ] } ``` Note the created `id` of the authentication for later. > [!WARNING] > > Please ensure keys are uploaded in a multiline format, without any extra spaces, new lines, or other characters. ## Usage To connect to the Visa API, create an HTTP request with the following headers. * `x-vault-forward-url`: `https://sandbox.api.visa.com/vop/v1/users/addcard` or the production equivalent * `x-vault-forward-http-method: POST` * `x-vault-forward-payment-methods`: the ID's for each of the payment methods to forward * `x-vault-forward-header-content-type: application/json` * `x-vault-forward-header-authorization:` * Go to **Visa Developer Center** -> **Merchant name** -> **Credentials** -> **expand row** * Generate [basic authentication header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#basic_authentication_scheme) with the given User ID and Password. * `x-vault-forward-authentications`: the comma separated IDs of the mTLS and MLE authentication methods set up earlier Then, make a `POST` request to [`/vault-forward`](/reference/vault-forwarding/make-vault-forward) as follows. ```json theme={"system"} { "card": { "expirationYear": "{{ CARD_EXPIRATION_DATE_YYYY_1 }}", "cvv": "123", "nameOnCard": "Test ABC", "billingZipCode": "94354", "expirationMonth": "{{ CARD_EXPIRATION_DATE_M_1 }}", "cardNumber": "{{ CARD_NUMBER_1 }}" }, "userKey": "9890124569", "communityCode": "GAP" } ``` Mutual TLS and merchant level encryption are automatically applied to the API request sent to Visa. The payload and headers received from Visa are returned like any other forward request. It's expected that the output is encrypted as merchants are expected to decrypt the response data themselves. # Webhook events Source: https://integration.wpay.com.au/guides/features/webhooks/events Webhooks are currently sent for the following events. | Resource | Payload `name` | Description | | :- | :- | :- | | API key pair | `api-key-pair.created` | An API key pair was created. | | API key pair | `api-key-pair.updated` | An API key pair was updated. | | API key pair | `api-key-pair.deleted` | An API key pair was deleted. | | Buyer | `buyer.created` | A buyer was created successfully. | | Buyer | `buyer.updated` | A buyer was updated successfully. This does not include its shipping details, see `shipping-details.deleted`. | | Buyer | `buyer.deleted` | A buyer was deleted. | | Capture | `capture.pending` | An individual capture against a transaction is pending. Sent per capture, including when a transaction has more than one capture. | | Capture | `capture.succeeded` | An individual capture against a transaction succeeded. | | Capture | `capture.declined` | An individual capture against a transaction was declined by the payment service. | | Capture | `capture.failed` | An individual capture against a transaction failed due to an internal or external reason. | | Gift card | `gift-card.created` | A stored gift card was created. | | Gift card | `gift-card.deleted` | A stored gift card was deleted. | | Monitoring incident | `monitoring.incident.created` | A monitoring incident has been created. | | Monitoring incident | `monitoring.incident.closed` | A monitoring incident has been closed. | | Payment method | `payment-method.paused` | The payment service paused the agreement or mandate behind a stored payment method. The payment method can't be used until it's resumed. | | Payment method | `payment-method.resumed` | A paused stored payment method was resumed and can be used again. | | Payment method | `payment-method.updated` | A stored payment method was updated, for example when an [account updater](/guides/features/account-updater/overview) replaced the card number or expiry date. The payment method keeps the same ID. | | Payment method | `payment-method.authorization-expiring` | The underlying agreement or mandate for a stored payment method is close to expiring. | | Payment method | `payment-method.authorization-update-required` | The underlying agreement or mandate for a stored payment method has expired and needs reauthorization. | | Payment method | `payment-method.authorization-updated` | The underlying agreement or mandate for a stored payment method is no longer expired or expiring because it was reauthorized. | | Payment method | `payment-method.deleted` | The payment service reported that a stored payment method was removed on its side, and Gr4vy deleted the payment method. This applies to payment methods that rely on a payment service token, such as redirect payment methods. This event isn't sent for stored cards, or when you delete a payment method through the API. | | Refund | `refund.declined` | A refund for a transaction was declined by the payment service. | | Refund | `refund.failed` | A refund for a transaction failed because of an error, or the payment service reported that a refund it first accepted failed. See [refunds](/guides/features/webhooks/technical-considerations#refunds). | | Refund | `refund.pending` | A refund for a transaction is pending. | | Refund | `refund.succeeded` | A refund for a transaction succeeded. | | Report execution | `report-execution.failed` | A report failed to generate. You [might need to re-create](/guides/dashboard/reports/how-to#webhooks-error-handling) the report. | | Report execution | `report-execution.succeeded` | A report was generated. | | Shipping details | `shipping-details.created` | A buyer successfully had its shipping details added. | | Shipping details | `shipping-details.updated` | A buyer's shipping details were updated successfully. | | Shipping details | `shipping-details.deleted` | A buyer's shipping details were deleted. | | Transaction | `transaction.authenticated` | A transaction was successfully authenticated. | | Transaction | `transaction.authentication-pending` | A transaction is pending authentication. | | Transaction | `transaction.authorized` | A transaction is authorized (through 3DS or otherwise) but has not yet been authenticated. | | Transaction | `transaction.canceled` | A pending transaction was canceled, for example with [Cancel transaction](/reference/transactions/cancel-transaction). The payload reports the `status` as `authorization_failed`. | | Transaction | `transaction.cancel-failed` | The payment service rejected a request to cancel a pending transaction. The transaction status doesn't change. | | Transaction | `transaction.capture-failed` | An authorized transaction failed to capture. | | Transaction | `transaction.capture-pending` | An authorized transaction is pending capture. | | Transaction | `transaction.captured` | An authorized transaction was captured. | | Transaction | `transaction.declined` | A transaction was declined. | | Transaction | `transaction.failed` | A transaction failed due to an internal or external reason. Please check the `error_code` for the cause. | | Transaction | `transaction.modified` | A transaction was updated, for example by Gr4vy support on your behalf, or when an [incremental authorization](/guides/features/incremental-authorization/overview) succeeds. | | Transaction | `transaction.review-accepted` | A transaction that was held in anti-fraud review has been accepted but its status has not changed, only the `pending_review` property has been set to `false`. This event isn't sent for every review. See [webhooks during a review](/guides/features/anti-fraud/reviews#webhooks-during-a-review). | | Transaction | `transaction.settled` | A transaction has been reported settled. This is currently only supported by The Giving Block. | | Transaction | `transaction.void-declined` | An authorized transaction had its request to void the authorization declined by the payment service. | | Transaction | `transaction.void-failed` | An authorized transaction had its request to void the authorization otherwise rejected by the payment service. | | Transaction | `transaction.void-pending` | An authorized transaction has a pending void request. | | Transaction | `transaction.voided` | An authorized transaction has been voided. | | Payout | `payout.succeeded` | A payout has been created successfully. | | Payout | `payout.pending` | A payout is pending a notification from the payment service. | | Payout | `payout.declined` | A payout was declined. | | Payout | `payout.failed` | A payout failed due to an internal or external reason. | | Payment link | `payment-link.created` | A payment link has been created successfully. | | Payment link | `payment-link.updated` | A payment link has been updated or expired. | No webhook is sent when a stored payment method moves to the `failed` status. See [payment method statuses](/guides/api/statuses/payment-methods#how-a-stored-payment-method-fails). No webhook is sent for chargebacks or disputes. When Gr4vy receives a chargeback from the payment service's reports, it sets the transaction's `disputed` field to `true`. To check for disputes, [get the transaction](/reference/transactions/get-transaction). > [!WARNING] > > Additional webhooks covering all the remaining events in the payment lifecycle > are planned for a very near release. # Webhooks Source: https://integration.wpay.com.au/guides/features/webhooks/overview Webhooks allow you to receive notifications of events that occurred on the platform. These notifications are delivered to an HTTPS endpoint of your choice. ## Setup Multiple webhook URLs per instance are supported. Webhooks can be configured by administrators by heading to **Settings** -> **Manage Integrations** -> **Webhook subscriptions**. For each subscription, it's possible to listen to the events for all merchant accounts in an instance, or to the events for one merchant account. ## Authentication Each subscription can authenticate its webhook requests to your endpoint in one of the following ways. * **Basic authentication**: Gr4vy sends the username and password in the `Authorization` header. * **OAuth**: Gr4vy requests an access token from your token URL with the password grant or the client credentials grant. It sends the `grant_type`, `client_id`, `client_secret`, and, for the password grant, the `username` and `password`, plus the `scope` if you set one, as form parameters in the body of the token request, with the `Content-Type` set to `application/x-www-form-urlencoded`. It doesn't send them in an `Authorization` header. Gr4vy then sends the access token as a bearer token in the `Authorization` header of each webhook request. Gr4vy reuses an access token for up to one hour. If your endpoint responds with a `4XX` status, Gr4vy discards the token and requests a new one for the next attempt. Webhook subscriptions don't support custom request headers. To verify that a request comes from Gr4vy, use [signatures](./signatures). ## Webhook delivery behavior 1. **Multiple subscriptions**\ If you configure multiple subscriptions pointing to different URLs, each subscription receives its own webhook deliveries. 2. **Retries are subscription-specific**\ If a webhook delivery fails, it is only retried for the subscription that encountered the failure, even if other subscriptions (pointing to the same or different URLs) succeed. ## Supported features * Multiple webhook subscriptions per merchant account * Merchant account and instance-level webhook subscriptions * Detailed [payloads](./payload) for transactions, payment methods, buyers, billing details, and most other transaction related events. * Over 20 different webhook [events](./events). * Basic authentication credentials (username/password) * OAuth authentication (password and credentials grant) * Automatic retries of undelivered events * Webhook [signatures](./signatures) to verify authenticity. ## Next steps * Learn about the structure of webhook messages in [Payloads](./payload) * Review all supported webhook [Events](./events) * Understand delivery guarantees and retry logic in [Technical considerations](./technical-considerations) * Secure your endpoints by verifying [Signatures](./signatures) # Webhook payload Source: https://integration.wpay.com.au/guides/features/webhooks/payload The payload for a webhook is a single event referencing a resource that has recently changed. The `target` object is a summary of that resource, captured when the event occurred. It contains a subset of the fields that the [API](/reference) returns for the resource, and some fields that only appear in webhooks, such as `payment_link_id` on transactions. Gr4vy stores the payload when the event occurs, so a retried delivery sends the same snapshot, even if the resource has changed since. Fetch the resource from the API before you take an irreversible action, such as shipping an order, or when you need a field that the summary doesn't include, such as a transaction's cart items or 3-D Secure details. ```json API key pair theme={"system"} { "type": "event", "id": "347901e1-8b53-42a4-951b-ec546a5078f1", "name": "api-key-pair.created", "created_at": "2012-12-12T10:53:43+00:00", "merchant_account_id": "default", "gr4vy_id": "example", "environment": "sandbox", "target": { "type": "api-key-pair", "id": "fe26475d-ec3e-4884-9553-f7356683f7f9", "thumbprint": "SopiN5XU0IWy8Xe9mwx1ICQbKh4-ydCDPx3LIyArkVI", "display_name": "Sandbox", "algorithm": "ECDSA", "roles": [ { "id": "e94fe431-b9e8-4f38-8509-f0e25be7d5a3", "name": "Administrator", "slug": "administrator" } ], "merchant_accounts": [ { "id": "default", "display_name": "Default" } ], "active": true, "created_at": "2024-11-20T16:50:52.603191+00:00", "updated_at": "2024-11-20T16:50:52.603191+00:00" } } ``` ```json Buyer theme={"system"} { "type": "event", "name": "buyer.created", "id": "d99a867a-a326-447f-b61e-2825ebe6f0c5", "merchant_account_id": "default", "gr4vy_id": "example", "environment": "sandbox", "created_at": "2024-11-20T16:50:52.609674+00:00", "target": { "type": "buyer", "id": "f9d46f71-60d8-4b61-bddb-001a04c8224c", "display_name": "John Smith", "external_identifier": "user-123", "created_at": "2024-11-20T16:50:52.603191+00:00", "updated_at": "2024-11-20T16:50:52.603191+00:00", "billing_details": { "first_name": "John", "last_name": "Smith", "email_address": "john@example.com", "phone_number": "+1234567890", "address": { "city": "Springfield", "country": "AU", "postal_code": "123456", "state": "CA", "state_code": "US-CA", "house_number_or_name": "21", "line1": "22nd Street", "line2": "", "organization": "Acme Inc" }, "tax_id": { "value": "12-3456789", "kind": "us.ein" } } } } ``` ```json Gift card theme={"system"} { "type": "event", "id": "347901e1-8b53-42a4-951b-ec546a5078f1", "name": "gift-card.created", "created_at": "2012-12-12T10:53:43+00:00", "merchant_account_id": "default", "gr4vy_id": "example", "environment": "sandbox", "target": { "type": "gift-card", "id": "00c6b82d-46d7-4d0c-b015-3f03a29b7a81", "created_at": "2024-09-17T16:49:55.064633+00:00", "updated_at": "2024-09-17T16:50:05.134346+00:00" } } ``` ```json Monitoring incident theme={"system"} { "type": "event", "name": "monitoring.incident.closed", "id": "c02012f3-ac25-495c-9f75-8fbe64d701f2", "created_at": "2026-03-03T14:54:56.485534+00:00", "target": { "type": "monitoring.incident", "id": "c2e4ffaf-2463-4e8a-a9a2-b33590f3b579", "spec": { "model": "auth_rate", "params": { "filters": { "datetime_range": "PT60M/hour_start", "payment_method_bin": ["123456", "411111"], "payment_method_scheme": ["visa"] } } }, "alerting_policy": { "strategy": "threshold", "params": { "alert_gt": 99.9, "alert_lt": 90 } }, "incident_policy": { "strategy": "debouncing", "params": { "create_after_n": 3, "close_after_n": 6 } }, "notes": "PSP-related incident", "created_at": "2025-10-28T11:47:32.566063+00:00", "updated_at": "2025-10-28T12:03:05.120019+00:00", "closed_at": "2025-10-28T11:57:32.566063+00:00" }, "merchant_account_id": "default", "gr4vy_id": "dev", "environment": "sandbox" } ``` ```json Payment link theme={"system"} { "type": "event", "id": "347901e1-8b53-42a4-951b-ec546a5078f1", "name": "payment-link.updated", "created_at": "2012-12-12T10:53:43+00:00", "merchant_account_id": "default", "gr4vy_id": "example", "environment": "sandbox", "target": { "type": "payment-link", "id": "8d29457b-683a-49c4-8afd-800cd7117236", "amount": 1299, "currency": "GBP", "country": "GB", "intent": "authorize", "payment_source": "ecommerce", "external_identifier": null, "expires_at": "2024-09-24T16:57:48.393644+00:00", "created_at": "2024-09-17T16:57:48.393644+00:00", "updated_at": "2024-09-17T16:57:50.553736+00:00" } } ``` ```json Payment method theme={"system"} { "type": "event", "id": "347901e1-8b53-42a4-951b-ec546a5078f1", "name": "payment-method.updated", "created_at": "2012-12-12T10:53:43+00:00", "merchant_account_id": "default", "gr4vy_id": "example", "environment": "sandbox", "target": { "type": "payment-method", "id": "8d29457b-683a-49c4-8afd-800cd7117236", "status": "succeeded", "method": "card", "external_identifier": null, "created_at": "2024-09-17T16:57:48.393644+00:00", "updated_at": "2024-09-17T16:57:50.553736+00:00" } } ``` ```json Payout theme={"system"} { "type": "event", "id": "347901e1-8b53-42a4-951b-ec546a5078f1", "name": "payout.succeeded", "created_at": "2012-12-12T10:53:43+00:00", "merchant_account_id": "default", "gr4vy_id": "example", "environment": "sandbox", "target": { "type": "payout", "id": "a994ec53-23b3-42e5-ab44-e4942d8aa7ba", "payout_id": "7ef82ff4-5862-4d2b-8bd0-4cbf86668fc5", "external_identifier": "payout-234", "payment_service_payout_id": "148206c4-fc32-44f3-906a-3d1b35865c19", "payment_service_id": "4504fa23-4318-44cd-95f5-872e6dabc9bc", "amount": 1299, "currency": "GBP", "status": "succeeded", "created_at": "2024-11-07T11:07:18.955537+00:00", "updated_at": "2024-11-07T11:07:19.111351+00:00" } } ``` ```json Refund theme={"system"} { "type": "event", "id": "347901e1-8b53-42a4-951b-ec546a5078f1", "name": "refund.succeeded", "created_at": "2012-12-12T10:53:43+00:00", "merchant_account_id": "default", "gr4vy_id": "example", "environment": "sandbox", "target": { "type": "refund", "id": "a994ec53-23b3-42e5-ab44-e4942d8aa7ba", "transaction_id": "7ef82ff4-5862-4d2b-8bd0-4cbf86668fc5", "payment_service_refund_id": "148206c4-fc32-44f3-906a-3d1b35865c19", "amount": 1299, "currency": "GBP", "status": "succeeded", "target_id": null, "created_at": "2024-11-07T11:07:18.955537+00:00", "updated_at": "2024-11-07T11:07:19.111351+00:00", "transaction_external_identifier": "tx-123", "external_identifier": "refund-234", "target_type": "payment-method", "reconciliation_id": "59zlCXgKuGfCz0wc8cTnOk", "transaction_reconciliation_id": "3raSDUgac8qWle5IbcZiMP", "error_code": null, "reason": null } } ``` ```json Report theme={"system"} { "type": "event", "id": "347901e1-8b53-42a4-951b-ec546a5078f1", "name": "report-execution.succeeded", "created_at": "2012-12-12T10:53:43+00:00", "merchant_account_id": "default", "gr4vy_id": "example", "environment": "sandbox", "target": { "type": "report-execution", "id": "8d29457b-683a-49c4-8afd-800cd7117236", "status": "succeeded", "report": { "type": "report", "id": "5b98970d-c6ee-48f1-a096-c5d37761f329" }, "created_at": "2024-09-17T16:49:55.064633+00:00", "updated_at": "2024-09-17T16:50:05.134346+00:00" } } ``` ```json Shipping details theme={"system"} { "type": "event", "name": "shipping-details.created", "id": "702863ee-fd78-467d-a0a7-533bf429b39c", "created_at": "2024-11-20T12:48:38.409619+00:00", "merchant_account_id": "default", "gr4vy_id": "example", "environment": "sandbox", "target": { "type": "shipping-details", "id": "3dda8d7a-e7cd-43b8-9128-21adf1ec868b", "buyer_id": "baa7b3b3-a4f1-49e3-afb0-0f41b48f5aa2", "first_name": "John", "last_name": "Smith", "email_address": "john@example.com", "phone_number": "+1234567890", "address": { "city": "Springfield", "country": "AU", "postal_code": "123456", "state": "CA", "state_code": "US-CA", "house_number_or_name": "21", "line1": "22nd Street", "line2": "", "organization": "Acme Inc" }, "created_at": "2024-11-20T12:48:38.402474+00:00", "updated_at": "2024-11-20T12:48:38.402474+00:00" } } ``` ```json Transaction theme={"system"} { "type": "event", "id": "347901e1-8b53-42a4-951b-ec546a5078f1", "name": "transaction.authorized", "created_at": "2012-12-12T10:53:43+00:00", "merchant_account_id": "default", "gr4vy_id": "example", "environment": "sandbox", "target": { "type": "transaction", "id": "95c27e9d-6cc3-40f6-a359-1355c434c30d", "external_identifier": "order-1234", "payment_service_transaction_id": "8110000000002017185", "status": "authorization_succeeded", "has_gift_card_redemptions": false, "pending_review": false, "amount": 1299, "authorized_amount": 1299, "captured_amount": 0, "refunded_amount": 0, "currency": "AUD", "metadata": { "profile": "profile-1234" }, "created_at": "2024-09-17T16:54:57.333316+00:00", "updated_at": "2024-09-17T16:55:06.560721+00:00", "payment_method": { "id": "852b951c-d7ea-4c98-b09e-4a1c9e97c077", "method": "card", "label": "1111", "scheme": "visa" }, "payment_link_id": null, "error_code": null, "iso_response_code": "00", "payment_source": "ecommerce", "merchant_initiated": false, "is_subsequent_payment": false, "buyer": { "id": "f9d46f71-60d8-4b61-bddb-001a04c8224c", "display_name": "John Smith", "external_identifier": "user-123", "account_number": null, "billing_details": { "first_name": "John", "last_name": "Smith", "email_address": "john@example.com", "phone_number": "+1234567890", "address": { "city": "Springfield", "country": "AU", "postal_code": "123456", "state": "CA", "state_code": "US-CA", "house_number_or_name": "21", "line1": "22nd Street", "line2": "", "organization": "Acme Inc" }, "tax_id": null } } } } ``` ## Transaction payloads In a transaction event, `payment_method` is `null` when the transaction has no payment method. `buyer` is `null` unless the transaction has at least one of a buyer ID, display name, external identifier, account number, or billing details. A guest checkout can have billing details without a buyer, in which case `buyer.id` is `null`. > [!WARNING] > > Transaction payloads can contain personal data, such as the buyer's name, email address, > phone number, and address, and any personal data you put in `metadata`. Mask or remove > these fields before you log a webhook payload. # Webhook signatures Source: https://integration.wpay.com.au/guides/features/webhooks/signatures Webhook signatures can be used to verify the authenticity and integrity of a received webhook notification. By verifying the webhook signature merchants can protect themselves from malicious parties sending tampered webhook data to the merchant's webhook endpoint. ## Configuration When configuring a [webhook subscription](./overview#setup), a secret can be generated that is used to sign the webhooks content. Please head over to the **Webhook subscriptions** dashboard, select the subscription, select the actions menu, and select **Generate secret**. Once created, the secret can be copied from the table and used to verify webhooks. > [!NOTE] > > Please note it may take 30 seconds for the signature to appear in webhook notifications. ## Signature verification When a secret is enabled for a webhook, the following HTTP headers are sent with every webhook message. * `X-Gr4vy-Webhook-Timestamp`: UNIX timestamp in seconds used to generate the signature. * `X-Gr4vy-Webhook-Signatures`: comma-separated list of signatures for each of the active secrets. * `X-Gr4vy-Webhook-ID`: unique reference to webhook across retries which acts as the idempotency value. Gr4vy sends this header with every webhook, even when no secret is enabled. To verify the webhook content the following steps must be followed. 1. Append the timestamp header and payload contents (`{timestamp}.{payload}`). 2. Compute the HMAC SHA256 value using the webhook subscription secret. 3. Make sure the computed value matches at least one of the signature header values. 4. (Optional) Check that timestamp is not too old to prevent replay attacks. ### SDK support The SDKs have been updated with support for verifying webhook signatures. The following code snippets show how to verify the webhook signature using these help methods. ```csharp C# theme={"system"} using Gr4vy; using Gr4vy.Models.Components; using System.Collections.Generic; // Loaded the key from a file, env variable, // or anywhere else var privateKey = "YOUR_PRIVATE_KEY"; var sdk = new Gr4vySDK( id: "example", server: SDKConfig.Server.Sandbox, bearerAuthSource: Auth.WithToken(privateKey), merchantAccountId: "default" ); ``` ```go Go theme={"system"} package main import ( "log" gr4vy "github.com/gr4vy/gr4vy-go" ) func main() { // Webhook payload and headers payload := "your-webhook-payload" secret := "your-webhook-secret" signatureHeader := "signatures-from-header" timestampHeader := "timestamp-from-header" timestampTolerance := 300 // optional, in seconds (default: 0) if err := gr4vy.VerifyWebhook(payload, secret, signatureHeader, timestampHeader, timestampTolerance); err != nil { log.Fatalf("invalid webhook: %v", err) } } ``` ```java Java theme={"system"} import com.gr4vy.sdk.Webhooks; String payload = 'your-webhook-payload'; String secret = 'your-webhook-secret'; String signatureHeader = 'signatures-from-header'; String timestampHeader = 'timestamp-from-header'; String timestampTolerance = 300; // optional, in seconds (default: 0) try { Webhooks.verifyWebhook(payload, secret, wrongSignatureHeader, timestampTolerance, timestampHeader); System.out.println("Webhook verified successfully."); } catch (IllegalArgumentException e) { System.err.println("Invalid input: " + e.getMessage()); } ``` ```php PHP theme={"system"} use Gr4vy; // Webhook payload and headers $payload = "your-webhook-payload"; $secret = "your-webhook-secret"; $signatureHeader = "signatures-from-header"; $timestampHeader = "timestamp-from-header"; $timestampTolerance = 300; // optional, in seconds (default: 0) try { Webhooks::verifyWebhook( secret: $secret$, payload: $payload, signatureHeader: $signatureHeader$, timestampHeader: $timestampHeader, timestampTolerance: $timestampTolerance ); } catch(Throwable $th$) { // handle the exception } ``` ```python Python theme={"system"} from gr4vy.webhooks import verify_webhook # Webhook payload and headers payload = 'your-webhook-payload' secret = 'your-webhook-secret' signature_header = 'signatures-from-header' timestamp_header = 'timestamp-from-header' timestamp_tolerance = 300 # optional, in seconds (default: 0) try: # Verify the webhook verify_webhook( payload=payload, secret=secret, signature_header=signature_header, timestamp_header=timestamp_header, timestamp_tolerance=timestamp_tolerance ) print('Webhook verified successfully!') except ValueError as error: print(f'Webhook verification failed: {error}') ``` ```ts TypeScript theme={"system"} import { verifyWebhook } from "@gr4vy/sdk"; const payload = 'your-webhook-payload' const secret = 'your-webhook-secret' const signatureHeader = 'signatures-from-header' const timestampHeader = 'timestamp-from-header' const timestampTolerance = 300 // optional, in seconds (default: 0) try { verifyWebhook( payload, secret, signatureHeader, timestampHeader, timestampTolerance ) console.log('Webhook verified successfully!') } catch (error) { console.error('Webhook verification failed:', error.message) } ``` > [!NOTE] > > Bear in mind that extracting the payload value must include all the formatting as it is received. For example, if the payload is a JSON object, it must be formatted as a string > with all the spaces and line breaks. If using a JSON library to parse the payload, make sure to use the original value received. ## Secret rotation In the webhook subscriptions page the secret can also be rotated. Secret rotation allows you to safely change the secret used to sign webhooks. It is possible to set a custom rotation period during which the system sends signatures for both the old secret and the new one. After this period, only the new secret is used to sign webhooks. To rotate a secret, please following these steps. 1. Generate a new secret using the **Rotate secret** menu in the webhook subscriptions page. 2. Update your webhook endpoint to use the new secret. 3. Verify the webhooks are processed using the new secret. 4. Wait for the old secret to expire after the rotation period. > [!NOTE] > > Please note it may take 30 seconds for the new signatures to appear in webhook notifications. ## Idempotency The `X-Gr4vy-Webhook-ID` header is a unique reference to the webhook across any retries. For example, you may successfully process a webhook and return response but due to network problems the system did not receive the response. The request is then retried and your code must know that the webhook was already processed. If the webhook handling is idempotent itself this might not be a real problem but still a waste of resources. You can store the latest webhooks processed extracting the id from `X-Gr4vy-Webhook-ID` to avoid processing them again. # Technical webhook considerations Source: https://integration.wpay.com.au/guides/features/webhooks/technical-considerations ## Endpoint URL The webhook subscription endpoint URL configured to receive the webhooks should be a HTTPS endpoint that supports TLS 1.2. Please ensure your Web Application Firewall (WAF) does not block the webhook requests. ## Webhook logic * Response time: The endpoint URL configured to receive the webhooks should return an HTTP response in the 200-299 range within 5000 milliseconds. * Retry: The webhook event is retried if the merchant endpoint does not respond with a `2XX` or times out. Retries are made with an exponential delay for a maximum of 7 days from the point of creation. If the request is not successful after this period then it is dropped and lost. * Delivery promise: Currently no guarantee is provided as to the delivery time between an event occurring in the system and it being delivered via a webhook to the merchant. * Caching: Webhook settings may be cached for a short period so changes may not be reflected immediately. ## Acknowledging webhooks When you receive a webhook event, you should always acknowledge its receipt, even if it contains additional information. To acknowledge a webhook, an HTTP response in the 200-299 range within 5000 milliseconds is expected. Any unacknowledged webhooks are retried and might cause delay in further webhook delivery. > [!WARNING] > > Please note that the content of a webhook event can change as more features are added. The addition of additional fields or enum values > is not considered a backwards incompatible change. Setting any JSON parser to ignore any unrecognized fields and values is recommended. ## Split authorization and capture In certain scenarios, when the `intent` for a transaction is set to `capture`, the system splits the operation into a separate authorization and capture. This means the payment is first authorized with the payment service, and then captured in a subsequent request. A capture is split into two operations when **all** of the following conditions are met: 1. The transaction `intent` is set to `capture`. 2. The payment service (connector) supports delayed captures. 3. The payment method is a card and is not using an alternative payment scheme. 4. **At least one** of the following is true: * The payment service does not support direct capture (sale). * The anti-fraud decision for the transaction was `review`. * Gift cards were used as part of the transaction (multi-tender). When a transaction is split, the authorization is performed first because it is the most likely step to fail. If the authorization succeeds, the capture follows automatically. If any additional step fails (for example, gift card redemption), the authorization is voided instead of requiring a refund. As a consequence, this sometimes results in multiple webhooks per transaction. In a normal situation, you would only receive a `transaction.captured` event, but when a capture is split you may receive a `transaction.authorized` webhook first, followed by a `transaction.captured` event. This same order of events happens when the `async_capture` property is set to `true` in the transaction request, as this always handles the capture asynchronously. If a capture request to the payment service times out, Gr4vy can retry the capture in the background, depending on the connection. See [capture timeouts](/guides/api/statuses/transactions#capture-timeouts). ## Missed webhooks Gr4vy retries a webhook only until your endpoint acknowledges it. Once your endpoint responds with a `2XX` status, the delivery is complete and Gr4vy can't send that webhook again. Gr4vy doesn't provide a delivery log or a way to replay webhooks. If you think you missed an event, fetch the current state of the resource from the API. For example, [get the transaction](/reference/transactions/get-transaction) or [list its refunds](/reference/transactions/list-transaction-refunds). Every webhook request includes an `X-Gr4vy-Webhook-ID` header. Its value matches the `id` in the payload and stays the same across retries and across your webhook subscriptions. Use it to search your own logs and to ignore duplicate deliveries. > [!NOTE] > > The **Webhook processed** entries in a transaction's events in the dashboard are > notifications that Gr4vy received from the payment service. They don't show deliveries > to your webhook endpoint. ## Refunds A refund doesn't change the transaction's `status`. Gr4vy sends `refund.*` events for a refund but no `transaction.*` event, so the `refunded_amount` in the last transaction webhook you received doesn't reflect the refund. To get the current `refunded_amount`, fetch the transaction from the API. A refund isn't always final when you first receive it. * Some payment services complete a refund asynchronously. Gr4vy sends `refund.pending`, and the refund keeps the `processing` status until the payment service confirms it. Gr4vy then sends `refund.succeeded`, `refund.declined`, or `refund.failed`. * Some payment services report a refund as succeeded and later report that it failed, for example when the issuer rejects it. The refund's status then changes to `failed`, and Gr4vy sends a `refund.failed` event after the earlier `refund.succeeded`. A `declined` refund was refused by the payment service. A `failed` refund couldn't be completed because of an error, or failed after the payment service first accepted it. Create refunds through the Gr4vy API or dashboard. A refund made directly in the payment service's dashboard isn't recorded in Gr4vy, so the transaction's `refunded_amount` doesn't change and Gr4vy sends no `refund.*` event for it. To help a buyer trace a refund, share the refund's `payment_service_refund_id`. This is the payment service's own ID for the refund. The refund `id` is only known to Gr4vy and to you. ## Captured transactions Once a transaction reaches `capture_succeeded`, its status doesn't change again. Later notifications from the payment service, such as a pending or error notification for the same payment, don't move the transaction back to an earlier status. Refunds don't change the status either. ## Transaction sync When you [sync a transaction](/reference/transactions/sync-transaction) and the sync changes its status, Gr4vy sends the matching transaction webhook, for example `transaction.captured`. A sync that doesn't change the status doesn't send a webhook. ## Modifying webhooks If you need to change a webhook subscription, for example the endpoint or authentication, it is strongly recommended that you allow for the processing of both the new and old values so that no messages are dropped. For example, if you were to update the endpoint; 1. Merchant updates their server to process webhooks on `https://new.webhooks.com` 2. Merchant updates the webhook subscription settings from `https://old.webhooks.com` to `https://new.webhooks.com` 3. Merchant processes webhooks on `https://old.webhooks.com` & `https://new.webhooks.com` 4. Merchant removes `https://old.webhooks.com` once all messages have been processed # Build with Wpay Source: https://integration.wpay.com.au/guides/get-started Connect, build, and scale payments using the Wpay payments platform. ![How scalable payments flow](https://mintcdn.com/wpay/c50rx3S48hHhNNxb/assets/images/general/hero.png?fit=max\&auto=format\&n=c50rx3S48hHhNNxb\&q=85\&s=5c0dc3bbe7dce37dbef3e06ecb37484f) Welcome to the documentation. Get started using payment orchestration today. If you have any questions please [contact us](https://wpay.io/contact/). ## Choose your integration path > **[Accept payments on the web](/guides/payments/web)** > > Add a drop-in payment UI to your website, or build a fully custom checkout with > Secure Fields. Supports cards, wallets, and 100+ payment methods. > **[Accept payments on mobile](/guides/payments/mobile)** > > Integrate native payment experiences on iOS and Android using the mobile > SDKs, or ship cross-platform with React Native. > **[Build custom flows via API](/guides/payments/direct-api/quick-start/overview)** > > Use the REST API or Native SDKs to build fully custom payment flows, vault cards, > and run server-side transactions. ## Popular features > **[3-D Secure](/guides/features/3ds/overview)** > > Choose from Embed, Hosted, Native, or External 3DS approaches. > **[Apple Pay](/guides/features/apple-pay/overview)** > > Accept Apple Pay on web and iOS. > **[Google Pay](/guides/features/google-pay/overview)** > > Accept Google Pay on web and Android. > **[Recurring payments](/guides/features/recurring-payments/overview)** > > Set up subscriptions and scheduled billing. > **[Flow rules](/guides/dashboard/flow/overview)** > > Route and control transactions dynamically with no-code rules. > **[Webhooks](/guides/features/webhooks/overview)** > > Receive real-time event notifications for transactions and more. ## Resources ### Server-side SDKs > **[C#](https://github.com/gr4vy/gr4vy-csharp)** > > Make server-side API calls with the C# SDK. > **[Go](https://github.com/gr4vy/gr4vy-go)** > > Make server-side API calls with the Go SDK. > **[Java](https://github.com/gr4vy/gr4vy-java)** > > Make server-side API calls with the Java SDK. > **[PHP](https://github.com/gr4vy/gr4vy-php)** > > Make server-side API calls with the PHP SDK. > **[Python](https://github.com/gr4vy/gr4vy-python)** > > Make server-side API calls with the Python SDK. > **[TypeScript](https://github.com/gr4vy/gr4vy-typescript)** > > Make server and client-side API calls with the TypeScript SDK. ### Client-side and mobile SDKs > **[Embed (React)](https://github.com/gr4vy/gr4vy-embed/tree/main/packages/embed-react)** > > Drop-in payment UI for React. > **[Embed (Node.js)](https://github.com/gr4vy/gr4vy-embed/tree/main/packages/embed)** > > Drop-in payment UI for web. > **[Secure Fields (React)](https://github.com/gr4vy/gr4vy-embed/tree/main/packages/secure-fields-react)** > > PCI-compliant card fields for React. > **[Secure Fields (Node.js)](https://github.com/gr4vy/gr4vy-embed/tree/main/packages/secure-fields)** > > PCI-compliant card fields for web. > **[React Native](https://github.com/gr4vy/gr4vy-react-native)** > > Cross-platform payments for iOS and Android. > **[Embed for iOS](https://github.com/gr4vy/gr4vy-ios)** > > Drop-in payment UI for iOS. > **[Embed for Android](https://github.com/gr4vy/gr4vy-android)** > > Drop-in payment UI for Android. > **[Swift SDK](https://github.com/gr4vy/gr4vy-swift)** > > Native low-level Swift SDK for iOS. > **[Kotlin SDK](https://github.com/gr4vy/gr4vy-kotlin)** > > Native low-level Kotlin SDK for Android. ### Developer tools > **[CLI](/guides/tools/cli)** > > Manage your account from the command line. > **[Postman](/guides/tools/postman)** > > Explore the API with the official Postman collection. > **[API Reference](/reference)** > > Browse the full API reference. # Identifiers & reconciliation Source: https://integration.wpay.com.au/guides/ids Integrating with a payment orchestrator involves managing multiple layers of identifiers. This guide explains the types of IDs returned by the platform API and provides recommendations on which IDs to store for reliable reconciliation and troubleshooting. For field definitions, see the [Get transaction](/reference/transactions/get-transaction) reference. ## Identifier categories Platform API responses include three categories of identifiers. Understanding the source and purpose of each is critical for a stable integration. * **Merchant identifiers**: You provide these IDs to the platform during an API request, such as the `external_identifier`. Use these to map platform resources back to your internal systems, like an order or customer record. * **Platform identifiers**: The platform generates these to uniquely identify resources within its ecosystem. These include resource IDs (such as the transaction `id`, refund `id`, and payment method `id`) and the `reconciliation_id`. These are typically formatted as UUIDs, but they can also appear as shorter, unique strings when a PSP reference field has strict limits. * **PSP identifiers (connector IDs)**: Payment Service Providers (PSPs) such as Stripe, Adyen, or PayPal return these IDs. Examples include authorization IDs, capture IDs, or session IDs. Use these to identify specific actions within a PSP’s own dashboard or API. ## Reconciliation Reconciliation is a two-step process: the platform maps the PSP record to a platform record, and then the platform maps that record back to your system. To ensure the mapping between the PSP and the platform is reliable, the platform often passes its own ID to the PSP. This is necessary because unique IDs generated by a PSP might not be available in real-time due to intermittent network timeouts or variations in upstream response formats. Because most PSPs have character limits for reference fields, standard UUIDs are often too long to send. To solve this, the platform uses the `reconciliation_id`. This is a shorter representation of the internal UUID and is compatible with most PSPs. Some connectors cannot send the `reconciliation_id` because the PSP field enforces additional length or character rules. In those cases, the platform sends an alternate reference derived from the transaction `id` that fits the PSP constraints. > [!NOTE] > > When you view a statement in a PSP dashboard, the **Reference** or > **Description** field usually contains the `reconciliation_id`. ## Recommended storage strategy To ensure you can map webhooks, settlement reports, and API responses back to your database, follow these storage recommendations: | Priority | ID type | Recommendation | | :- | :- | :- | | **Primary** | **Platform ID** | Store the ID of the resource (for example, the transaction `id`). This is the most stable reference for API look-up. | | **Secondary** | **Reconciliation ID** | Store the `reconciliation_id`. Use this key to link platform data to PSP settlement reports. | | **Optional** | **Merchant ID** | Store your `external_identifier` for internal mapping. Resolve it to the platform `id` for reconciliation and look-ups. | | **Optional** | **PSP IDs** | Store for reference or logging only. Don't use these as primary keys for your reconciliation logic. | The API returns both the `id` and the `reconciliation_id` on a transaction, so you don't need to convert one into the other. Store both when you receive the transaction. ## Nuances and implementation details ### Use `additional_identifiers` for PSP references Transaction responses include a field called `payment_service_transaction_id`. This field is intended for the platform's internal use to track the primary PSP entity. Because a single transaction might involve multiple PSP entities—such as a session, order, authorization, and capture—this ID might change as the transaction progresses or as PSP APIs evolve. Instead, use the `additional_identifiers` object. This object provides a structured, merchant-friendly breakdown of all IDs returned by the PSP, such as `payment_service_authorization_id` and `payment_service_capture_id`. ### Avoid relying on PSP-specific fields Do not use PSP-specific fields as primary identifiers. PSPs frequently update API versions, which can change the format or the field used for transaction IDs. Additionally, the platform might update how PSP fields are mapped to improve data quality. By relying on the platform's internal IDs, your integration remains insulated from these external changes. ### Card digits differ in the PSP dashboard When a transaction is processed with a network token, the PSP receives the network token instead of the card number, so its dashboard can show different card digits than the platform. The transaction's `instrument_type` shows which instrument was used. See [card details in your payment service's dashboard](/guides/features/network-tokens/overview#card-details-in-your-payment-services-dashboard). ## Summary * **For your UI and support**: Use the transaction `id`. * **For financial reconciliation**: Use the `reconciliation_id`. * **For PSP dashboard look-up**: Use the values found in `additional_identifiers`. * **Avoid**: Don't use `payment_service_transaction_id` as a unique key in your database; consider it an internal platform reference. # Agentic Development Kit (ADK) Source: https://integration.wpay.com.au/guides/payments/adk/overview Build AI-native storefronts within conversational AI platforms using Gr4vy. The Agentic Development Kit (ADK) gives merchants a practical way to make their products purchasable directly inside AI platforms—without rebuilding their existing payment infrastructure. Built on the [Model Context Protocol](https://modelcontextprotocol.io) (MCP), the ADK enables embedded shopping and checkout experiences within conversational interfaces, with Gr4vy handling payment orchestration through a single API. ## Supported platforms > **[ChatGPT](/guides/payments/chatgpt-app/overview)** > > Build an AI-native storefront inside ChatGPT. Let customers browse > products, manage a cart, and check out with Gr4vy Embed—all within a > conversation. > **Gemini** > > Coming soon. Build an AI-native storefront inside Google Gemini. ## How it works Each ADK integration follows the same pattern: 1. **MCP server:** Your server exposes tools (actions the AI can invoke) and resources (UI widgets it can embed) over the Model Context Protocol. 2. **Widgets:** Self-contained HTML widgets render inside the AI platform as inline frames, giving you full control over the shopping experience. 3. **Gr4vy Embed:** A PCI-compliant payment form loads inside the checkout widget, backed by Gr4vy orchestration, real-time routing, and webhook verification. 4. **Single API:** Once live, your storefront connects to Gr4vy through a single integration, giving you access to 400+ payment methods and PSPs using your existing payment stack. ## Reference implementations > **[ChatGPT - TypeScript](https://github.com/gr4vy/gr4vy-typescript-adk)** > > Reference implementation using Node.js, Express, React, and the MCP SDK. > **[ChatGPT - Python](https://github.com/gr4vy/gr4vy-python-adk)** > > Reference implementation using Python and FastMCP. # Android events Source: https://integration.wpay.com.au/guides/payments/android/events The following events can be raised by the Android SDK. `Gr4vyResult.TransactionCreated ` Returns data about the transaction object when the transaction was successfully created. ```json theme={"system"} { "transactionID": "8724fd24-5489-4a5d-90fd-0604df7d3b83", "status": "pending", "paymentMethodID": "17d57b9a-408d-49b8-9a97-9db382593003" } ``` `Gr4vyEvent.TransactionFailed` Returned when the transaction encounters an error. ```json theme={"system"} { "transactionID": "8724fd24-5489-4a5d-90fd-0604df7d3b83", "status": "pending", "paymentMethodID": "17d57b9a-408d-49b8-9a97-9db382593003" } ``` When a network error occurs, the payload includes a `responseCode`. ```json theme={"system"} { "transactionID": "", "status": "", "paymentMethodID": null, "responseCode": "ETIMEDOUT" } ``` Here's a list of potential values for `responseCode`: | Code | Definition | | - | - | | `ERR_BAD_OPTION_VALUE` | Invalid or unsupported value provided in axios configuration. | | `ERR_BAD_OPTION` | Invalid option provided in axios configuration. | | `ECONNABORTED` | Request timed out due to exceeding timeout specified in axios configuration. | | `ETIMEDOUT` | Request timed out due to exceeding default axios time limit. | | `ERR_NETWORK` | Network-related issue. | | `ERR_FR_TOO_MANY_REDIRECTS` | Request is redirected too many times; exceeds max redirects specified in axios configuration. | | `ERR_DEPRECATED` | Deprecated feature or method used in axios. | | `ERR_BAD_RESPONSE` | Response cannot be parsed properly or is in an unexpected format. | | `ERR_BAD_REQUEST` | Requested has unexpected format or missing required parameters. | | `ERR_CANCELED` | Feature or method is canceled explicitly by the user. | | `ERR_NOT_SUPPORT` | Feature or method not supported in the current axios environment. | | `ERR_INVALID_URL` | Invalid URL provided for axios request. | `Gr4vyResult.Cancelled` Returned when the user cancels/quits the SDK. `Gr4vyResult.GeneralError` Returned when the SDK encounters an error. ```json theme={"system"} { "Gr4vy Error: Failed to load" } ``` `Gr4vyEvent.CardDetailsChanged` Returned when the card BIN changes in the form. It contains information on the inputted card, such as the BIN, card type and scheme. ```json theme={"system"} { "bin": "42424242", "scheme": "visa", "cardType": "debit" } ``` # Google Pay on Android Source: https://integration.wpay.com.au/guides/payments/android/google-pay On Android, Google Pay can be easily integrated both with and without the SDK. In both offerings, the need to sign up for a Google Developer account has been removed, reducing additional financial and maintenance costs. | | [Using SDK][mobile] | [Without SDK][mobile-without] | | - | - | - | | Google Developer account required? | No | No | | Customization & Theming? | Limited | Full | | Integrated with Checkout Flow? | Yes | Optional | > [!NOTE] > > \[Google Pay]\[google-pay] is also available on web. [mobile]: /guides/features/google-pay/mobile [mobile-without]: /guides/features/google-pay/mobile-without-sdk [google-pay]: /guides/features/google-pay # Android options Source: https://integration.wpay.com.au/guides/payments/android/options The SDK can be configured in a few ways. | Field | Optional / Required | Description | | - | - | - | | `gr4vyId` | **Required** | Provided in the manifest. The ID automatically sets the `apiHost` to `api..gr4vy.app` for production and `api.sandbox..gr4vy.app` and to `embed.sandbox..gr4vy.app` for the sandbox environment. | | `environment` | `Optional` | `"sandbox"`, `"production"`. Defaults to `"production"`. When `"sandbox"` is provided the URL contains `sandbox.GR4VY_ID`. | | `token` | **Required** | The server-side generated JWT token used to authenticate any of the API calls. | | `amount` | **Required** | The amount to authorize or capture in the specified `currency` only. | | `currency` | **Required** | A valid, active, 3-character `ISO 4217` currency code to authorize or capture the `amount` for. | | `country` | **Required** | A valid `ISO 3166` country code. | | `buyerId` | `Optional` | The buyer ID for this transaction. The transaction is automatically associated with a buyer with that ID. | | `buyerExternalIdentifier` | `Optional` | An optional external ID for a buyer. The transaction is automatically associated with a buyer with that external ID. If no buyer with this external ID exists then it is ignored. This option is ignored if the `buyerId` is provided. | | `externalIdentifier` | `Optional` | An optional external identifier that can be supplied. This is automatically associated with any resource created and can subsequently be used to find a resource by that ID. | | `store` | `Optional` | `'ask'`, `'preselect'`, `true`, `false` - Explicitly store the payment method, ask the buyer or preselect it by default. Requires `buyerId` or `buyerExternalIdentifier`. | | `display` | `Optional` | `all`, `addOnly`, `storedOnly`, `supportsTokenization` - Filters the payment methods to show stored methods only, new payment methods only or methods that support tokenization. | | `intent` | `Optional` | `authorize`, `preferAuthorize`, `capture` - Defines the intent of this API call. This determines the desired initial state of the transaction. When used, `preferAuthorize` automatically switches to `capture` if the selected payment method doesn't support delayed capture. | | `metadata` | `Optional` | An optional dictionary of keys and values for transaction metadata. All values should be a string. | | `paymentSource` | `Optional` | `installment`, `recurring` - Can be used to signal that Embed is used to capture the first transaction for a subscription or an installment. When used, `store` is implied to be `true` and `display` is implied to be `supportsTokenization`. This means that payment options that do not support tokenization are automatically hidden. | | `cartItems` | `Optional` | An optional array of cart item objects, each object must define a `name`, `quantity`, and `unitAmount`. | | `theme` | `Optional` | Theme customization options. See [Theming](https://github.com/gr4vy/gr4vy-embed/tree/main/packages/embed#theming). The iOS SDK also contains an additional two properties within the `colors` object; `headerBackground` and `headerText`. These are used for the navigation background and foreground colors. | | `locale` | `Optional` | An optional locale, consists of an `ISO 639 Language Code` followed by an optional `ISO 3166 Country Code`, for example `en`, `en-gb` or `pt-br`. | | `statementDescriptor` | `Optional` | An optional object with information about the purchase to construct the statement information the buyer sees in their bank statement. Please note support for these fields varies across payment service providers and underlying banks, so only a best-effort approach for each supported platform can be ensured. As an example, most platforms only support a concatenation of the `name` and `description` fields, this is truncated to a length of 22 characters within embed. The object can contain `name`, `description`, `phoneNumber`, `city` and `url` properties, with string values. `phoneNumber` should be in E164 format. Avoiding characters outside the alphanumeric range and the dot (`.`) is recommended to ensure wide compatibility. | | `requireSecurityCode` | `Optional` | An optional boolean that forces security code to be prompted for stored card payments. | | `shippingDetailsId` | `Optional` | An optional unique identifier of a set of shipping details stored for the buyer. | | `installmentCount` | `Optional` | An optional value that indicates the number of installments a buyer is required to make. | | `excludedMethods` | `Optional` | An optional list of payment methods to exclude. | | `approvalExpiresAt` | `Optional` | An optional `datetime` string (ISO 8601 format recommended) that sets the expiration date for transaction approval. If set to `null`, it is treated the same as if it were omitted. | # Installing the Android SDK Source: https://integration.wpay.com.au/guides/payments/android/quick-start/android Next, quickly embed the SDK in your Android app to store card details, authorize payments, and capture a transaction. ## Add the SDK The [Android SDK](https://github.com/gr4vy/gr4vy-android) is available as a Maven bundle on [Jitpack](https://jitpack.io/#gr4vy/gr4vy-android). In Gradle, this can be set up as follows. ```js theme={"system"} repositories { mavenCentral() maven { url "https://jitpack.io" } } dependencies { implementation 'com.github.gr4vy:gr4vy-android:v1.4.0' } ``` ## Set up the app Next, add the following meta information to your app's manifest. ```xml theme={"system"} ``` > [!WARNING] > > Replace `[ID]` with the ID of your instance and > `[ENVIRONMENT]` with either `sandbox` or `production`. Additionally, ensure you have internet access added to the permissions in your manifest. ```xml theme={"system"} ``` ## Prepare the SDK Next, add the following to the top of your activity where you want to enable the payment sheet. ```java theme={"system"} private lateinit var gr4vySDK: Gr4vySDK ``` Then, initialize the SDK within the `onCreate()` method of your activity. ```java theme={"system"} gr4vySDK = Gr4vySDK(activityResultRegistry, this, this) ``` Next, register the observer as follows. ```java theme={"system"} lifecycle.addObserver(gr4vySDK) ``` Finally, implement the `Gr4vyResultHandler` interface on the activity and implement the required methods. ```java theme={"system"} class MainActivity : ComponentActivity(), Gr4vyResultHandler ``` # Creating an API key for use with Android Source: https://integration.wpay.com.au/guides/payments/android/quick-start/key Before the app can communicate with the API, create a new API key. The API handles authentication using a signed **JSON Web Token** (JWT) which is passed in an HTTP header. ```bash theme={"system"} curl -X GET https://api.example.gr4vy.app/transactions \ -H "authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIi..." ``` > [!TIP] > > Using an SDK is recommended. The SDKs make the process of generating a token a lot easier and are probably > available in the preferred programming language. This is covered in the > next step. ## Create a new API key To use the API, generate a new **API key**. Head over to the dashboard and visit the **Integrations** page. ![API key dashboard](https://mintcdn.com/wpay/c50rx3S48hHhNNxb/assets/images/api-keys/add-key.png?fit=max\&auto=format\&n=c50rx3S48hHhNNxb\&q=85\&s=ffbfb085954c55e845050e44a7c9e73e) On this page, click the **Add API key** button select a name for the key. The name is purely to track what key is for what integration. The downloaded key needs to be stored securely as it is not stored by the system. > [!NOTE] > > Learn more about private keys, key-pairs, and JWT in the > [in-depth guide on authentication](/guides/api/authentication). ## Summary In this step: * Learned about API authentication. * Created a new private key for the API. * Stored this API key in a secure place where the app can access it. # Android SDK Source: https://integration.wpay.com.au/guides/payments/android/quick-start/overview Get set up with the Android SDK for your checkout experience. > [!NOTE] > > **New Kotlin SDK Available** > > It is recommended to use the new [Kotlin SDK](/guides/payments/native/overview) for native Android development. > It provides native 3DS support, improved performance, and a modern API. This Android SDK (Embed-based) is still supported and maintained for web view implementations. > > With the Kotlin SDK, your app collects the card number in its own fields, which puts your app in scope for the Payment Card Industry Data Security Standard (PCI DSS). See [Self-certification and scope](/guides/payments/native/overview#self-certification-and-scope) before you choose it. To keep card data out of your app, use this Embed-based SDK. ![Embed](https://mintcdn.com/wpay/c50rx3S48hHhNNxb/assets/images/android/hero.png?fit=max\&auto=format\&n=c50rx3S48hHhNNxb\&q=85\&s=f0a0689cec81d86e235a07f0591861be) In this quick start guide, the following steps are covered. 1. [Create an API key](/guides/payments/android/quick-start/key) which is needed to generate the signed JWT token in step 2. 2. [Set up a server-side SDK](/guides/payments/android/quick-start/sdks) to generate a client token for authenticating to the API. 3. [Install the Android SDK](/guides/payments/android/quick-start/android) which handles the capture of the payment method details and process the payment. 4. [Process the transaction](/guides/payments/android/quick-start/transact) and inspect the result. # Generating tokens for authentication Source: https://integration.wpay.com.au/guides/payments/android/quick-start/sdks Before adding the SDK to the Android app, create a client token that can be used to authenticate the transaction. ## Install a server-side SDK Use the package manager in the preferred programming language to install the server-side SDK. Token generation can only be done server side and doing this client side is not recommended as it exposes the API key to customers. ```sh C# theme={"system"} dotnet add package Gr4vy ``` ```sh Go theme={"system"} go get github.com/gr4vy/gr4vy-go ``` ```sh Java theme={"system"} # Please check for the latest version implementation 'com.gr4vy:sdk:1.0.0' ``` ```sh PHP theme={"system"} composer require "gr4vy/gr4vy-php" ``` ```sh Python theme={"system"} pip install gr4vy ``` ```sh TypeScript theme={"system"} npm install @gr4vy/sdk # or: yarn add @gr4vy/sdk ``` > [!NOTE] > > Please always check and install the [latest release](/guides/get-started#server-side-sdks) of the preferred SDK. ## Initialize the SDK client Next, initialize the SDK with the ID of the instance and the private key. ```csharp C# theme={"system"} using Gr4vy; using Gr4vy.Models.Components; using System.Collections.Generic; // Loaded the key from a file, env variable, // or anywhere else var privateKey = "..."; var sdk = new Gr4vySDK( id: "example", server: SDKConfig.Server.Sandbox, bearerAuthSource: Auth.WithToken(privateKey), merchantAccountId: "default" ); ``` ```go Go theme={"system"} package main import ( "context" gr4vy "github.com/gr4vy/gr4vy-go" "github.com/gr4vy/gr4vy-go/models/operations" "log" "os" ) func main() { ctx := context.Background() privateKey := "...." // Private key loaded from disk or env var withToken := gr4vy.WithToken(privateKey, []JWTScope{ReadAll, WriteAll}, 60) s := gr4vy.New( gr4vy.WithID("example"), gr4vy.WithServer(gr4vy.ServerSandbox), gr4vy.WithSecuritySource(withToken), gr4vy.WithMerchantAccountID("default"), ) } ``` ```java Java theme={"system"} package hello.world; import com.gr4vy.sdk.BearerSecuritySource; import com.gr4vy.sdk.Gr4vy; import com.gr4vy.sdk.Gr4vy.AvailableServers; import com.gr4vy.sdk.models.components.AccountUpdaterJobCreate; import com.gr4vy.sdk.models.errors.*; import com.gr4vy.sdk.models.operations.ListTransactionsRequest; import java.lang.Exception; import java.util.List; public class Application { public static void main(String[] args) throws Exception { String privateKey = "-----BEGIN PRIVATE KEY-----\n...."; // a valid private key Gr4vy sdk = Gr4vy.builder() .id("example") .server(AvailableServers.SANDBOX) .merchantAccountId("default") .securitySource(new BearerSecuritySource.Builder(privateKey).build()) .build(); } } ``` ```php PHP theme={"system"} declare(strict_types=1); require 'vendor/autoload.php'; use Gr4vy; use Gr4vy\Auth; // Loaded the key from a file, env variable, // or anywhere else $privateKey = "..."; $sdk = Gr4vy\SDK::builder() ->setId('example') ->setServer('sandbox') ->setSecuritySource(Auth::withToken($privateKey)) ->setMerchantAccountId('default') ->build(); ``` ```python Python theme={"system"} from gr4vy import Gr4vy, auth import os client = Gr4vy( id="example", server="production", merchant_account_id="default", bearer_auth=auth.with_token(open("./private_key.pem").read()) ) ``` ```ts TypeScript theme={"system"} import fs from "fs"; import { Gr4vy, withToken } from "@gr4vy/sdk"; const gr4vy = new Gr4vy({ server: "sandbox", id: "example", bearerAuth: withToken({ privateKey: fs.readFileSync("private_key.pem", "utf8"), }), }); ``` > [!NOTE] > > The instance ID is the unique identifier for the deployment of the system and is included in every API call. > Together with the environment (sandbox or production) it is used to connect to the right APIs, as well as dashboard. > > The possible values for your ID are one of `wpay`, `wpay1`, `wpay2`, or `wpay3`. You are informed > in which instance your account has been set up. ## Generate a token The next step is to create a new token for frontend authentication. The SDK comes with a helper function for exactly that purpose. It requires an amount and currency for the transaction to be created and additionally accepts either the ID of a buyer or an external identifier for a buyer to link the transaction to. For Embed, it is recommended to attach a checkout session to every transaction. The `getEmbedTokenWithCheckoutSession` helper creates a checkout session using your SDK client and returns an Embed token with the resulting `checkout_session_id` already pinned, in a single call. ```csharp C# theme={"system"} using System.Collections.Generic; using Gr4vy; // Loaded the key from a file, env variable, // or anywhere else var privateKey = "..."; var sdk = new Gr4vySDK( id: "example", server: SDKConfig.Server.Sandbox, bearerAuthSource: Auth.WithToken(privateKey), merchantAccountId: "default" ); var token = await Auth.GetEmbedTokenWithCheckoutSession( client: sdk, privateKey: privateKey, embedParams: new Dictionary { ["amount"] = 1299, ["currency"] = "AUD", ["buyer_external_identifier"] = "user-1234", } ); ``` ```go Go theme={"system"} package main import ( "context" gr4vy "github.com/gr4vy/gr4vy-go" "log" ) func main() { privateKey := "...." // Private key loaded from disk or env var client := gr4vy.New( gr4vy.WithID("example"), gr4vy.WithServer(gr4vy.ServerSandbox), gr4vy.WithSecuritySource(gr4vy.WithToken(privateKey, nil, 3600)), gr4vy.WithMerchantAccountID("default"), ) embedParams := map[string]interface{}{ "amount": 1299, "currency": "AUD", "buyer_external_identifier": "user-1234", } token, err := gr4vy.GetEmbedTokenWithCheckoutSession(context.Background(), client, privateKey, embedParams, nil, nil) if err != nil { log.Fatal(err) } log.Println(token) } ``` ```java Java theme={"system"} package hello.world; import com.gr4vy.sdk.Auth; import com.gr4vy.sdk.BearerSecuritySource; import com.gr4vy.sdk.Gr4vy; import com.gr4vy.sdk.Gr4vy.AvailableServers; import java.util.HashMap; import java.util.Map; public class Application { public static void main(String[] args) throws Exception { String privateKey = "-----BEGIN PRIVATE KEY-----\n...."; // a valid private key Gr4vy client = Gr4vy.builder() .id("example") .server(AvailableServers.SANDBOX) .securitySource(new BearerSecuritySource.Builder(privateKey).build()) .merchantAccountId("default") .build(); Map embedParams = new HashMap<>(); embedParams.put("amount", 1299); embedParams.put("currency", "AUD"); embedParams.put("buyer_external_identifier", "user-1234"); String token = Auth.getEmbedTokenWithCheckoutSession(client, privateKey, embedParams); System.out.println(token); } } ``` ```php PHP theme={"system"} declare(strict_types=1); require 'vendor/autoload.php'; use Gr4vy; use Gr4vy\Auth; // Loaded the key from a file, env variable, // or anywhere else $privateKey = "..."; $sdk = Gr4vy\SDK::builder() ->setId('example') ->setServer('sandbox') ->setSecuritySource(Auth::withToken($privateKey)) ->setMerchantAccountId('default') ->build(); $token = Auth::getEmbedTokenWithCheckoutSession( client: $sdk, privateKey: $privateKey, embedParams: [ "amount" => 1299, "currency" => "AUD", "buyer_external_identifier" => "user-1234", ], ); ``` ```python Python theme={"system"} from gr4vy import Gr4vy, auth private_key = open("./private_key.pem").read() g_client = Gr4vy( id="example", server="production", merchant_account_id="default", bearer_auth=auth.with_token(private_key) ) token = auth.get_embed_token_with_checkout_session( g_client, private_key, embed_params={ "amount": 1299, "currency": "AUD", "buyer_external_identifier": "user-1234", }, ) ``` ```ts TypeScript theme={"system"} import { Gr4vy, getEmbedTokenWithCheckoutSession, withToken } from "@gr4vy/sdk"; import fs from "fs"; async function run() { const privateKey = fs.readFileSync("private_key.pem", "utf8") const gr4vy = new Gr4vy({ server: "sandbox", id: "example", bearerAuth: withToken({ privateKey }), }); const token = await getEmbedTokenWithCheckoutSession({ client: gr4vy, privateKey, embedParams: { amount: 1299, currency: 'AUD', buyerExternalIdentifier: 'user-1234', } }); console.log(token); } run(); ``` This token can now be served to the frontend where it is used by the SDK. If generating the client token without using a server-side SDK is preferred, then please read the guide on [manually generating a JWT](/guides/api/authentication#without-sdks). ## Checkout sessions Passing a [checkout session](/reference/checkout-sessions/new-checkout-session) ID to the Embed token function is recommended. This ID can be used to help identify multiple transaction attempts. This is useful to identify if one or more failed/declined transactions eventually result in a successful transaction. Checkout sessions can also store `cart_items`, `metadata`, airline data, and more, removing the need to pass these values on the transaction request. ```csharp C# theme={"system"} using Gr4vy; using Gr4vy.Models.Components; using NodaTime; using System; using System.Collections.Generic; var res = await sdk.CheckoutSessions.CreateAsync(checkoutSessionCreate: new CheckoutSessionCreate() { CartItems = new List() { new CartItem() { Name = "GoPro HD", Quantity = 2, UnitAmount = 1299, DiscountAmount = 0, TaxAmount = 0, ExternalIdentifier = "goprohd", Sku = "GPHD1078", ProductUrl = "https://example.com/catalog/go-pro-hd", ImageUrl = "https://example.com/images/go-pro-hd.jpg", Categories = new List() { "camera", "travel", "gear", }, ProductType = "physical", SellerCountry = "GB", }, new CartItem() { Name = "GoPro HD", Quantity = 2, UnitAmount = 1299, DiscountAmount = 0, TaxAmount = 0, ExternalIdentifier = "goprohd", Sku = "GPHD1078", ProductUrl = "https://example.com/catalog/go-pro-hd", ImageUrl = "https://example.com/images/go-pro-hd.jpg", Categories = new List() { "camera", "travel", "gear", }, ProductType = "physical", SellerCountry = "GB", }, new CartItem() { Name = "GoPro HD", Quantity = 2, UnitAmount = 1299, DiscountAmount = 0, TaxAmount = 0, ExternalIdentifier = "goprohd", Sku = "GPHD1078", ProductUrl = "https://example.com/catalog/go-pro-hd", ImageUrl = "https://example.com/images/go-pro-hd.jpg", Categories = new List() { "camera", "travel", "gear", }, ProductType = "physical", SellerCountry = "AU", }, }, Metadata = new Dictionary() { { "cohort", "cohort-a" }, { "order_id", "order-12345" }, }, Buyer = new GuestBuyer() { DisplayName = "John Doe", ExternalIdentifier = "buyer-12345", BillingDetails = new BillingDetails() { FirstName = "John", LastName = "Doe", EmailAddress = "john@example.com", PhoneNumber = "+1234567890", Address = new Address() { City = "San Jose", Country = "AU", PostalCode = "94560", State = "California", StateCode = "US-CA", HouseNumberOrName = "10", Line1 = "Stafford Apartments", Line2 = "29th Street", Organization = "Gr4vy", }, TaxId = new TaxId() { Value = "12345678931", Kind = "ar.cuit", }, }, ShippingDetails = new ShippingDetailsCreate() { FirstName = "John", LastName = "Doe", EmailAddress = "john@example.com", PhoneNumber = "+1234567890", Address = new Address() { City = "San Jose", Country = "AU", PostalCode = "94560", State = "California", StateCode = "US-CA", HouseNumberOrName = "10", Line1 = "Stafford Apartments", Line2 = "29th Street", Organization = "Gr4vy", }, }, }, }); // handle response ``` ```go Go theme={"system"} package main import( "context" gr4vygo "github.com/gr4vy/gr4vy-go" "os" "github.com/gr4vy/gr4vy-go/models/components" "github.com/gr4vy/gr4vy-go/types" "log" ) func main() { ctx := context.Background() // Init SDK res, err := s.CheckoutSessions.Create(ctx, &components.CheckoutSessionCreate{ CartItems: []components.CartItem{ components.CartItem{ Name: "GoPro HD", Quantity: 2, UnitAmount: 1299, DiscountAmount: gr4vygo.Int64(0), TaxAmount: gr4vygo.Int64(0), ExternalIdentifier: gr4vygo.String("goprohd"), Sku: gr4vygo.String("GPHD1078"), ProductURL: gr4vygo.String("https://example.com/catalog/go-pro-hd"), ImageURL: gr4vygo.String("https://example.com/images/go-pro-hd.jpg"), Categories: []string{ "camera", "travel", "gear", }, ProductType: components.ProductTypePhysical.ToPointer(), SellerCountry: gr4vygo.String("AU"), }, components.CartItem{ Name: "GoPro HD", Quantity: 2, UnitAmount: 1299, DiscountAmount: gr4vygo.Int64(0), TaxAmount: gr4vygo.Int64(0), ExternalIdentifier: gr4vygo.String("goprohd"), Sku: gr4vygo.String("GPHD1078"), ProductURL: gr4vygo.String("https://example.com/catalog/go-pro-hd"), ImageURL: gr4vygo.String("https://example.com/images/go-pro-hd.jpg"), Categories: []string{ "camera", "travel", "gear", }, ProductType: components.ProductTypePhysical.ToPointer(), SellerCountry: gr4vygo.String("AU"), }, }, Metadata: map[string]string{ "cohort": "cohort-a", "order_id": "order-12345", }, Buyer: nil, }) if err != nil { log.Fatal(err) } if res != nil { // handle response } } ``` ```java Java theme={"system"} package hello.world; import com.gr4vy.sdk.Gr4vy; import com.gr4vy.sdk.models.components.*; import com.gr4vy.sdk.models.errors.*; import com.gr4vy.sdk.models.operations.CreateCheckoutSessionResponse; import java.lang.Exception; import java.time.LocalDate; import java.time.OffsetDateTime; import java.util.List; import java.util.Map; import org.openapitools.jackson.nullable.JsonNullable; public class Application { public static void main(String[] args) throws Exception { // Init SDK CreateCheckoutSessionResponse res = sdk.checkoutSessions().create() .checkoutSessionCreate(CheckoutSessionCreate.builder() .cartItems(List.of( CartItem.builder() .name("GoPro HD") .quantity(2L) .unitAmount(1299L) .discountAmount(0L) .taxAmount(0L) .externalIdentifier("goprohd") .sku("GPHD1078") .productUrl("https://example.com/catalog/go-pro-hd") .imageUrl("https://example.com/images/go-pro-hd.jpg") .categories(List.of( "camera", "travel", "gear")) .productType(ProductType.PHYSICAL) .sellerCountry("AU") .build(), CartItem.builder() .name("GoPro HD") .quantity(2L) .unitAmount(1299L) .discountAmount(0L) .taxAmount(0L) .externalIdentifier("goprohd") .sku("GPHD1078") .productUrl("https://example.com/catalog/go-pro-hd") .imageUrl("https://example.com/images/go-pro-hd.jpg") .categories(List.of( "camera", "travel", "gear")) .productType(ProductType.PHYSICAL) .sellerCountry("AU") .build())) .metadata(Map.ofEntries( Map.entry("cohort", "cohort-a"), Map.entry("order_id", "order-12345"))) .buyer(JsonNullable.of(null)) .build()) .call(); if (res.checkoutSession().isPresent()) { // handle response } } } ``` ```php PHP theme={"system"} declare(strict_types=1); require 'vendor/autoload.php'; use Brick\DateTime\LocalDate; use Gr4vy; use Gr4vy\Utils; // Init SDK $checkoutSessionCreate = new Gr4vy\CheckoutSessionCreate( cartItems: [ new Gr4vy\CartItem( name: 'GoPro HD', quantity: 2, unitAmount: 1299, discountAmount: 0, taxAmount: 0, externalIdentifier: 'goprohd', sku: 'GPHD1078', productUrl: 'https://example.com/catalog/go-pro-hd', imageUrl: 'https://example.com/images/go-pro-hd.jpg', categories: [ 'camera', 'travel', 'gear', ], productType: 'physical', sellerCountry: 'AU', ), new Gr4vy\CartItem( name: 'GoPro HD', quantity: 2, unitAmount: 1299, discountAmount: 0, taxAmount: 0, externalIdentifier: 'goprohd', sku: 'GPHD1078', productUrl: 'https://example.com/catalog/go-pro-hd', imageUrl: 'https://example.com/images/go-pro-hd.jpg', categories: [ 'camera', 'travel', 'gear', ], productType: 'physical', sellerCountry: 'AU', ), ], metadata: [ 'cohort' => 'cohort-a', 'order_id' => 'order-12345', ], buyer: new Gr4vy\GuestBuyer( displayName: 'John Doe', externalIdentifier: 'buyer-12345', billingDetails: new Gr4vy\BillingDetails( firstName: 'John', lastName: 'Doe', emailAddress: 'john@example.com', phoneNumber: '+1234567890', address: new Gr4vy\Address( city: 'San Jose', country: 'AU', postalCode: '94560', state: 'California', stateCode: 'US-CA', houseNumberOrName: '10', line1: 'Stafford Apartments', line2: '29th Street', organization: 'Gr4vy', ), taxId: new Gr4vy\TaxId( value: '12345678931', kind: 'my.frp', ), ), shippingDetails: new Gr4vy\ShippingDetailsCreate( firstName: 'John', lastName: 'Doe', emailAddress: 'john@example.com', phoneNumber: '+1234567890', address: null, ), ), ); $response = $sdk->checkoutSessions->create( checkoutSessionCreate: $checkoutSessionCreate ); if ($response->checkoutSession !== null) { // handle response } ``` ```python Python theme={"system"} from datetime import date from gr4vy import Gr4vy, models from gr4vy.utils import parse_datetime import os # Init SDK res = g_client.checkout_sessions.create(checkout_session_create=models.CheckoutSessionCreate( cart_items=[ models.CartItem( name="GoPro HD", quantity=2, unit_amount=1299, discount_amount=0, tax_amount=0, external_identifier="goprohd", sku="GPHD1078", product_url="https://example.com/catalog/go-pro-hd", image_url="https://example.com/images/go-pro-hd.jpg", categories=[ "camera", "travel", "gear", ], product_type="physical", seller_country="GB", ), models.CartItem( name="GoPro HD", quantity=2, unit_amount=1299, discount_amount=0, tax_amount=0, external_identifier="goprohd", sku="GPHD1078", product_url="https://example.com/catalog/go-pro-hd", image_url="https://example.com/images/go-pro-hd.jpg", categories=[ "camera", "travel", "gear", ], product_type="physical", seller_country="GB", ), models.CartItem( name="GoPro HD", quantity=2, unit_amount=1299, discount_amount=0, tax_amount=0, external_identifier="goprohd", sku="GPHD1078", product_url="https://example.com/catalog/go-pro-hd", image_url="https://example.com/images/go-pro-hd.jpg", categories=[ "camera", "travel", "gear", ], product_type="physical", seller_country="GB", ), ], metadata={ "cohort": "cohort-a", "order_id": "order-12345", }, buyer=models.GuestBuyer( display_name="John Doe", external_identifier="buyer-12345", billing_details=models.BillingDetails( first_name="John", last_name="Doe", email_address="john@example.com", phone_number="+1234567890", address=models.Address( city="San Jose", country="AU", postal_code="94560", state="California", state_code="US-CA", house_number_or_name="10", line1="Stafford Apartments", line2="29th Street", organization="Gr4vy", ), tax_id=models.TaxID( value="12345678931", kind="ar.cuit", ), ), shipping_details=models.ShippingDetailsCreate( first_name="John", last_name="Doe", email_address="john@example.com", phone_number="+1234567890", address=models.Address( city="San Jose", country="AU", postal_code="94560", state="California", state_code="US-CA", house_number_or_name="10", line1="Stafford Apartments", line2="29th Street", organization="Gr4vy", ), ), ), )) # Handle response print(res) ``` ```ts Typescript theme={"system"} const result = await gr4vy.checkoutSessions.create(); ``` > [!WARNING] > > **Checkout session expiry** > > Checkout sessions expire after 1 hour by default. Updating a checkout session does not extend the time. ## Loading token The token needs to be shared with the Android app over a custom API. Creating a token per checkout and only using a token for a short amount of time is recommended. ## Summary In this step: * Created a signed JWT token using a server-side SDK. * Shared the JWT with the Android app # Creating transactions Source: https://integration.wpay.com.au/guides/payments/android/quick-start/transact With the SDK ready, it's time to create a transaction. ## Open the payment sheet To open the payment sheet call the `.launch()` method on the SDK. ```java theme={"system"} gr4vySDK.launch( context = this, token = "[TOKEN]", amount = 1299, currency = "AUD", country = "AU" ) ``` > [!WARNING] > > Replace the `[TOKEN]` with the JWT created in step 2. Additional > [options](/guides/payments/android/options) can be provided when launching the SDK. ## Handle SDK events The `Gr4vyResultHandler` requires you to handle the events emitted by the SDKs as follows. ```java theme={"system"} override fun onGr4vyResult(result: Gr4vyResult) { when(result) { is Gr4vyResult.GeneralError -> startActivity(Intent(this, FailureActivity::class.java)) is Gr4vyResult.TransactionCreated -> startActivity(Intent(this, SuccessActivity::class.java)) is Gr4vyResult.TransactionFailed -> startActivity(Intent(this, FailureActivity::class.java)) } } ``` > [!NOTE] > > Learn more about the events triggered by the Android SDK in the [events](/guides/payments/android/events) guide. # Architecture Source: https://integration.wpay.com.au/guides/payments/chatgpt-app/architecture How MCP, tools, resources, and widgets fit together in a ChatGPT app. The patterns on this page apply to every implementation language and guide the decisions you'll make when you [set up the project](/guides/payments/chatgpt-app/setup) and [build the server](/guides/payments/chatgpt-app/mcp-server). ## How MCP works The [Model Context Protocol](https://modelcontextprotocol.io) (MCP) is an open protocol that standardises how AI models discover and interact with external applications. Your server declares what it can do, and ChatGPT decides when and how to use those capabilities during a conversation. MCP defines two core primitives: * **Tools** — Actions ChatGPT can invoke. Each tool has a name, a description (so ChatGPT knows *when* to use it), an input schema (*what parameters* to pass), and a handler (your business logic). * **Resources** — Content ChatGPT can present to the user. In a ChatGPT app, resources are HTML widgets embedded as iframes in the conversation—product catalog, shopping cart, and checkout form. When a user installs your app, ChatGPT connects to your MCP server, discovers the available tools and resources, and invokes them based on conversation context. Here is what a single interaction looks like end-to-end: **1. User sends a message** "What plants would do best in the sunroom? There are several windows on three sides of the room." **2. ChatGPT selects a tool** ChatGPT reads your tool descriptions and decides to call `list_products(sunlight: "Full Sun")`. **3. Your MCP server returns a three-part response** * A text summary ("Showing 12 plants") for ChatGPT's conversational context * Structured data (the product array + filter options) for the widget to render * Metadata pointing to the product catalog widget **4. ChatGPT embeds the widget** ChatGPT fetches the product catalog resource (a self-contained HTML file) and embeds it as an iframe in the conversation. **5. The widget renders** The widget receives the structured data and renders an interactive product grid with the "Full Sun" filter pre-selected. **6. The user interacts** The user browses products and adds items to their cart—all within the widget, with no further round-trips to ChatGPT until the user triggers a new action. ## Anatomy of a ChatGPT MCP app ![Diagram showing the architecture of the system](https://mintcdn.com/wpay/gi2877W1vP8KkMkR/assets/images/embed/chatgpt-app.svg?fit=max\&auto=format\&n=gi2877W1vP8KkMkR\&q=85\&s=6c14a696c72d6a3df0b5a6207b099265) * **Why tools and resources are separate.** Tools handle server-side logic such as querying your product database, validating prices, and calling the Gr4vy payment API. Resources, on the other hand, contain the client-side UI—the HTML, CSS, and JavaScript that ChatGPT renders in the conversation. When a tool runs, its return value includes both a text summary for ChatGPT's conversational context and structured data that ChatGPT forwards to a widget for display. * **Why widgets must be self-contained HTML.** ChatGPT renders each widget inside an iframe using the `text/html+skybridge` MIME type. Because the iframe is sandboxed, widgets cannot make external network requests to fetch stylesheets, scripts, or images from your server. This means you must inline all assets—styles, scripts, and any images—into a single HTML file that the resource returns. The same sandbox blocks popups and top-level navigation, which limits which payment methods can complete inside ChatGPT—see [payment method support inside ChatGPT](/guides/payments/chatgpt-app/gr4vy-embed#payment-method-support-inside-chatgpt). ## The three-part tool return value Every tool returns three things: 1. **`content`** — A text summary for ChatGPT's conversation context. Keep this minimal (for example, "Showing 14 products") since the widget handles display. 2. **`structuredContent`** — A JSON object containing the data for the widget. ChatGPT forwards this to the widget as `window.openai.toolOutput`. 3. **`_meta`** — Metadata telling ChatGPT *which* widget to render. The key field is `openai/outputTemplate`, which contains the URI of the resource to embed. ## The widget lifecycle Every widget follows the same lifecycle using the `window.openai` API inside the iframe: 1. **Receive initial data** — Wait for `window.openai.toolOutput` (the `structuredContent` from the tool). 2. **Restore persisted state** — Read `window.openai.widgetState` to restore state (for example, cart contents) from a previous render. 3. **Render the UI** — Display your app-specific design with the received data. 4. **Persist state on change** — Call `window.openai.setWidgetState(...)` so state survives re-renders. 5. **Listen for updates** — Handle `ui/notifications/tool-result` messages via `postMessage` for fresh data. 6. **Trigger the next step** — Call `window.openai.sendFollowUpMessage(...)` to advance the flow (for example, "Proceed to Checkout"). ## Server-side price validation **Never trust prices, amounts, or cart contents that come in through a tool call.** The arguments your tools receive are assembled by ChatGPT from widget interactions and conversation context—the model could alter a value, or a compromised widget could send manipulated input. The checkout tool must always look up canonical prices from your product catalog and pin them into the signed Gr4vy embed token. Mismatches are rejected by Gr4vy at checkout. ## Next steps Continue to [Set up the project](/guides/payments/chatgpt-app/setup). # Deploy to ChatGPT Source: https://integration.wpay.com.au/guides/payments/chatgpt-app/deployment Expose your MCP server over HTTPS, connect to ChatGPT, and go live. The last step is making your MCP endpoint reachable from ChatGPT over HTTPS. **1. Start the server locally** Build the widgets (if applicable) and start the development server: ```bash TypeScript theme={"system"} # Build the widget HTML files npm run build:widgets # Start the MCP server with hot reload npm run dev ``` ```bash Python theme={"system"} # Start the MCP server (widgets are served from assets/) python server.py ``` Your server will be running at `http://localhost:3000` (TypeScript) or `http://localhost:8000` (Python) with the MCP endpoint at `/mcp`. Keep this running in one terminal. **2. Expose your server with ngrok** ChatGPT needs an HTTPS URL to connect to your MCP server. Use [ngrok](https://ngrok.com) to create a tunnel: ```bash theme={"system"} ngrok http 3000 # or 8000 for the Python example ``` ngrok will provide a public URL like `https://abc123.ngrok-free.app`. Your MCP endpoint is at `https://abc123.ngrok-free.app/mcp`. **3. Configure Content Security Policy domains** Configure CSP domains in your OpenAI app settings so the checkout widget can load Gr4vy Embed: * **`connectDomains`**: `["api.*.gr4vy.app"]` * **`resourceDomains`**: `["cdn.*.gr4vy.app"]` * **`frameDomains`**: `["*.gr4vy.app"]` > [!WARNING] > > These are optional during local development with ngrok, but required in production. Without them, Gr4vy Embed will fail silently. **4. Connect to ChatGPT** 1. Open ChatGPT (a **Pro**, **Business**, **Team**, or **Enterprise** subscription is required). 2. Enable **Developer Mode** in your ChatGPT settings. 3. Navigate to **Settings → Apps** (or the equivalent in your ChatGPT version). 4. Click **Create App**. 5. Paste your MCP endpoint URL: `https://abc123.ngrok-free.app/mcp` 6. ChatGPT auto-discovers your tools (`list_products`, `show_cart`, `start_checkout`) and resources. Start a new conversation in ChatGPT and enable the app: ![Screenshot of ChatGPT showing the option to enable the app in a conversation](https://mintcdn.com/wpay/gi2877W1vP8KkMkR/assets/images/embed/enable_app_chatgpt.png?fit=max\&auto=format\&n=gi2877W1vP8KkMkR\&q=85\&s=670d938f64a30d11cc0663afc2705350) * Ask "What plants do you have?" — the catalog widget should appear inline. * Filter, add a few items, then click **Proceed to Checkout** — the checkout widget should appear with a Gr4vy payment form. * Enter a test card and submit — the `onComplete` callback should fire with a transaction ID. **5. View transaction in Gr4vy Dashboard** After a successful payment, the transaction appears in your Gr4vy dashboard where you can manage routing rules, payment methods, and orchestration. **6. Deploy to production** Deploy your MCP server to any hosting provider that supports your runtime: 1. Set all Gr4vy environment variables (`GR4VY_ID`, `GR4VY_PRIVATE_KEY` or `GR4VY_PRIVATE_KEY_PATH`, `GR4VY_MERCHANT_ACCOUNT_ID`, `GR4VY_ENVIRONMENT`, `GR4VY_WEBHOOK_SECRET`) on your hosting provider. 2. Build the widgets and server as part of your deploy pipeline (`npm run build` for TypeScript, or `pip install .` for Python). 3. Use the appropriate start command for your runtime (`npm start` for TypeScript, `python server.py` for Python). 4. Update your ChatGPT App's MCP URL to point at your production HTTPS domain. 5. Register the production webhook URL in your Gr4vy dashboard (`https://your-domain.com/webhooks/gr4vy`) if you enabled webhook verification. 6. Switch `GR4VY_ENVIRONMENT` from `sandbox` to `production` once you are ready. Once these are in place, your ChatGPT app is live and ready to accept real payments through Gr4vy. # Integrate Gr4vy Embed Source: https://integration.wpay.com.au/guides/payments/chatgpt-app/gr4vy-embed Generate embed tokens, load Gr4vy Embed in the checkout widget, and verify webhooks. [Gr4vy Embed](/guides/payments/embed/quick-start/overview) renders a PCI-compliant payment form inside the checkout widget. The integration has a server-side component (embed token generation) and a client-side component (loading the payment form). ## Payment method support inside ChatGPT > [!WARNING] > > ChatGPT renders each widget in a sandboxed iframe that cannot open popups or navigate the top-level page. Gr4vy Embed's redirect handling—the [`redirectMode`](/guides/payments/embed/options) options—relies on one of those two capabilities. As a result, any payment method whose flow ends in an `approval_url` on a third-party domain cannot finish inside a ChatGPT app today. A payment method works inside ChatGPT only when its entire authentication flow stays within the nested iframe, with no popup and no top-level redirect. * **Card** is fully supported. Secure Fields capture the card details in the iframe, and the 3-D Secure (3DS) challenge renders in the iframe. * **Venmo** is supported. Its approval flow uses a QR code that the buyer scans on a separate device, so nothing in the iframe needs to redirect. * **Other methods may work** when their flow never leaves the iframe. For example, Apple Pay uses the browser-native payment sheet, which the operating system presents rather than the iframe. This is not guaranteed—test each method before relying on it. * **Redirect-based methods do not work.** Methods that send the buyer to a third-party domain to authenticate, such as PayPal, cannot complete inside a ChatGPT app. A stored (vaulted) payment method often works even when its first-time flow would not, because the redirect-based consent happened when the buyer first stored the method. A later charge against the stored token usually runs without a redirect. This is not guaranteed for every payment service provider, so test the stored-method flow as well. **1. Generate the embed token (server-side)** Use the [Gr4vy SDK](/guides/payments/embed/quick-start/sdks) to generate a signed JWT embed token: ```ts TypeScript theme={"system"} // src/server/services/purchases.ts import { readFileSync } from "fs"; import { Gr4vy, getEmbedToken, withToken, verifyWebhook } from "@gr4vy/sdk"; interface CheckoutItem { name: string; quantity: number; unitAmount: number; // in cents } function getPrivateKey(): string { const privateKeyPath = process.env.GR4VY_PRIVATE_KEY_PATH; if (!privateKeyPath) { throw new Error("Set GR4VY_PRIVATE_KEY_PATH environment variable."); } return readFileSync(privateKeyPath, "utf8"); } function getGr4vyClient(): Gr4vy { const gr4vyId = process.env.GR4VY_ID; if (!gr4vyId) { throw new Error("Set GR4VY_ID environment variable."); } return new Gr4vy({ server: (process.env.GR4VY_ENVIRONMENT || "sandbox") as "sandbox" | "production", id: gr4vyId, merchantAccountId: process.env.GR4VY_MERCHANT_ACCOUNT_ID, bearerAuth: withToken({ privateKey: getPrivateKey() }), }); } export async function generateEmbedToken( amount: number, currency: string = "AUD", cartItems: CheckoutItem[] = [] ): Promise { const gr4vy = getGr4vyClient(); const privateKey = getPrivateKey(); // 1. Create a checkout session on the Gr4vy backend const checkoutSession = await gr4vy.checkoutSessions.create({ amount, currency, cartItems: cartItems.map((item) => ({ name: item.name, quantity: item.quantity, unitAmount: item.unitAmount, })), metadata: { source: "plantly-chatgpt-app", }, }); // 2. Generate a signed JWT embed token return getEmbedToken({ privateKey, checkoutSessionId: checkoutSession.id, embedParams: { amount, currency, merchantAccountId: process.env.GR4VY_MERCHANT_ACCOUNT_ID, cartItems: cartItems.map((item) => ({ name: item.name, quantity: item.quantity, unitAmount: item.unitAmount, })), }, }); } ``` ```python Python theme={"system"} # purchases.py import os from gr4vy import auth def generate_embed_token(buyer_id: str, amount: int, currency: str = "AUD") -> str: """Generate a Gr4vy Embed token for client-side checkout. Args: buyer_id: The Gr4vy buyer ID. amount: Transaction amount in smallest currency unit (cents). currency: ISO 4217 currency code. Returns: A signed JWT token for Gr4vy Embed. """ private_key = os.environ.get("GR4VY_PRIVATE_KEY") if not private_key: raise ValueError("Set GR4VY_PRIVATE_KEY environment variable.") return auth.get_embed_token( private_key, embed_params={ "amount": amount, "currency": currency, "buyer_id": buyer_id, }, ) ``` > [!WARNING] > > The JWT token pins the payment amount and cart items. If the checkout widget passes mismatched values to `gr4vy.setup()`, Gr4vy rejects the request. The values must match exactly. **2. Load Gr4vy Embed in the checkout widget** The checkout widget follows the same [widget lifecycle](/guides/payments/chatgpt-app/widgets) as other widgets. After receiving the embed configuration from `start_checkout`, it loads the Gr4vy Embed script and initialises the payment form. This code runs inside the widget regardless of server language. **Receive the embed configuration** ```tsx TypeScript theme={"system"} // src/widgets/checkout/App.tsx const [embedConfig, setEmbedConfig] = useState(null); const [embedError, setEmbedError] = useState(null); const embedLoaded = useRef(false); useEffect(() => { waitForToolOutput().then((data) => { if (data.embed) setEmbedConfig(data.embed); if (data.embed_error) setEmbedError(data.embed_error); }); const unsub = onToolResult((data) => { if (data?.embed) setEmbedConfig(data.embed); }); return unsub; }, []); ``` ```js Python theme={"system"} // assets/checkout.html (inline ``` > [!NOTE] > > When using the CDN the latest version of the library is always pulled straight > from the server for every request. ### Content Security Policy If your checkout page sets a Content Security Policy (CSP), allow the hosts that Embed uses. | Directive | Host | Used for | | - | - | - | | `frame-src` | `embed.{instance_id}.gr4vy.app` | The Embed iframe. | | `script-src` | `cdn.{instance_id}.gr4vy.app` | The library, if you load it from the CDN. | | `script-src` | `applepay.cdn-apple.com` | The Apple Pay JS SDK, if you offer [Apple Pay](/guides/features/apple-pay/web) in browsers other than Safari. | In sandbox, add `sandbox.` after the service name, for example `embed.sandbox.{instance_id}.gr4vy.app`. The wildcard `https://*.gr4vy.app` covers the Gr4vy hosts in both environments. If you only set `default-src`, add the hosts there instead. ## Initialize the UI With Embed installed, initializing the embedded payment page is now possible. Embed expects the ID of your instance (`gr4vyId`), the transaction amount and currency, the country to process this transaction in, and the embed token you generated in the previous step. ```js React theme={"system"} import Embed from "@gr4vy/embed-react" // or: const { default: Embed } = require("@gr4vy/embed-react") ``` ```html Node theme={"system"}
``` ```html CDN theme={"system"}
``` > [!NOTE] > > The instance ID is the unique identifier for the deployment of the system and is included in every API call. > Together with the environment (sandbox or production) it is used to connect to the right APIs, as well as dashboard. > > The possible values for your ID are one of `wpay`, `wpay1`, `wpay2`, or `wpay3`. You are informed > in which instance your account has been set up. > [!NOTE] > > The Node and CDN versions of Embed need to be attached to an HTML element. > In this case, the UI is attached to a `
` with the class `container`. > The UI can be attached to any element using any `querySelector`-compatible query. Embed should now be loaded on the page. The available payment methods heavily depend on the enabled payment services in the account. ## Catch transaction ID The Embed UI handles the capture of any payment details and then creates a transaction. Once the transaction has been created Embed submits the form it was attached to and append the query string parameters `gr4vy_transaction_id` and `gr4vy_transaction_status`. Optionally, this form submission behavior can be overridden using the [`onComplete`](/guides/payments/embed/options#options) option. ```js React theme={"system"} const Embed = require("@gr4vy/embed-react") // or: import Embed from "@gr4vy/embed-react"
{ ... }} />
``` ```html Node theme={"system"}
``` ```html CDN theme={"system"}
``` > [!NOTE] > > **`form` vs `onComplete`** > > In this example, Embed uses the `onComplete` callback to catch the transaction ID when > the transaction was created. When no `onComplete` is present it submits the > attached form and appends `gr4vy_transaction_id` and > `gr4vy_transaction_status` to the query string of the page that's being submitted to. ### Submission without a form Embed does not require a form to be present, `submit` can be called directly. `onComplete` should be used if you are choosing this option. Additionally, the `form` option should not be passed in this scenario to avoid issues related to Embed being submitted twice in a row. ```js React theme={"system"} const { default: Embed, EmbedInstance } = require("@gr4vy/embed-react") // or: import Embed, { EmbedInstance } from "@gr4vy/embed-react" const embed = useRef();
{ ... }} />
``` ```html Node theme={"system"}
``` ```html CDN theme={"system"}
``` ## Summary In this step you: * Installed and initialized Embed. * Caught the resulting transaction identifier. # Inspecting transactions Source: https://integration.wpay.com.au/guides/payments/embed/quick-start/inspect After the transaction has been created, the app can get the latest details and status using the transaction ID submitted in the form or passed back in the `onComplete` callback. This transaction ID can be used with the SDK to fetch additional details about the transaction. ```csharp C# theme={"system"} using Gr4vy; using Gr4vy.Models.Components; var sdk = new Gr4vySDK( merchantAccountId: "default", bearerAuth: "" ); var res = await sdk.Transactions.GetAsync(transactionId: "7099948d-7286-47e4-aad8-b68f7eb44591"); // handle response ``` ```go Go theme={"system"} package main import( "context" gr4vygo "github.com/gr4vy/gr4vy-go" "os" "log" ) func main() { ctx := context.Background() s := gr4vygo.New( gr4vygo.WithMerchantAccountID("default"), gr4vygo.WithSecurity(os.Getenv("GR4VY_BEARER_AUTH")), ) res, err := s.Transactions.Get(ctx, "7099948d-7286-47e4-aad8-b68f7eb44591") if err != nil { log.Fatal(err) } if res != nil { // handle response } } ``` ```java Java theme={"system"} package hello.world; import com.gr4vy.sdk.Gr4vy; import com.gr4vy.sdk.models.errors.*; import com.gr4vy.sdk.models.operations.GetTransactionResponse; import java.lang.Exception; public class Application { public static void main(String[] args) throws Exception { Gr4vy sdk = Gr4vy.builder() .merchantAccountId("default") .bearerAuth("") .build(); GetTransactionResponse res = sdk.transactions().get() .transactionId("7099948d-7286-47e4-aad8-b68f7eb44591") .call(); if (res.transaction().isPresent()) { // handle response } } } ``` ```php PHP theme={"system"} declare(strict_types=1); require 'vendor/autoload.php'; use Gr4vy; $sdk = Gr4vy\SDK::builder() ->setMerchantAccountId('default') ->setSecurity( '' ) ->build(); $response = $sdk->transactions->get( transactionId: '7099948d-7286-47e4-aad8-b68f7eb44591' ); if ($response->transaction !== null) { // handle response } ``` ```python Python theme={"system"} from gr4vy import Gr4vy import os with Gr4vy( merchant_account_id="default", bearer_auth=os.getenv("GR4VY_BEARER_AUTH", ""), ) as g_client: res = g_client.transactions.get(transaction_id="7099948d-7286-47e4-aad8-b68f7eb44591") # Handle response print(res) ``` The transaction includes details about the payment method used, and the status of the transaction. ```json Response theme={"system"} { "type": "transaction", "id": "f7099948d-7286-47e4-aad8-b68f7eb44591", "status": "authorized", "amount": 1299, "currency": "AUD", "payment_method": { "type": "payment-method", "id": "77a76f7e-d2de-4bbc-ada9-d6a0015e6bd5", "method": "card", ... }, ... } ``` > [!NOTE] > > Visit the [API reference documentation](/reference) for full details about the > transaction resource, and any other API. # Creating an API key for use with Embed Source: https://integration.wpay.com.au/guides/payments/embed/quick-start/key Before the app can communicate with the API, create a new API key. The API handles authentication using a signed **JSON Web Token** (JWT) which is passed in a HTTP header. ```bash theme={"system"} curl -X GET https://api.example.gr4vy.app/transactions \ -H "authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIi..." ``` > [!TIP] > > Using an SDK is recommended. SDKs make the process of generating a token a lot easier and are probably > available in the preferred programming language. This is covered in the > next step. ## Create a new API key To use the API you need to generate a new **API key**. Please reach out to your Wpay representative who provides you with the API key. > [!NOTE] > > Learn more about private keys, key-pairs, and JWT in the > [in-depth guide on authentication](/guides/api/authentication). ## Summary In this step: * Learned about API authentication. * Created a new private key for the API. * Stored this API key in a secure place where the app can access it. # Going live Source: https://integration.wpay.com.au/guides/payments/embed/quick-start/live After testing your implementation in your sandbox environment, the next step is to set up your production environment and go live. 1. Change the `environment` parameter to `production`. 2. Create a production API key ```html React theme={"system"}
``` ```js Node theme={"system"} setup({ gr4vyId: "example", environment: "production" ... }); ``` ```js CDN theme={"system"} gr4vy.setup({ gr4vyId: "example", environment "production", ... }); ``` > [!NOTE] > > The instance ID is the unique identifier for the deployment of the system and is included in every API call. > Together with the environment (sandbox or production) it is used to connect to the right APIs, as well as dashboard. > > The possible values for your ID are one of `wpay`, `wpay1`, `wpay2`, or `wpay3`. You are informed > in which instance your account has been set up. After completion, your transactions should appear in production environment. ## Summary * Go live with Embed # Embed Source: https://integration.wpay.com.au/guides/payments/embed/quick-start/overview Get set up with Embed as your web checkout experience. ![Embed](https://mintcdn.com/wpay/c50rx3S48hHhNNxb/assets/images/embed/hero.png?fit=max\&auto=format\&n=c50rx3S48hHhNNxb\&q=85\&s=2083b6e3f9a51025d578e0fa98f2be45) Embed is the hosted checkout experience that provides you with a payment selector integrated right inside your checkout page. In this quick start guide you are taken through the following steps. 1. [Create an API key](/guides/payments/embed/quick-start/key) which is needed to generate the signed JWT token in step 2. 2. [Set up a server-side SDK](/guides/payments/embed/quick-start/sdks) to generate a client token for authenticating to the API. 3. [Set up Embed](/guides/payments/embed/quick-start/embed) which handles the capture of the payment method details. 4. [Process the transaction](/guides/payments/embed/quick-start/inspect) and inspect the result. 5. [And finally prepare to go live](/guides/payments/embed/quick-start/live) by switching to production. # Generating tokens for authentication Source: https://integration.wpay.com.au/guides/payments/embed/quick-start/sdks Before adding Embed to the web checkout, create a client token that can be used to authenticate the transaction. ## Install a server-side SDK Use the package manager in the preferred programming language to install the server-side SDK. Token generation can only be done server side and doing this client side is not recommended as it exposes the API key to customers. ```sh C# theme={"system"} dotnet add package Gr4vy ``` ```sh Go theme={"system"} go get github.com/gr4vy/gr4vy-go ``` ```sh Java theme={"system"} # Please check for the latest version implementation 'com.gr4vy:sdk:1.0.0' ``` ```sh PHP theme={"system"} composer require "gr4vy/gr4vy-php" ``` ```sh Python theme={"system"} pip install gr4vy ``` ```sh TypeScript theme={"system"} npm install @gr4vy/sdk # or: yarn add @gr4vy/sdk ``` > [!NOTE] > > Please always check and install the [latest release](/guides/get-started#server-side-sdks) of the preferred SDK. ## Initialize the SDK client Next, initialize the SDK with the ID of the instance and the private key. ```csharp C# theme={"system"} using Gr4vy; using Gr4vy.Models.Components; using System.Collections.Generic; // Loaded the key from a file, env variable, // or anywhere else var privateKey = "..."; var sdk = new Gr4vySDK( id: "example", server: SDKConfig.Server.Sandbox, bearerAuthSource: Auth.WithToken(privateKey), merchantAccountId: "default" ); ``` ```go Go theme={"system"} package main import ( "context" gr4vy "github.com/gr4vy/gr4vy-go" "github.com/gr4vy/gr4vy-go/models/operations" "log" "os" ) func main() { ctx := context.Background() privateKey := "...." // Private key loaded from disk or env var withToken := gr4vy.WithToken(privateKey, []JWTScope{ReadAll, WriteAll}, 60) s := gr4vy.New( gr4vy.WithID("example"), gr4vy.WithServer(gr4vy.ServerSandbox), gr4vy.WithSecuritySource(withToken), gr4vy.WithMerchantAccountID("default"), ) } ``` ```java Java theme={"system"} package hello.world; import com.gr4vy.sdk.BearerSecuritySource; import com.gr4vy.sdk.Gr4vy; import com.gr4vy.sdk.Gr4vy.AvailableServers; import com.gr4vy.sdk.models.components.AccountUpdaterJobCreate; import com.gr4vy.sdk.models.errors.*; import com.gr4vy.sdk.models.operations.ListTransactionsRequest; import java.lang.Exception; import java.util.List; public class Application { public static void main(String[] args) throws Exception { String privateKey = "-----BEGIN PRIVATE KEY-----\n...."; // a valid private key Gr4vy sdk = Gr4vy.builder() .id("example") .server(AvailableServers.SANDBOX) .merchantAccountId("default") .securitySource(new BearerSecuritySource.Builder(privateKey).build()) .build(); } } ``` ```php PHP theme={"system"} declare(strict_types=1); require 'vendor/autoload.php'; use Gr4vy; use Gr4vy\Auth; // Loaded the key from a file, env variable, // or anywhere else $privateKey = "..."; $sdk = Gr4vy\SDK::builder() ->setId('example') ->setServer('sandbox') ->setSecuritySource(Auth::withToken($privateKey)) ->setMerchantAccountId('default') ->build(); ``` ```python Python theme={"system"} from gr4vy import Gr4vy, auth import os client = Gr4vy( id="example", server="production", merchant_account_id="default", bearer_auth=auth.with_token(open("./private_key.pem").read()) ) ``` ```ts TypeScript theme={"system"} import fs from "fs"; import { Gr4vy, withToken } from "@gr4vy/sdk"; const gr4vy = new Gr4vy({ server: "sandbox", id: "example", bearerAuth: withToken({ privateKey: fs.readFileSync("private_key.pem", "utf8"), }), }); ``` > [!NOTE] > > The instance ID is the unique identifier for the deployment of the system and is included in every API call. > Together with the environment (sandbox or production) it is used to connect to the right APIs, as well as dashboard. > > The possible values for your ID are one of `wpay`, `wpay1`, `wpay2`, or `wpay3`. You are informed > in which instance your account has been set up. ## Generate a token The next step is to create a new token for frontend authentication. The SDK comes with a helper function for exactly that purpose. It requires an amount and currency for the transaction to be created and additionally accepts either the ID of a buyer or an external identifier for a buyer to link the transaction to. For Embed, it is recommended to attach a checkout session to every transaction. The `getEmbedTokenWithCheckoutSession` helper creates a checkout session using your SDK client and returns an Embed token with the resulting `checkout_session_id` already pinned, in a single call. ```csharp C# theme={"system"} using System.Collections.Generic; using Gr4vy; // Loaded the key from a file, env variable, // or anywhere else var privateKey = "..."; var sdk = new Gr4vySDK( id: "example", server: SDKConfig.Server.Sandbox, bearerAuthSource: Auth.WithToken(privateKey), merchantAccountId: "default" ); var token = await Auth.GetEmbedTokenWithCheckoutSession( client: sdk, privateKey: privateKey, embedParams: new Dictionary { ["amount"] = 1299, ["currency"] = "AUD", ["buyer_external_identifier"] = "user-1234", } ); ``` ```go Go theme={"system"} package main import ( "context" gr4vy "github.com/gr4vy/gr4vy-go" "log" ) func main() { privateKey := "...." // Private key loaded from disk or env var client := gr4vy.New( gr4vy.WithID("example"), gr4vy.WithServer(gr4vy.ServerSandbox), gr4vy.WithSecuritySource(gr4vy.WithToken(privateKey, nil, 3600)), gr4vy.WithMerchantAccountID("default"), ) embedParams := map[string]interface{}{ "amount": 1299, "currency": "AUD", "buyer_external_identifier": "user-1234", } token, err := gr4vy.GetEmbedTokenWithCheckoutSession(context.Background(), client, privateKey, embedParams, nil, nil) if err != nil { log.Fatal(err) } log.Println(token) } ``` ```java Java theme={"system"} package hello.world; import com.gr4vy.sdk.Auth; import com.gr4vy.sdk.BearerSecuritySource; import com.gr4vy.sdk.Gr4vy; import com.gr4vy.sdk.Gr4vy.AvailableServers; import java.util.HashMap; import java.util.Map; public class Application { public static void main(String[] args) throws Exception { String privateKey = "-----BEGIN PRIVATE KEY-----\n...."; // a valid private key Gr4vy client = Gr4vy.builder() .id("example") .server(AvailableServers.SANDBOX) .securitySource(new BearerSecuritySource.Builder(privateKey).build()) .merchantAccountId("default") .build(); Map embedParams = new HashMap<>(); embedParams.put("amount", 1299); embedParams.put("currency", "AUD"); embedParams.put("buyer_external_identifier", "user-1234"); String token = Auth.getEmbedTokenWithCheckoutSession(client, privateKey, embedParams); System.out.println(token); } } ``` ```php PHP theme={"system"} declare(strict_types=1); require 'vendor/autoload.php'; use Gr4vy; use Gr4vy\Auth; // Loaded the key from a file, env variable, // or anywhere else $privateKey = "..."; $sdk = Gr4vy\SDK::builder() ->setId('example') ->setServer('sandbox') ->setSecuritySource(Auth::withToken($privateKey)) ->setMerchantAccountId('default') ->build(); $token = Auth::getEmbedTokenWithCheckoutSession( client: $sdk, privateKey: $privateKey, embedParams: [ "amount" => 1299, "currency" => "AUD", "buyer_external_identifier" => "user-1234", ], ); ``` ```python Python theme={"system"} from gr4vy import Gr4vy, auth private_key = open("./private_key.pem").read() g_client = Gr4vy( id="example", server="production", merchant_account_id="default", bearer_auth=auth.with_token(private_key) ) token = auth.get_embed_token_with_checkout_session( g_client, private_key, embed_params={ "amount": 1299, "currency": "AUD", "buyer_external_identifier": "user-1234", }, ) ``` ```ts TypeScript theme={"system"} import { Gr4vy, getEmbedTokenWithCheckoutSession, withToken } from "@gr4vy/sdk"; import fs from "fs"; async function run() { const privateKey = fs.readFileSync("private_key.pem", "utf8") const gr4vy = new Gr4vy({ server: "sandbox", id: "example", bearerAuth: withToken({ privateKey }), }); const token = await getEmbedTokenWithCheckoutSession({ client: gr4vy, privateKey, embedParams: { amount: 1299, currency: 'AUD', buyerExternalIdentifier: 'user-1234', } }); console.log(token); } run(); ``` This token can now be served to the frontend where it is used by the SDK. If generating the client token without using a server-side SDK is preferred, then please read the guide on [manually generating a JWT](/guides/api/authentication#without-sdks). ## Checkout sessions Passing a [checkout session](/reference/checkout-sessions/new-checkout-session) ID to the Embed token function is recommended. This ID can be used to help identify multiple transaction attempts. This is useful to identify if one or more failed/declined transactions eventually result in a successful transaction. Checkout sessions can also store `cart_items`, `metadata`, airline data, and more, removing the need to pass these values on the transaction request. ```csharp C# theme={"system"} using Gr4vy; using Gr4vy.Models.Components; using NodaTime; using System; using System.Collections.Generic; var res = await sdk.CheckoutSessions.CreateAsync(checkoutSessionCreate: new CheckoutSessionCreate() { CartItems = new List() { new CartItem() { Name = "GoPro HD", Quantity = 2, UnitAmount = 1299, DiscountAmount = 0, TaxAmount = 0, ExternalIdentifier = "goprohd", Sku = "GPHD1078", ProductUrl = "https://example.com/catalog/go-pro-hd", ImageUrl = "https://example.com/images/go-pro-hd.jpg", Categories = new List() { "camera", "travel", "gear", }, ProductType = "physical", SellerCountry = "GB", }, new CartItem() { Name = "GoPro HD", Quantity = 2, UnitAmount = 1299, DiscountAmount = 0, TaxAmount = 0, ExternalIdentifier = "goprohd", Sku = "GPHD1078", ProductUrl = "https://example.com/catalog/go-pro-hd", ImageUrl = "https://example.com/images/go-pro-hd.jpg", Categories = new List() { "camera", "travel", "gear", }, ProductType = "physical", SellerCountry = "GB", }, new CartItem() { Name = "GoPro HD", Quantity = 2, UnitAmount = 1299, DiscountAmount = 0, TaxAmount = 0, ExternalIdentifier = "goprohd", Sku = "GPHD1078", ProductUrl = "https://example.com/catalog/go-pro-hd", ImageUrl = "https://example.com/images/go-pro-hd.jpg", Categories = new List() { "camera", "travel", "gear", }, ProductType = "physical", SellerCountry = "AU", }, }, Metadata = new Dictionary() { { "cohort", "cohort-a" }, { "order_id", "order-12345" }, }, Buyer = new GuestBuyer() { DisplayName = "John Doe", ExternalIdentifier = "buyer-12345", BillingDetails = new BillingDetails() { FirstName = "John", LastName = "Doe", EmailAddress = "john@example.com", PhoneNumber = "+1234567890", Address = new Address() { City = "San Jose", Country = "AU", PostalCode = "94560", State = "California", StateCode = "US-CA", HouseNumberOrName = "10", Line1 = "Stafford Apartments", Line2 = "29th Street", Organization = "Gr4vy", }, TaxId = new TaxId() { Value = "12345678931", Kind = "ar.cuit", }, }, ShippingDetails = new ShippingDetailsCreate() { FirstName = "John", LastName = "Doe", EmailAddress = "john@example.com", PhoneNumber = "+1234567890", Address = new Address() { City = "San Jose", Country = "AU", PostalCode = "94560", State = "California", StateCode = "US-CA", HouseNumberOrName = "10", Line1 = "Stafford Apartments", Line2 = "29th Street", Organization = "Gr4vy", }, }, }, }); // handle response ``` ```go Go theme={"system"} package main import( "context" gr4vygo "github.com/gr4vy/gr4vy-go" "os" "github.com/gr4vy/gr4vy-go/models/components" "github.com/gr4vy/gr4vy-go/types" "log" ) func main() { ctx := context.Background() // Init SDK res, err := s.CheckoutSessions.Create(ctx, &components.CheckoutSessionCreate{ CartItems: []components.CartItem{ components.CartItem{ Name: "GoPro HD", Quantity: 2, UnitAmount: 1299, DiscountAmount: gr4vygo.Int64(0), TaxAmount: gr4vygo.Int64(0), ExternalIdentifier: gr4vygo.String("goprohd"), Sku: gr4vygo.String("GPHD1078"), ProductURL: gr4vygo.String("https://example.com/catalog/go-pro-hd"), ImageURL: gr4vygo.String("https://example.com/images/go-pro-hd.jpg"), Categories: []string{ "camera", "travel", "gear", }, ProductType: components.ProductTypePhysical.ToPointer(), SellerCountry: gr4vygo.String("AU"), }, components.CartItem{ Name: "GoPro HD", Quantity: 2, UnitAmount: 1299, DiscountAmount: gr4vygo.Int64(0), TaxAmount: gr4vygo.Int64(0), ExternalIdentifier: gr4vygo.String("goprohd"), Sku: gr4vygo.String("GPHD1078"), ProductURL: gr4vygo.String("https://example.com/catalog/go-pro-hd"), ImageURL: gr4vygo.String("https://example.com/images/go-pro-hd.jpg"), Categories: []string{ "camera", "travel", "gear", }, ProductType: components.ProductTypePhysical.ToPointer(), SellerCountry: gr4vygo.String("AU"), }, }, Metadata: map[string]string{ "cohort": "cohort-a", "order_id": "order-12345", }, Buyer: nil, }) if err != nil { log.Fatal(err) } if res != nil { // handle response } } ``` ```java Java theme={"system"} package hello.world; import com.gr4vy.sdk.Gr4vy; import com.gr4vy.sdk.models.components.*; import com.gr4vy.sdk.models.errors.*; import com.gr4vy.sdk.models.operations.CreateCheckoutSessionResponse; import java.lang.Exception; import java.time.LocalDate; import java.time.OffsetDateTime; import java.util.List; import java.util.Map; import org.openapitools.jackson.nullable.JsonNullable; public class Application { public static void main(String[] args) throws Exception { // Init SDK CreateCheckoutSessionResponse res = sdk.checkoutSessions().create() .checkoutSessionCreate(CheckoutSessionCreate.builder() .cartItems(List.of( CartItem.builder() .name("GoPro HD") .quantity(2L) .unitAmount(1299L) .discountAmount(0L) .taxAmount(0L) .externalIdentifier("goprohd") .sku("GPHD1078") .productUrl("https://example.com/catalog/go-pro-hd") .imageUrl("https://example.com/images/go-pro-hd.jpg") .categories(List.of( "camera", "travel", "gear")) .productType(ProductType.PHYSICAL) .sellerCountry("AU") .build(), CartItem.builder() .name("GoPro HD") .quantity(2L) .unitAmount(1299L) .discountAmount(0L) .taxAmount(0L) .externalIdentifier("goprohd") .sku("GPHD1078") .productUrl("https://example.com/catalog/go-pro-hd") .imageUrl("https://example.com/images/go-pro-hd.jpg") .categories(List.of( "camera", "travel", "gear")) .productType(ProductType.PHYSICAL) .sellerCountry("AU") .build())) .metadata(Map.ofEntries( Map.entry("cohort", "cohort-a"), Map.entry("order_id", "order-12345"))) .buyer(JsonNullable.of(null)) .build()) .call(); if (res.checkoutSession().isPresent()) { // handle response } } } ``` ```php PHP theme={"system"} declare(strict_types=1); require 'vendor/autoload.php'; use Brick\DateTime\LocalDate; use Gr4vy; use Gr4vy\Utils; // Init SDK $checkoutSessionCreate = new Gr4vy\CheckoutSessionCreate( cartItems: [ new Gr4vy\CartItem( name: 'GoPro HD', quantity: 2, unitAmount: 1299, discountAmount: 0, taxAmount: 0, externalIdentifier: 'goprohd', sku: 'GPHD1078', productUrl: 'https://example.com/catalog/go-pro-hd', imageUrl: 'https://example.com/images/go-pro-hd.jpg', categories: [ 'camera', 'travel', 'gear', ], productType: 'physical', sellerCountry: 'AU', ), new Gr4vy\CartItem( name: 'GoPro HD', quantity: 2, unitAmount: 1299, discountAmount: 0, taxAmount: 0, externalIdentifier: 'goprohd', sku: 'GPHD1078', productUrl: 'https://example.com/catalog/go-pro-hd', imageUrl: 'https://example.com/images/go-pro-hd.jpg', categories: [ 'camera', 'travel', 'gear', ], productType: 'physical', sellerCountry: 'AU', ), ], metadata: [ 'cohort' => 'cohort-a', 'order_id' => 'order-12345', ], buyer: new Gr4vy\GuestBuyer( displayName: 'John Doe', externalIdentifier: 'buyer-12345', billingDetails: new Gr4vy\BillingDetails( firstName: 'John', lastName: 'Doe', emailAddress: 'john@example.com', phoneNumber: '+1234567890', address: new Gr4vy\Address( city: 'San Jose', country: 'AU', postalCode: '94560', state: 'California', stateCode: 'US-CA', houseNumberOrName: '10', line1: 'Stafford Apartments', line2: '29th Street', organization: 'Gr4vy', ), taxId: new Gr4vy\TaxId( value: '12345678931', kind: 'my.frp', ), ), shippingDetails: new Gr4vy\ShippingDetailsCreate( firstName: 'John', lastName: 'Doe', emailAddress: 'john@example.com', phoneNumber: '+1234567890', address: null, ), ), ); $response = $sdk->checkoutSessions->create( checkoutSessionCreate: $checkoutSessionCreate ); if ($response->checkoutSession !== null) { // handle response } ``` ```python Python theme={"system"} from datetime import date from gr4vy import Gr4vy, models from gr4vy.utils import parse_datetime import os # Init SDK res = g_client.checkout_sessions.create(checkout_session_create=models.CheckoutSessionCreate( cart_items=[ models.CartItem( name="GoPro HD", quantity=2, unit_amount=1299, discount_amount=0, tax_amount=0, external_identifier="goprohd", sku="GPHD1078", product_url="https://example.com/catalog/go-pro-hd", image_url="https://example.com/images/go-pro-hd.jpg", categories=[ "camera", "travel", "gear", ], product_type="physical", seller_country="GB", ), models.CartItem( name="GoPro HD", quantity=2, unit_amount=1299, discount_amount=0, tax_amount=0, external_identifier="goprohd", sku="GPHD1078", product_url="https://example.com/catalog/go-pro-hd", image_url="https://example.com/images/go-pro-hd.jpg", categories=[ "camera", "travel", "gear", ], product_type="physical", seller_country="GB", ), models.CartItem( name="GoPro HD", quantity=2, unit_amount=1299, discount_amount=0, tax_amount=0, external_identifier="goprohd", sku="GPHD1078", product_url="https://example.com/catalog/go-pro-hd", image_url="https://example.com/images/go-pro-hd.jpg", categories=[ "camera", "travel", "gear", ], product_type="physical", seller_country="GB", ), ], metadata={ "cohort": "cohort-a", "order_id": "order-12345", }, buyer=models.GuestBuyer( display_name="John Doe", external_identifier="buyer-12345", billing_details=models.BillingDetails( first_name="John", last_name="Doe", email_address="john@example.com", phone_number="+1234567890", address=models.Address( city="San Jose", country="AU", postal_code="94560", state="California", state_code="US-CA", house_number_or_name="10", line1="Stafford Apartments", line2="29th Street", organization="Gr4vy", ), tax_id=models.TaxID( value="12345678931", kind="ar.cuit", ), ), shipping_details=models.ShippingDetailsCreate( first_name="John", last_name="Doe", email_address="john@example.com", phone_number="+1234567890", address=models.Address( city="San Jose", country="AU", postal_code="94560", state="California", state_code="US-CA", house_number_or_name="10", line1="Stafford Apartments", line2="29th Street", organization="Gr4vy", ), ), ), )) # Handle response print(res) ``` ```ts Typescript theme={"system"} const result = await gr4vy.checkoutSessions.create(); ``` > [!WARNING] > > **Checkout session expiry** > > Checkout sessions expire after 1 hour by default. Updating a checkout session does not extend the time. ## Summary In this step: * Created a signed JWT token using a server-side SDK. * Served the JWT token to the front-end app, where it is used by Embed. # Embed Theming Source: https://integration.wpay.com.au/guides/payments/embed/theming Embed can be customized to match your brand using the `theme` property. The theme property allows you to set common styles such as the font, color, and border styles. ## Overview Embed is a composition of UI components such as a *Payment Header*, *Content* and *Lists*. These components can change over time and so direct styling of these is not supported. The following diagram shows an example of how basic theming is applied. **Embed styles are derived from the theme values you provide.** ![Theming options](https://mintcdn.com/wpay/c50rx3S48hHhNNxb/assets/images/embed/theming.png?fit=max\&auto=format\&n=c50rx3S48hHhNNxb\&q=85\&s=9936bfd6d0c83e0ce5be392faf082d93) > [!WARNING] > > When customizing the visual appearance using theming options, please ensure your design meets accessibility standards set by the [European Accessibility Act (EAA)](https://commission.europa.eu/strategy-and-policy/policies/justice-and-fundamental-rights/disability/union-equality-strategy-rights-persons-disabilities-2021-2030/european-accessibility-act_en). Make sure your color choices have enough contrast and your text is easy to read for all users. ## Usage To apply a theme, embed can be passed an Object containing [Theme Options](#options). **The theme options override the default theme**. ```js React theme={"system"} ``` ```js CDN theme={"system"} setup({ gr4vyId: "example", element: ".container", amount: 1299, country='AU' currency: "AUD", token: token, theme: { colors: { primary: "green", text: "black", }, borderWidths: { input: "thick", }, radii: { input: "none", }, } }); /> ``` ## Options ### Fonts Fonts can be customized with a valid CSS property. ```js theme={"system"} { fonts: { /** * Body - Used for all text */ body: "google:Lato, Tahoma, Arial"; } } ``` > [!TIP] > > **Using Google Fonts** > > Fonts prefixed with `google:` are downloaded automatically from > [Google Fonts](https://fonts.google.com/). This allows you to choose the closest > font to your brand. We recommend choosing a font with support for the weights > `400`, `600` and `700`. ### Colors Colors are named semantically, for example `primary` or `danger`. This allows new features or UI elements to be introduced without explicitly requiring you to update your theme. As an example, a 'Default Card' button may be introduced at a later stage that would be based on the `danger` set of colors. ```js theme={"system"} { colors: { /** * Text - Main body text, labels and headers */ text: "#0f0f0f", /** * Subtle Text - Hints and Smaller text, for example Card Expiry */ subtleText: "#434343", /** * Label Text - Labels for inputs can be a different color */ labelText: "#0f0f0f", /** * Primary - Interactive elements or Brand */ primary: "#006cd0", /** * Page - The main body/page of the Embed iFrame */ pageBackground: "#ffffff", /** * Container - Used for bounding elements, for example an individual payment option */ containerBackgroundUnchecked: "#fcfcfc", containerBackgroundHover: "#eeeeee", containerBackground: "#ffffff", containerBorder: "#f2f2f2", /** * Input - Used for inputs */ inputBorder: "#b6b6b6", inputBackground: "#ffffff", inputText: "#0f0f0f", inputRadioBorder: "#ddd", // defaults to containerBorder inputRadioBorderChecked: "#ddd", // default to primary /** * Danger - Used for Error states or Destructive Action */ danger: "#d82121", dangerBackground: "#fbf7f7", dangerText: "#b81010" /** * Info - Used for information / loading states */ info: "#3ea2ff", infoBackground: "#e7f2fb", infoText: "#0367c4", /** * Focus - Color of the focus ring if shadows.focus is not set. */ focus: "#4844ff", /** * Badge - Color of badge background. */ badgeBackground: "transparent", } } ``` ### Borders Widths Borders can be set using an alias of `thin` for a `1px` border width or `thick` for a `2px` border width. ```js theme={"system"} { borderWidths: { /** * Container - A bounding layout element */ container: 'thin', // 'thin', 'thick' or 'none' /** * Input - A Form Input, for example Card Number */ input: 'thin' // 'thin' or 'thick' } } ``` ### Radii Radii can be set to either `none`, `subtle` or `rounded`. These aliases correspond to `0`, `2px` and `4px`. Setting the radii to `none` removes it entirely. ```js theme={"system"} { radii: { /** * Container - A bounding layout element */ container: "subtle"; // 'subtle', 'rounded' or 'none' /** * Input - A Form Input, for example Card Number */ input: "subtle"; // 'subtle', 'rounded' or 'none' } } ``` ### Focus Ring The focus ring is set using a CSS `box-shadow` instead of the default browser focus ring. It can be overridden, but keeping a visible focus ring for accessibility is advised. This overrides any focus styles set elsewhere in the theme. ```js theme={"system"} { shadows: { /** * Focus Ring - Surrounds an element that is currently focused */ focusRing: "0 0 0 2px #ffffff, 0 0 0 4px #4844ff"; } } ``` ## FAQ > **How do I load my own CSS files?** > > Adding your own styles directly via CSS is currently not supported. Using a > theme allows updates to be rolled out and ensures compatibility with new > features. > **What browsers are supported?** > > Theming is currently supported in all modern browsers including Microsoft > Edge. Some browsers like Internet Explorer 11 are not supported, and in these > the default theme is used. > **What values can be set?** > > Values can be a valid CSS property value. for example `colors.primary` could be a > hex, RGB, or HSL. You can also have multiple shadows for a focus ring, for example `0 > 0 0 2px white, 0 0 0 4px green` # Apple Pay on iOS Source: https://integration.wpay.com.au/guides/payments/ios/apple-pay On mobile apps, Apple Pay can be easily integrated both with and without the SDK. In both offerings, sign up for an Apple Developer account and create an Apple merchant ID and processing certificate. | | [Using SDK][mobile] | [Without SDK][mobile-without] | | - | - | - | | Apple Developer account required? | Yes | Yes | | Apple merchant account required? | Yes | Yes | | Customization & Theming? | Limited | Full | | Domain association required? | No | No | | Integrated with Checkout Flow? | Yes | Optional | > [!NOTE] > > \[Apple Pay]\[apple-pay] is also available on the web. [mobile]: /guides/features/apple-pay/ios [mobile-without]: /guides/features/apple-pay/ios-without-sdk [apple-pay]: /guides/features/apple-pay/overview # iOS events Source: https://integration.wpay.com.au/guides/payments/ios/events The following events can be raised by the iOS SDK. `transactionCreated` Returns data about the transaction object when the transaction was successfully created. ```json theme={"system"} { "transactionID": "8724fd24-5489-4a5d-90fd-0604df7d3b83", "status": "pending", "paymentMethodID": "17d57b9a-408d-49b8-9a97-9db382593003" } ``` `transactionFailed` Returned when the transaction encounters an error. ```json theme={"system"} { "transactionID": "8724fd24-5489-4a5d-90fd-0604df7d3b83", "status": "pending", "paymentMethodID": "17d57b9a-408d-49b8-9a97-9db382593003" } ``` When a network error occurs, the payload includes a `responseCode`. ```json theme={"system"} { "transactionID": "", "status": "", "paymentMethodID": null, "responseCode": "ETIMEDOUT", } ``` Here's a list of potential values for `responseCode`: | Code | Definition | | - | - | | `ERR_BAD_OPTION_VALUE` | Invalid or unsupported value provided in axios configuration. | | `ERR_BAD_OPTION` | Invalid option provided in axios configuration. | | `ECONNABORTED` | Request timed out due to exceeding timeout specified in axios configuration. | | `ETIMEDOUT` | Request timed out due to exceeding default axios time limit. | | `ERR_NETWORK` | Network-related issue. | | `ERR_FR_TOO_MANY_REDIRECTS` | Request is redirected too many times; exceeds max redirects specified in axios configuration. | | `ERR_DEPRECATED` | Deprecated feature or method used in axios. | | `ERR_BAD_RESPONSE` | Response cannot be parsed properly or is in an unexpected format. | | `ERR_BAD_REQUEST` | Requested has unexpected format or missing required parameters. | | `ERR_CANCELED` | Feature or method is canceled explicitly by the user. | | `ERR_NOT_SUPPORT` | Feature or method not supported in the current axios environment. | | `ERR_INVALID_URL` | Invalid URL provided for axios request. | `cancelled` Returned when the user closes the payment sheet without creating a transaction. `generalError` Returned when the SDK encounters an error. ```json theme={"system"} { "Gr4vy Error: Failed to load" } ``` `cardDetailsChanged` Returned when the card BIN changes in the form. It contains information on the inputted card, such as the BIN, card type and scheme. ```json theme={"system"} { "bin": "42424242", "scheme": "visa", "cardType": "debit" } ``` # iOS options Source: https://integration.wpay.com.au/guides/payments/ios/options The SDK can be configured in a few ways. | Field | Optional / Required | Description | | - | - | - | | `gr4vyId` | **`Required`** | This ID automatically sets the `apiHost` to `api..gr4vy.app` for production and `api.sandbox..gr4vy.app` and to `embed.sandbox..gr4vy.app` for the sandbox environment. | | `token` | **`Required`** | The server-side generated JWT token which is used to authenticate any of the API calls. | | `environment` | `Optional` | `.sandbox`, `.production`. Defaults to `.production`. When `.sandbox` is provided the URL contains `sandbox.GR4VY_ID`. | | `amount` | **`Required`** | The amount to authorize or capture in the specified `currency` only. | | `currency` | **`Required`** | A valid, active, 3-character `ISO 4217` currency code to authorize or capture the `amount` for. | | `country` | **`Required`** | A valid `ISO 3166` country code. | | `buyerId` | `Optional` | An optional ID for a buyer. The transaction is automatically associated with a buyer with that ID. If no buyer with this ID exists then it is ignored. | | `buyerExternalIdentifier` | `Optional` | An optional external ID for a buyer. The transaction is automatically associated with a buyer with that external ID. If no buyer with this external ID exists then it is ignored. This option is ignored if the `buyerId` is provided. | | `presentingViewController` | **`Required`** | The view controller that opens the payment sheet. | | `externalIdentifier` | `Optional` | An optional external identifier that can be supplied. This is automatically associated with any resource that opens the payment sheet and subsequently used to find a resource by that ID. | | `store` | `Optional` | `'ask'`, `'preselect'`, `true`, `false` - Explicitly store the payment method, ask the buyer or preselect it by default. Requires `buyerId` or `buyerExternalIdentifier`. | | `display` | `Optional` | `all`, `addOnly`, `storedOnly`, `supportsTokenization` - Filters the payment methods to show stored methods only, new payment methods only or methods that support tokenization. | | `intent` | `Optional` | `authorize`, `preferAuthorize`, `capture` - Defines the intent of this API call. This determines the desired initial state of the transaction. When used, `preferAuthorize` automatically switches to `capture` if the selected payment method doesn't support delayed capture. | | `metadata` | `Optional` | An optional dictionary of keys and values for transaction metadata. All values should be a string. | | `paymentSource` | `Optional` | `installment`, `recurring` - Can be used to signal that Embed is used to capture the first transaction for a subscription or an installment. When used, `store` is implied to be `true` and `display` is implied to be `supportsTokenization`. This means that payment options that do not support tokenization are automatically hidden. | | `cartItems` | `Optional` | An optional array of cart item objects, each object must define a `name`, `quantity`, and `unitAmount`. | | `applePayMerchantId` | `Optional` | The Apple merchant ID to be used during Apple Pay transactions. | | `debugMode` | `Optional` | `true`, `false`. Defaults to `false`, this prints to the console. | | `theme` | `Optional` | Theme customization options. See [Theming](https://github.com/gr4vy/gr4vy-embed/tree/main/packages/embed#theming). The iOS SDK also contains an additional two properties within the `colors` object; `headerBackground` and `headerText`. These are used for the navigation background and foreground colors. | | `locale` | `Optional` | An optional locale, consists of an `ISO 639 Language Code` followed by an optional `ISO 3166 Country Code`, for example `en`, `en-gb` or `pt-br`. | | `statementDescriptor` | `Optional` | An optional object with information about the purchase to construct the statement information the buyer sees in their bank statement. Please note support for these fields varies across payment service providers and underlying banks, so the system can only ensure a best-effort approach for each supported platform. As an example, most platforms only support a concatenation of the `name` and `description` fields, this is truncated to a length of 22 characters within embed. The object can contain `name`, `description`, `phoneNumber`, `city`, and `url` properties, with string values. `phoneNumber` should be in E164 format. Avoiding characters outside the alphanumeric range and the dot (`.`) to ensure wide compatibility is recommended. | | `requireSecurityCode` | `Optional` | An optional boolean that forces security code to be prompted for stored card payments. | | `shippingDetailsId` | `Optional` | An optional unique identifier of a set of shipping details stored for the buyer. | | `merchantAccountId` | `Optional` | An optional merchant account ID. | | `installmentCount` | `Optional` | An optional value that indicates the number of installments a buyer is required to make. | | `excludedMethods` | `Optional` | An optional list of payment methods to exclude. | | `approvalExpiresAt` | `Optional` | An optional `datetime` string (ISO 8601 format recommended) that sets the expiration date for transaction approval. If set to `null`, it is treated the same as if it were omitted. | | `onEvent` | `Optional` | **Please see below for more details.** | # Installing the iOS SDK Source: https://integration.wpay.com.au/guides/payments/ios/quick-start/ios Next, you can embed the SDK in your iOS app to store card details, authorize payments, and capture a transaction. ## Add the SDK The [iOS SDK](https://github.com/gr4vy/gr4vy-ios) is available as a CocoaPods pod and can be set up as follows. ```ruby Podfile theme={"system"} use_frameworks! target 'TARGET_NAME' do pod 'gr4vy-ios', '1.3.0' end ``` > [!NOTE] > > Replace `TARGET_NAME` with your own build target. Please check [GitHub](https://github.com/gr4vy/gr4vy-ios) for > the latest version of the SDK. Then, in the `Podfile` directory type the following command. ```sh theme={"system"} pod install ``` On M1 Macs please add `arch -x86_64` to the command. ```sh theme={"system"} arch -x86_64 pod install ``` # Creating an API key for use with iOS Source: https://integration.wpay.com.au/guides/payments/ios/quick-start/key Before the app can communicate with the API, create a new API key. The API handles authentication using a signed **JSON Web Token** (JWT) which is passed in a HTTP header. ```bash theme={"system"} curl -X GET https://api.example.gr4vy.app/transactions \ -H "authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIi..." ``` > [!TIP] > > Using an SDK is recommended. SDKs make the process of generating a token a lot easier and are probably > available in the preferred programming language. This is covered in the > next step. ## Create a new API key To use the API, generate a new **API key**. Head over to the dashboard and visit the **Integrations** page. ![API key dashboard](https://mintcdn.com/wpay/c50rx3S48hHhNNxb/assets/images/api-keys/add-key.png?fit=max\&auto=format\&n=c50rx3S48hHhNNxb\&q=85\&s=ffbfb085954c55e845050e44a7c9e73e) On this page, click the **Add API key** button select a name for the key. The name is purely to track what key is for what integration. The downloaded key needs to be stored securely as it is not stored by the system. > [!NOTE] > > Learn more about private keys, key-pairs, and JWT in the > [in-depth guide on authentication](/guides/api/authentication). ## Summary In this step: * Learned about API authentication. * Created a new private key for the API. * Stored this API key in a secure place where the app can access it. # iOS SDK Source: https://integration.wpay.com.au/guides/payments/ios/quick-start/overview Get set up with the iOS SDK for your checkout experience. > [!NOTE] > > **New Swift SDK Available** > > It is recommended to use the new [Swift SDK](/guides/payments/native/overview) for native iOS development. It provides native 3DS support, improved performance, and a modern API. This iOS SDK (Embed-based) is still supported and maintained for web view implementations. > > With the Swift SDK, your app collects the card number in its own fields, which puts your app in scope for the Payment Card Industry Data Security Standard (PCI DSS). See [Self-certification and scope](/guides/payments/native/overview#self-certification-and-scope) before you choose it. To keep card data out of your app, use this Embed-based SDK. ![Embed](https://mintcdn.com/wpay/c50rx3S48hHhNNxb/assets/images/ios/hero.png?fit=max\&auto=format\&n=c50rx3S48hHhNNxb\&q=85\&s=0513fee3acf492a3d2a1e9af5164856f) In this quick start guide, you are taken through the following steps. 1. [Create an API key](/guides/payments/ios/quick-start/key) which is needed to generate the signed JWT token in step 2. 2. [Set up a server-side SDK](/guides/payments/ios/quick-start/sdks) to generate a client token for authenticating to the API. 3. [Install the iOS SDK](/guides/payments/ios/quick-start/ios) which handles the capture of the payment method details and process the payment. 4. [Process the transaction](/guides/payments/ios/quick-start/transact) and inspect the result. \` # Generating tokens for authentication Source: https://integration.wpay.com.au/guides/payments/ios/quick-start/sdks Before adding the SDK to the iOS app, create a client token that can be used to authenticate the transaction. ## Install a server-side SDK Use the package manager in the preferred programming language to install the server-side SDK. Token generation can only be done server side and doing this client side is not recommended as it exposes the API key to customers. ```sh C# theme={"system"} dotnet add package Gr4vy ``` ```sh Go theme={"system"} go get github.com/gr4vy/gr4vy-go ``` ```sh Java theme={"system"} # Please check for the latest version implementation 'com.gr4vy:sdk:1.0.0' ``` ```sh PHP theme={"system"} composer require "gr4vy/gr4vy-php" ``` ```sh Python theme={"system"} pip install gr4vy ``` ```sh TypeScript theme={"system"} npm install @gr4vy/sdk # or: yarn add @gr4vy/sdk ``` > [!NOTE] > > Please always check and install the [latest release](/guides/get-started#server-side-sdks) of the preferred SDK. ## Initialize the SDK client Next, initialize the SDK with the ID of the instance and the private key. ```csharp C# theme={"system"} using Gr4vy; using Gr4vy.Models.Components; using System.Collections.Generic; // Loaded the key from a file, env variable, // or anywhere else var privateKey = "..."; var sdk = new Gr4vySDK( id: "example", server: SDKConfig.Server.Sandbox, bearerAuthSource: Auth.WithToken(privateKey), merchantAccountId: "default" ); ``` ```go Go theme={"system"} package main import ( "context" gr4vy "github.com/gr4vy/gr4vy-go" "github.com/gr4vy/gr4vy-go/models/operations" "log" "os" ) func main() { ctx := context.Background() privateKey := "...." // Private key loaded from disk or env var withToken := gr4vy.WithToken(privateKey, []JWTScope{ReadAll, WriteAll}, 60) s := gr4vy.New( gr4vy.WithID("example"), gr4vy.WithServer(gr4vy.ServerSandbox), gr4vy.WithSecuritySource(withToken), gr4vy.WithMerchantAccountID("default"), ) } ``` ```java Java theme={"system"} package hello.world; import com.gr4vy.sdk.BearerSecuritySource; import com.gr4vy.sdk.Gr4vy; import com.gr4vy.sdk.Gr4vy.AvailableServers; import com.gr4vy.sdk.models.components.AccountUpdaterJobCreate; import com.gr4vy.sdk.models.errors.*; import com.gr4vy.sdk.models.operations.ListTransactionsRequest; import java.lang.Exception; import java.util.List; public class Application { public static void main(String[] args) throws Exception { String privateKey = "-----BEGIN PRIVATE KEY-----\n...."; // a valid private key Gr4vy sdk = Gr4vy.builder() .id("example") .server(AvailableServers.SANDBOX) .merchantAccountId("default") .securitySource(new BearerSecuritySource.Builder(privateKey).build()) .build(); } } ``` ```php PHP theme={"system"} declare(strict_types=1); require 'vendor/autoload.php'; use Gr4vy; use Gr4vy\Auth; // Loaded the key from a file, env variable, // or anywhere else $privateKey = "..."; $sdk = Gr4vy\SDK::builder() ->setId('example') ->setServer('sandbox') ->setSecuritySource(Auth::withToken($privateKey)) ->setMerchantAccountId('default') ->build(); ``` ```python Python theme={"system"} from gr4vy import Gr4vy, auth import os client = Gr4vy( id="example", server="production", merchant_account_id="default", bearer_auth=auth.with_token(open("./private_key.pem").read()) ) ``` ```ts TypeScript theme={"system"} import fs from "fs"; import { Gr4vy, withToken } from "@gr4vy/sdk"; const gr4vy = new Gr4vy({ server: "sandbox", id: "example", bearerAuth: withToken({ privateKey: fs.readFileSync("private_key.pem", "utf8"), }), }); ``` > [!NOTE] > > The instance ID is the unique identifier for the deployment of the system and is included in every API call. > Together with the environment (sandbox or production) it is used to connect to the right APIs, as well as dashboard. > > The possible values for your ID are one of `wpay`, `wpay1`, `wpay2`, or `wpay3`. You are informed > in which instance your account has been set up. ## Generate a token The next step is to create a new token for frontend authentication. The SDK comes with a helper function for exactly that purpose. It requires an amount and currency for the transaction to be created and additionally accepts either the ID of a buyer or an external identifier for a buyer to link the transaction to. For Embed, it is recommended to attach a checkout session to every transaction. The `getEmbedTokenWithCheckoutSession` helper creates a checkout session using your SDK client and returns an Embed token with the resulting `checkout_session_id` already pinned, in a single call. ```csharp C# theme={"system"} using System.Collections.Generic; using Gr4vy; // Loaded the key from a file, env variable, // or anywhere else var privateKey = "..."; var sdk = new Gr4vySDK( id: "example", server: SDKConfig.Server.Sandbox, bearerAuthSource: Auth.WithToken(privateKey), merchantAccountId: "default" ); var token = await Auth.GetEmbedTokenWithCheckoutSession( client: sdk, privateKey: privateKey, embedParams: new Dictionary { ["amount"] = 1299, ["currency"] = "AUD", ["buyer_external_identifier"] = "user-1234", } ); ``` ```go Go theme={"system"} package main import ( "context" gr4vy "github.com/gr4vy/gr4vy-go" "log" ) func main() { privateKey := "...." // Private key loaded from disk or env var client := gr4vy.New( gr4vy.WithID("example"), gr4vy.WithServer(gr4vy.ServerSandbox), gr4vy.WithSecuritySource(gr4vy.WithToken(privateKey, nil, 3600)), gr4vy.WithMerchantAccountID("default"), ) embedParams := map[string]interface{}{ "amount": 1299, "currency": "AUD", "buyer_external_identifier": "user-1234", } token, err := gr4vy.GetEmbedTokenWithCheckoutSession(context.Background(), client, privateKey, embedParams, nil, nil) if err != nil { log.Fatal(err) } log.Println(token) } ``` ```java Java theme={"system"} package hello.world; import com.gr4vy.sdk.Auth; import com.gr4vy.sdk.BearerSecuritySource; import com.gr4vy.sdk.Gr4vy; import com.gr4vy.sdk.Gr4vy.AvailableServers; import java.util.HashMap; import java.util.Map; public class Application { public static void main(String[] args) throws Exception { String privateKey = "-----BEGIN PRIVATE KEY-----\n...."; // a valid private key Gr4vy client = Gr4vy.builder() .id("example") .server(AvailableServers.SANDBOX) .securitySource(new BearerSecuritySource.Builder(privateKey).build()) .merchantAccountId("default") .build(); Map embedParams = new HashMap<>(); embedParams.put("amount", 1299); embedParams.put("currency", "AUD"); embedParams.put("buyer_external_identifier", "user-1234"); String token = Auth.getEmbedTokenWithCheckoutSession(client, privateKey, embedParams); System.out.println(token); } } ``` ```php PHP theme={"system"} declare(strict_types=1); require 'vendor/autoload.php'; use Gr4vy; use Gr4vy\Auth; // Loaded the key from a file, env variable, // or anywhere else $privateKey = "..."; $sdk = Gr4vy\SDK::builder() ->setId('example') ->setServer('sandbox') ->setSecuritySource(Auth::withToken($privateKey)) ->setMerchantAccountId('default') ->build(); $token = Auth::getEmbedTokenWithCheckoutSession( client: $sdk, privateKey: $privateKey, embedParams: [ "amount" => 1299, "currency" => "AUD", "buyer_external_identifier" => "user-1234", ], ); ``` ```python Python theme={"system"} from gr4vy import Gr4vy, auth private_key = open("./private_key.pem").read() g_client = Gr4vy( id="example", server="production", merchant_account_id="default", bearer_auth=auth.with_token(private_key) ) token = auth.get_embed_token_with_checkout_session( g_client, private_key, embed_params={ "amount": 1299, "currency": "AUD", "buyer_external_identifier": "user-1234", }, ) ``` ```ts TypeScript theme={"system"} import { Gr4vy, getEmbedTokenWithCheckoutSession, withToken } from "@gr4vy/sdk"; import fs from "fs"; async function run() { const privateKey = fs.readFileSync("private_key.pem", "utf8") const gr4vy = new Gr4vy({ server: "sandbox", id: "example", bearerAuth: withToken({ privateKey }), }); const token = await getEmbedTokenWithCheckoutSession({ client: gr4vy, privateKey, embedParams: { amount: 1299, currency: 'AUD', buyerExternalIdentifier: 'user-1234', } }); console.log(token); } run(); ``` This token can now be served to the frontend where it is used by the SDK. If generating the client token without using a server-side SDK is preferred, then please read the guide on [manually generating a JWT](/guides/api/authentication#without-sdks). ## Checkout sessions Passing a [checkout session](/reference/checkout-sessions/new-checkout-session) ID to the Embed token function is recommended. This ID can be used to help identify multiple transaction attempts. This is useful to identify if one or more failed/declined transactions eventually result in a successful transaction. Checkout sessions can also store `cart_items`, `metadata`, airline data, and more, removing the need to pass these values on the transaction request. ```csharp C# theme={"system"} using Gr4vy; using Gr4vy.Models.Components; using NodaTime; using System; using System.Collections.Generic; var res = await sdk.CheckoutSessions.CreateAsync(checkoutSessionCreate: new CheckoutSessionCreate() { CartItems = new List() { new CartItem() { Name = "GoPro HD", Quantity = 2, UnitAmount = 1299, DiscountAmount = 0, TaxAmount = 0, ExternalIdentifier = "goprohd", Sku = "GPHD1078", ProductUrl = "https://example.com/catalog/go-pro-hd", ImageUrl = "https://example.com/images/go-pro-hd.jpg", Categories = new List() { "camera", "travel", "gear", }, ProductType = "physical", SellerCountry = "GB", }, new CartItem() { Name = "GoPro HD", Quantity = 2, UnitAmount = 1299, DiscountAmount = 0, TaxAmount = 0, ExternalIdentifier = "goprohd", Sku = "GPHD1078", ProductUrl = "https://example.com/catalog/go-pro-hd", ImageUrl = "https://example.com/images/go-pro-hd.jpg", Categories = new List() { "camera", "travel", "gear", }, ProductType = "physical", SellerCountry = "GB", }, new CartItem() { Name = "GoPro HD", Quantity = 2, UnitAmount = 1299, DiscountAmount = 0, TaxAmount = 0, ExternalIdentifier = "goprohd", Sku = "GPHD1078", ProductUrl = "https://example.com/catalog/go-pro-hd", ImageUrl = "https://example.com/images/go-pro-hd.jpg", Categories = new List() { "camera", "travel", "gear", }, ProductType = "physical", SellerCountry = "AU", }, }, Metadata = new Dictionary() { { "cohort", "cohort-a" }, { "order_id", "order-12345" }, }, Buyer = new GuestBuyer() { DisplayName = "John Doe", ExternalIdentifier = "buyer-12345", BillingDetails = new BillingDetails() { FirstName = "John", LastName = "Doe", EmailAddress = "john@example.com", PhoneNumber = "+1234567890", Address = new Address() { City = "San Jose", Country = "AU", PostalCode = "94560", State = "California", StateCode = "US-CA", HouseNumberOrName = "10", Line1 = "Stafford Apartments", Line2 = "29th Street", Organization = "Gr4vy", }, TaxId = new TaxId() { Value = "12345678931", Kind = "ar.cuit", }, }, ShippingDetails = new ShippingDetailsCreate() { FirstName = "John", LastName = "Doe", EmailAddress = "john@example.com", PhoneNumber = "+1234567890", Address = new Address() { City = "San Jose", Country = "AU", PostalCode = "94560", State = "California", StateCode = "US-CA", HouseNumberOrName = "10", Line1 = "Stafford Apartments", Line2 = "29th Street", Organization = "Gr4vy", }, }, }, }); // handle response ``` ```go Go theme={"system"} package main import( "context" gr4vygo "github.com/gr4vy/gr4vy-go" "os" "github.com/gr4vy/gr4vy-go/models/components" "github.com/gr4vy/gr4vy-go/types" "log" ) func main() { ctx := context.Background() // Init SDK res, err := s.CheckoutSessions.Create(ctx, &components.CheckoutSessionCreate{ CartItems: []components.CartItem{ components.CartItem{ Name: "GoPro HD", Quantity: 2, UnitAmount: 1299, DiscountAmount: gr4vygo.Int64(0), TaxAmount: gr4vygo.Int64(0), ExternalIdentifier: gr4vygo.String("goprohd"), Sku: gr4vygo.String("GPHD1078"), ProductURL: gr4vygo.String("https://example.com/catalog/go-pro-hd"), ImageURL: gr4vygo.String("https://example.com/images/go-pro-hd.jpg"), Categories: []string{ "camera", "travel", "gear", }, ProductType: components.ProductTypePhysical.ToPointer(), SellerCountry: gr4vygo.String("AU"), }, components.CartItem{ Name: "GoPro HD", Quantity: 2, UnitAmount: 1299, DiscountAmount: gr4vygo.Int64(0), TaxAmount: gr4vygo.Int64(0), ExternalIdentifier: gr4vygo.String("goprohd"), Sku: gr4vygo.String("GPHD1078"), ProductURL: gr4vygo.String("https://example.com/catalog/go-pro-hd"), ImageURL: gr4vygo.String("https://example.com/images/go-pro-hd.jpg"), Categories: []string{ "camera", "travel", "gear", }, ProductType: components.ProductTypePhysical.ToPointer(), SellerCountry: gr4vygo.String("AU"), }, }, Metadata: map[string]string{ "cohort": "cohort-a", "order_id": "order-12345", }, Buyer: nil, }) if err != nil { log.Fatal(err) } if res != nil { // handle response } } ``` ```java Java theme={"system"} package hello.world; import com.gr4vy.sdk.Gr4vy; import com.gr4vy.sdk.models.components.*; import com.gr4vy.sdk.models.errors.*; import com.gr4vy.sdk.models.operations.CreateCheckoutSessionResponse; import java.lang.Exception; import java.time.LocalDate; import java.time.OffsetDateTime; import java.util.List; import java.util.Map; import org.openapitools.jackson.nullable.JsonNullable; public class Application { public static void main(String[] args) throws Exception { // Init SDK CreateCheckoutSessionResponse res = sdk.checkoutSessions().create() .checkoutSessionCreate(CheckoutSessionCreate.builder() .cartItems(List.of( CartItem.builder() .name("GoPro HD") .quantity(2L) .unitAmount(1299L) .discountAmount(0L) .taxAmount(0L) .externalIdentifier("goprohd") .sku("GPHD1078") .productUrl("https://example.com/catalog/go-pro-hd") .imageUrl("https://example.com/images/go-pro-hd.jpg") .categories(List.of( "camera", "travel", "gear")) .productType(ProductType.PHYSICAL) .sellerCountry("AU") .build(), CartItem.builder() .name("GoPro HD") .quantity(2L) .unitAmount(1299L) .discountAmount(0L) .taxAmount(0L) .externalIdentifier("goprohd") .sku("GPHD1078") .productUrl("https://example.com/catalog/go-pro-hd") .imageUrl("https://example.com/images/go-pro-hd.jpg") .categories(List.of( "camera", "travel", "gear")) .productType(ProductType.PHYSICAL) .sellerCountry("AU") .build())) .metadata(Map.ofEntries( Map.entry("cohort", "cohort-a"), Map.entry("order_id", "order-12345"))) .buyer(JsonNullable.of(null)) .build()) .call(); if (res.checkoutSession().isPresent()) { // handle response } } } ``` ```php PHP theme={"system"} declare(strict_types=1); require 'vendor/autoload.php'; use Brick\DateTime\LocalDate; use Gr4vy; use Gr4vy\Utils; // Init SDK $checkoutSessionCreate = new Gr4vy\CheckoutSessionCreate( cartItems: [ new Gr4vy\CartItem( name: 'GoPro HD', quantity: 2, unitAmount: 1299, discountAmount: 0, taxAmount: 0, externalIdentifier: 'goprohd', sku: 'GPHD1078', productUrl: 'https://example.com/catalog/go-pro-hd', imageUrl: 'https://example.com/images/go-pro-hd.jpg', categories: [ 'camera', 'travel', 'gear', ], productType: 'physical', sellerCountry: 'AU', ), new Gr4vy\CartItem( name: 'GoPro HD', quantity: 2, unitAmount: 1299, discountAmount: 0, taxAmount: 0, externalIdentifier: 'goprohd', sku: 'GPHD1078', productUrl: 'https://example.com/catalog/go-pro-hd', imageUrl: 'https://example.com/images/go-pro-hd.jpg', categories: [ 'camera', 'travel', 'gear', ], productType: 'physical', sellerCountry: 'AU', ), ], metadata: [ 'cohort' => 'cohort-a', 'order_id' => 'order-12345', ], buyer: new Gr4vy\GuestBuyer( displayName: 'John Doe', externalIdentifier: 'buyer-12345', billingDetails: new Gr4vy\BillingDetails( firstName: 'John', lastName: 'Doe', emailAddress: 'john@example.com', phoneNumber: '+1234567890', address: new Gr4vy\Address( city: 'San Jose', country: 'AU', postalCode: '94560', state: 'California', stateCode: 'US-CA', houseNumberOrName: '10', line1: 'Stafford Apartments', line2: '29th Street', organization: 'Gr4vy', ), taxId: new Gr4vy\TaxId( value: '12345678931', kind: 'my.frp', ), ), shippingDetails: new Gr4vy\ShippingDetailsCreate( firstName: 'John', lastName: 'Doe', emailAddress: 'john@example.com', phoneNumber: '+1234567890', address: null, ), ), ); $response = $sdk->checkoutSessions->create( checkoutSessionCreate: $checkoutSessionCreate ); if ($response->checkoutSession !== null) { // handle response } ``` ```python Python theme={"system"} from datetime import date from gr4vy import Gr4vy, models from gr4vy.utils import parse_datetime import os # Init SDK res = g_client.checkout_sessions.create(checkout_session_create=models.CheckoutSessionCreate( cart_items=[ models.CartItem( name="GoPro HD", quantity=2, unit_amount=1299, discount_amount=0, tax_amount=0, external_identifier="goprohd", sku="GPHD1078", product_url="https://example.com/catalog/go-pro-hd", image_url="https://example.com/images/go-pro-hd.jpg", categories=[ "camera", "travel", "gear", ], product_type="physical", seller_country="GB", ), models.CartItem( name="GoPro HD", quantity=2, unit_amount=1299, discount_amount=0, tax_amount=0, external_identifier="goprohd", sku="GPHD1078", product_url="https://example.com/catalog/go-pro-hd", image_url="https://example.com/images/go-pro-hd.jpg", categories=[ "camera", "travel", "gear", ], product_type="physical", seller_country="GB", ), models.CartItem( name="GoPro HD", quantity=2, unit_amount=1299, discount_amount=0, tax_amount=0, external_identifier="goprohd", sku="GPHD1078", product_url="https://example.com/catalog/go-pro-hd", image_url="https://example.com/images/go-pro-hd.jpg", categories=[ "camera", "travel", "gear", ], product_type="physical", seller_country="GB", ), ], metadata={ "cohort": "cohort-a", "order_id": "order-12345", }, buyer=models.GuestBuyer( display_name="John Doe", external_identifier="buyer-12345", billing_details=models.BillingDetails( first_name="John", last_name="Doe", email_address="john@example.com", phone_number="+1234567890", address=models.Address( city="San Jose", country="AU", postal_code="94560", state="California", state_code="US-CA", house_number_or_name="10", line1="Stafford Apartments", line2="29th Street", organization="Gr4vy", ), tax_id=models.TaxID( value="12345678931", kind="ar.cuit", ), ), shipping_details=models.ShippingDetailsCreate( first_name="John", last_name="Doe", email_address="john@example.com", phone_number="+1234567890", address=models.Address( city="San Jose", country="AU", postal_code="94560", state="California", state_code="US-CA", house_number_or_name="10", line1="Stafford Apartments", line2="29th Street", organization="Gr4vy", ), ), ), )) # Handle response print(res) ``` ```ts Typescript theme={"system"} const result = await gr4vy.checkoutSessions.create(); ``` > [!WARNING] > > **Checkout session expiry** > > Checkout sessions expire after 1 hour by default. Updating a checkout session does not extend the time. ## Loading token The token needs to be shared with the iOS app via a custom API. Creating a token per checkout and only using a token for a short amount of time is recommended. ## Summary In this step: * Created a signed JWT token using a server-side SDK. * Shared the JWT with the iOS app # Creating transactions Source: https://integration.wpay.com.au/guides/payments/ios/quick-start/transact With the SDK ready, it's time to create a transaction. ## Initialize the SDK To initialize the SDK, call the `.init()` method on the SDK. ```swift theme={"system"} import gr4vy_ios let result = Gr4vy.init( gr4vyId: "[GR4VY_ID]", token: "[TOKEN]", amount: 1299, currency: "AUD", country: "AU", buyerId: nil, environment: Gr4vyEnvironment.sandbox ) ``` > [!NOTE] > > Replace the `[GR4VY_ID]` with your instance identifier and `[TOKEN]` with the JWT created in step 2. > Additional [options](/guides/payments/ios/options) can be provided when launching the SDK. > [!NOTE] > > The instance ID is the unique identifier for the deployment of the system and is included in every API call. > Together with the environment (sandbox or production) it is used to connect to the right APIs, as well as dashboard. > > The possible values for your ID are one of `wpay`, `wpay1`, `wpay2`, or `wpay3`. You are informed > in which instance your account has been set up. ## Launch & handle events Next, it's time to open the payment sheet with the `.launch()` method. The launch method requires attaching an event handler that listens for transactions that have been created or failed, as well as more generic errors. ```swift theme={"system"} result?.launch(presentingViewController: self, onEvent: { event in switch event { case .transactionFailed(let transactionID, let status, let paymentMethodID, let responseCode): print("Handle a transaction failure, ID: \(transactionID), Status: \(status), PaymentMethodID: \(paymentMethodID ?? "Unknown"), ResponseCode: \(responseCode ?? "Unknown")") case .transactionCreated(let transactionID, let status, let paymentMethodID): print("Handle successful transaction, ID: \(transactionID), Status: \(status), PaymentMethodID: \(paymentMethodID ?? "Unknown")") case .generalError(let error): print("Error: \(error.description)") } }) ``` When calling `.launch()` the `presentingViewController` is the current view controller this SDK is launched from. The SDK presents the payment sheet on top of the current view controller, passing events back to the presenting view controller via the `onEvent` callback. > [!NOTE] > > Learn more about the events triggered by the iOS SDK in the [events](/guides/payments/ios/events) guide. # Accept payments on mobile Source: https://integration.wpay.com.au/guides/payments/mobile Choose the right mobile integration for your app: Embed SDKs or Native SDKs. Gr4vy offers two approaches for accepting payments in mobile apps. Both support the most common card and wallet payment flows. The right choice depends on how much UI control and native platform depth you need, and on how much PCI DSS scope you can take on. ## Compare approaches | | Embed SDKs | Native SDKs | | - | - | - | | **Best for** | Fast integration with a complete UI | Fully custom card forms, native 3DS | | **Platforms** | iOS, Android, React Native | iOS (Swift), Android (Kotlin) | | **UI** | Pre-built, customizable payment sheet | You build the UI, full control | | **Payment methods** | Cards, wallets, BNPL, local methods | Cards with native SDK flows | | **Wallet support** | Apple Pay and Google Pay via platform integrations | Apple Pay and Google Pay via direct wallet integrations | | **3DS** | Handled automatically via web view | Native 3DS in-app (no web view) | | **PCI scope** | SAQ A | [SAQ D](/guides/payments/native/overview#self-certification-and-scope), because your app handles card data | | **Setup time** | Fastest | More involved | ## Embed SDKs The Embed SDKs provide a drop-in payment sheet for iOS, Android, and React Native. The checkout UI is rendered in a web view, giving you access to all supported payment methods with minimal setup. > **[iOS](/guides/payments/ios/quick-start/overview)** > > Drop-in payment sheet for iOS. > **[Android](/guides/payments/android/quick-start/overview)** > > Drop-in payment sheet for Android. > **[React Native](/guides/payments/react-native/quick-start/overview)** > > Cross-platform payments for iOS and Android. ### Extend your Embed SDK integration > **[Apple Pay (iOS)](/guides/payments/ios/apple-pay)** > > Enable Apple Pay for iOS Embed SDK integrations. > **[Google Pay (Android)](/guides/payments/android/google-pay)** > > Enable Google Pay for Android Embed SDK integrations. > **[Apple Pay (React Native)](/guides/payments/react-native/apple-pay)** > > Configure Apple Pay for React Native checkouts. > **[Google Pay (React Native)](/guides/payments/react-native/google-pay)** > > Configure Google Pay for React Native checkouts. ## Native SDKs The Native SDKs (Swift for iOS, Kotlin for Android) let you build card capture directly into your app's native view hierarchy with no web view required. They support native 3DS authentication, giving you a smooth, fully in-app experience. The SDKs don't include card entry UI, so your app collects the card details and is in scope for PCI DSS. > **[Swift (iOS)](/guides/payments/native/overview)** > > Native card forms and 3DS for iOS with Swift. > **[Kotlin (Android)](/guides/payments/native/overview)** > > Native card forms and 3DS for Android with Kotlin. ### Extend your Native SDK integration > **[Native 3DS](/guides/payments/native/3ds)** > > Add and test native 3DS authentication in your app. > **[List payment options](/guides/payments/native/list-payment-options)** > > Query available payment options for your mobile checkout context. > **[Apple Pay (iOS)](/guides/features/apple-pay/ios-without-sdk)** > > Add Apple Pay to native iOS checkouts outside the Embed SDK. > **[Google Pay (Android)](/guides/features/google-pay/mobile-without-sdk)** > > Add Google Pay to native Android checkouts outside the Embed SDK. ## Not sure which to choose? Start with the **Embed SDKs** if you want the fastest path to a working checkout with full payment method coverage, or if you want to keep card data out of your app. Choose the **Native SDKs** if you need complete UI control, a fully native 3DS experience, or want to avoid web view rendering in your app. # Adding 3DS authentication Source: https://integration.wpay.com.au/guides/payments/native/3ds This guide explains how to add 3DS Authentication to your native mobile integration. By using the native 3DS flow, you handle the "Identify" and "Challenge" phases directly within your app, providing a seamless experience that avoids browser redirects and improves conversion rates. ## Prerequisites This guide explains how to add 3DS authentication to your native mobile integration. Before you begin, ensure you have: 1. The Gr4vy SDK installed in your project (see [native vaulting guide](./vault) for installation steps). 2. Your backend set up to create checkout sessions and process transactions. > [!WARNING] > > Native mobile 3DS requires a merchant account-level 3DS configuration. Follow the [3DS setup guide](/guides/features/3ds/setup#add-a-merchant-account-configuration) to configure it before proceeding with this integration. ## How it works The native 3DS implementation automatically handles all device fingerprinting requirements, including collecting the device's IP address, user agent, location data, and other authentication metadata needed for 3-D Secure compliance. This ensures full compliance with 3-D Secure specifications without requiring manual fingerprint collection on your side. ### Limitations & Considerations When you use native 3DS with these native SDKs, it is important to pass the transaction context (for example, `metadata`, `merchant_initiated`, or `cart_items`) when you create the Checkout Session. Native 3DS runs before the transaction is created, so this context must already be present when you create the session. Gr4vy uses the session's `metadata` to [pick the 3DS configuration](/guides/features/3ds/setup#matching-a-merchant-account-level-configuration), and the full context to evaluate Flow rules. Additionally, it's important to note that the system runs 3DS Flow rules at the time of the enrollment check, which is run at the time of submitting Secure Fields. Currently, this Flow rule check doesn't support anti-fraud checks as these are currently tied to a transactions, though there are plans to add this in the near future. ## Implementation ### 1. Create a Checkout Session First, create a checkout session on your backend with the transaction amount and currency. This session stores the authenticated card data for 3DS processing. ```csharp C# theme={"system"} using Gr4vy; using Gr4vy.Models.Components; var sdk = new Gr4vySDK( merchantAccountId: "default", bearerAuth: "" ); var checkoutSession = await sdk.CheckoutSessions.CreateAsync( checkoutSessionCreate: new CheckoutSessionCreate() { Amount = 1800, Currency = "EUR", } ); // Return the session ID to your mobile app var sessionId = checkoutSession.CheckoutSession.Id; ``` ```go Go theme={"system"} package main import( "context" gr4vygo "github.com/gr4vy/gr4vy-go" "os" "github.com/gr4vy/gr4vy-go/models/components" "log" ) func main() { ctx := context.Background() s := gr4vygo.New( gr4vygo.WithMerchantAccountID("default"), gr4vygo.WithSecurity(os.Getenv("GR4VY_BEARER_AUTH")), ) checkoutSession, err := s.CheckoutSessions.Create(ctx, &components.CheckoutSessionCreate{ Amount: gr4vygo.Int64(1800), Currency: gr4vygo.String("EUR"), }) if err != nil { log.Fatal(err) } if checkoutSession != nil { // Return the session ID to your mobile app log.Printf("Session ID: %s", checkoutSession.CheckoutSession.ID) } } ``` ```java Java theme={"system"} import com.gr4vy.sdk.Gr4vy; import com.gr4vy.sdk.models.components.CheckoutSessionCreate; import com.gr4vy.sdk.models.operations.CreateCheckoutSessionResponse; import java.lang.Exception; public class Application { public static void main(String[] args) throws Exception { Gr4vy sdk = Gr4vy.builder() .merchantAccountId("default") .bearerAuth("") .build(); CreateCheckoutSessionResponse res = sdk.checkoutSessions().create( CheckoutSessionCreate.builder() .amount(1800L) .currency("EUR") .build() ).call(); if (res.checkoutSession().isPresent()) { // Return the session ID to your mobile app String sessionId = res.checkoutSession().get().id(); System.out.println("Session ID: " + sessionId); } } } ``` ```php PHP theme={"system"} declare(strict_types=1); require 'vendor/autoload.php'; use Gr4vy; $sdk = Gr4vy\SDK::builder() ->setMerchantAccountId('default') ->setSecurity('') ->build(); $response = $sdk->checkoutSessions->create( checkoutSessionCreate: new Gr4vy\CheckoutSessionCreate( amount: 1800, currency: 'EUR', country: 'DE' ) ); if ($response->checkoutSession !== null) { // Return the session ID to your mobile app echo "Session ID: " . $response->checkoutSession->id; } ``` ```python Python theme={"system"} from gr4vy import Gr4vy import os with Gr4vy( merchant_account_id="default", bearer_auth=os.getenv("GR4VY_BEARER_AUTH", ""), ) as g_client: # Create a checkout session with amount and currency checkout_session = g_client.checkout_sessions.create( checkout_session_create={ "amount": 1800, "currency": "EUR", } ) # Return the session ID to your mobile app print(checkout_session.id) ``` ```ts TypeScript theme={"system"} import { Gr4vy } from '@gr4vy/sdk' const gr4vy = new Gr4vy({ gr4vyId: 'your-gr4vy-id', privateKey: 'your-private-key', environment: 'sandbox', }) async function createCheckoutSession() { const checkoutSession = await gr4vy.checkoutSessions.create({ amount: 1800, currency: 'EUR', }) // Return the session ID to your mobile app console.log(checkoutSession.id) } createCheckoutSession() ``` Pass this session ID to your mobile app so it can be used in the next step. ### 2. Update SDK logic ```swift Swift (iOS) {16, 22-34, 59, 65-67} theme={"system"} /// Create card data let cardData = Gr4vyCardData( paymentMethod: .card(CardPaymentMethod( number: "4111111111111111", expirationDate: "12/25", securityCode: "123" )) ) // Tokenize with 3DS authentication (async/await) do { let result = try await gr4vy.tokenize( checkoutSessionId: "session_123", cardData: cardData, sdkMaxTimeoutMinutes: 5, authenticate: true ) if result.tokenized { print("Payment method tokenized successfully") // Check authentication details if let auth = result.authentication { print("3DS attempted: \(auth.attempted)") print("Authentication type: \(auth.type ?? "N/A")") print("Transaction status: \(auth.transactionStatus ?? "N/A")") if auth.hasCancelled { print("User cancelled authentication") } if auth.hasTimedOut { print("Authentication timed out") } } } } catch { print("Error tokenizing payment method: \(error)") } // With explicit view controller (async/await) do { let result = try await gr4vy.tokenize( checkoutSessionId: "session_123", cardData: cardData, viewController: self, // Explicitly provide the presenting view controller sdkMaxTimeoutMinutes: 5, authenticate: true ) print("Tokenization complete: \(result.tokenized)") } catch { print("Error: \(error)") } // Completion handler variant gr4vy.tokenize( checkoutSessionId: "session_123", cardData: cardData, sdkMaxTimeoutMinutes: 5, authenticate: true ) { result in switch result { case .success(let tokenizeResult): if tokenizeResult.tokenized { print("Payment method tokenized successfully") if let auth = tokenizeResult.authentication { print("Transaction status: \(auth.transactionStatus ?? "N/A")") } } case .failure(let error): print("Error tokenizing payment method: \(error)") } } ``` ```kotlin Kotlin (Android) {18,24-36, 49, 56-58} theme={"system"} // Create card data val cardData = Gr4vyCheckoutSessionRequest( paymentMethod = Gr4vyPaymentMethod.Card( number = "4111111111111111", expirationDate = "12/25", securityCode = "123" ) ) // Tokenize with 3DS authentication (suspend function) lifecycleScope.launch { try { val result = gr4vy.tokenize( checkoutSessionId = "session_123", cardData = cardData, activity = this@PaymentActivity, sdkMaxTimeoutMinutes = 5, authenticate = true ) if (result.tokenized) { println("Payment method tokenized successfully") // Check authentication details result.authentication?.let { auth -> println("3DS attempted: ${auth.attempted}") println("Authentication type: ${auth.type}") println("Transaction status: ${auth.transactionStatus}") if (auth.hasCancelled) { println("User cancelled authentication") } if (auth.hasTimedOut) { println("Authentication timed out") } } } } catch (error: Gr4vyError) { println("Error tokenizing payment method: $error") } } // Callback version gr4vy.tokenize( checkoutSessionId = "session_123", cardData = cardData, activity = this@PaymentActivity, sdkMaxTimeoutMinutes = 5, authenticate = true ) { result -> when { result.isSuccess -> { val tokenizeResult = result.getOrNull() if (tokenizeResult?.tokenized == true) { println("Payment method tokenized successfully") tokenizeResult.authentication?.let { auth -> println("Transaction status: ${auth.transactionStatus}") } } } result.isFailure -> { println("Error tokenizing payment method: ${result.exceptionOrNull()}") } } } ``` ### Authentication result The native 3DS flow follows the same EMV 3DS specification as the Secure Fields flow, so it returns the same set of values. The `authentication` object on the tokenization result exposes the following properties. See [Authentication results](/guides/features/3ds/statuses) for the full list of `transactionStatus` values. | Property | Type or value | Description | | - | - | - | | `transactionStatus` | `"Y"` | Authentication was successful. | | | `"A"` | Not authenticated, but a proof of authentication attempt was generated. | | | `"N"` | Not authenticated; the transaction was denied. | | | `"R"` | Authentication was rejected by the issuer, which requests that authorization not be attempted. | | | `"U"` | Authentication could not be performed because of a technical or other problem. | | | `null` | No authentication status could be determined, typically only when an error occurred during authentication. | | `type` | `null` | The flow failed because the card used is not enrolled. | | | `"challenge"` | The flow required the user to perform a challenge. | | | `"frictionless"` | The flow was successful without requiring a challenge. | | | `"error"` | The flow encountered an error during the process. | | `attempted` | `Boolean` | Indicates whether authentication was attempted. | | `hasTimedOut` | `Boolean` | Indicates whether the authentication process timed out. | | `hasCancelled` | `Boolean` | Indicates whether the user cancelled the challenge. | ### 3. Backend Authorization Once the SDK returns a successful result, the 3DS verification data is securely stored within the Checkout Session. When your app notifies your backend, create the transaction by referencing the session ID in the `payment_method` object. The system automatically detects the 3DS data and ensures it is passed to your payment processor to satisfy SCA requirements. ```json theme={"system"} POST /transactions { "amount": 1800, "currency": "EUR", "country": "DE", "intent": "capture", "payment_method": { "method": "checkout-session", "id": "[checkout-session-id]" }, ... } ``` ### Handling failed authentication If a customer fails or cancels their 3DS authentication, you have the option to still process the payment using the vaulted card data. This is useful for scenarios where: * The customer wants to retry with a different payment method later * You want to reduce friction by allowing offline card storage * Your business model supports processing payments without 3DS authentication (subject to regional regulations and processor rules) Gr4vy doesn't stop a transaction because of the authentication result. The checkout session only passes 3DS data to the transaction when authentication produced an authentication value, for example with a `transactionStatus` of `"Y"`. After an `"N"` or `"R"`, the transaction is created without 3DS data. Your 3DS settings and Flow rules then decide whether Gr4vy runs 3DS again for that transaction. To avoid authorizing a payment that the issuer rejected, check `transactionStatus` in your app and don't ask your backend to create the transaction when it's `"R"`, or `"N"` if you want the same rule there. To keep this decision in Flow instead, pass the result as transaction `metadata` and add a [Decline rule](/guides/dashboard/flow/card-transactions#action-decline) with a Metadata condition on that value. ## Theming When a 3DS challenge is required, the Gr4vy SDK presents a native interface provided by the card issuer. You can customize the appearance of these screens to ensure they match your app's branding and provide a consistent user experience. Customization is handled via a `uiCustomization` object passed during the SDK initialization. This allows you to define colors, fonts, and button styles that are applied to the 3DS challenge modals. * **Text:** Colors and font sizes for headings, body text, and input fields. * **Buttons:** Background colors, text colors, and corner radii for "Continue" and "Cancel" actions. * **Navigation Bar:** Background and title colors for the header. * **Input Fields:** Border colors and focus states. ```swift Swift theme={"system"} // Create toolbar customization let toolbar = Gr4vyThreeDSToolbarCustomization( textColorHex: "#FFFFFF", backgroundColorHex: "#007AFF", headerText: "Secure Verification", buttonText: "Cancel" ) // Create button customizations let submitButton = Gr4vyThreeDSButtonCustomization( textFontSize: 16, textColorHex: "#FFFFFF", backgroundColorHex: "#007AFF", cornerRadius: 8 ) let cancelButton = Gr4vyThreeDSButtonCustomization( textFontSize: 16, textColorHex: "#007AFF", backgroundColorHex: "#F0F0F0", cornerRadius: 8 ) // Create label customization let label = Gr4vyThreeDSLabelCustomization( textFontSize: 14, textColorHex: "#333333", headingTextFontSize: 18, headingTextColorHex: "#000000" ) // Create text box customization let textBox = Gr4vyThreeDSTextBoxCustomization( textFontSize: 16, textColorHex: "#000000", borderWidth: 1, borderColorHex: "#CCCCCC", cornerRadius: 4 ) // Combine into UI customization let uiCustomization = Gr4vyThreeDSUiCustomization( label: label, toolbar: toolbar, textBox: textBox, buttons: [ .submit: submitButton, .cancel: cancelButton ] ) // Use with tokenization let result = try await gr4vy.tokenize( checkoutSessionId: "session_123", cardData: cardData, authenticate: true, uiCustomization: Gr4vyThreeDSUiCustomizationMap(default: uiCustomization) ) ``` ```kotlin Kotlin theme={"system"} // Create toolbar customization val toolbar = Gr4vyThreeDSToolbarCustomization( textFontName = "sans-serif-medium", textFontSize = 17, textColorHex = "#FFFFFF", backgroundColorHex = "#007AFF", headerText = "Secure Verification", buttonText = "Cancel" ) // Create button customizations val submitButton = Gr4vyThreeDSButtonCustomization( textFontSize = 16, textColorHex = "#FFFFFF", backgroundColorHex = "#007AFF", cornerRadius = 8 ) val cancelButton = Gr4vyThreeDSButtonCustomization( textFontSize = 16, textColorHex = "#007AFF", backgroundColorHex = "#F0F0F0", cornerRadius = 8 ) // Create label customization val label = Gr4vyThreeDSLabelCustomization( textFontSize = 14, textColorHex = "#333333", headingTextFontSize = 18, headingTextColorHex = "#000000" ) // Create text box customization val textBox = Gr4vyThreeDSTextBoxCustomization( textFontSize = 16, textColorHex = "#000000", borderWidth = 1, borderColorHex = "#CCCCCC", cornerRadius = 4 ) // Combine into UI customization val uiCustomization = Gr4vyThreeDSUiCustomization( label = label, toolbar = toolbar, textBox = textBox, buttons = mapOf( Gr4vyThreeDSButtonType.SUBMIT to submitButton, Gr4vyThreeDSButtonType.CANCEL to cancelButton ) ) // Use with tokenization val result = gr4vy.tokenize( checkoutSessionId = "session_123", cardData = cardData, activity = this@PaymentActivity, authenticate = true, uiCustomization = Gr4vyThreeDSUiCustomizationMap(default = uiCustomization) ) ``` All color values should be provided as hexadecimal strings (for example, `#007AFF`). ## Testing and validation To test your implementation, use the 3DS Test Cards defined here in your sandbox environment. * Frictionless Flow: Use a card that is configured to skip the challenge phase. The SDK can return success immediately after the authentication call. * Challenge Flow: Use a card that requires an SMS or Biometric challenge. The SDK can automatically present the native modal for you to complete. Use these card numbers and scenarios to test your native 3DS implementation in the Gr4vy Sandbox environment. These cards allow you to simulate various outcomes, including frictionless authentication, mandatory challenges, and specific failure states. ### Testing scenarios #### Visa | Card Number | Expected 3DS Outcome | | - | - | | `4000 0000 0000 0010` | Frictionless Success: Authenticated without user interaction. | | `4000 0000 0000 0028` | Challenge Required: Triggers a native 3DS challenge UI. | | `4000 0000 0000 0044` | Authentication Failed: Simulates a "Not Authenticated" result. | | `4000 0000 0000 1042` | Rejected: The authentication request was explicitly rejected. | #### Mastercard | Card Number | Expected 3DS Outcome | | - | - | | `5100 0000 0000 0010` | Challenge Required: Triggers a native 3DS challenge UI. | | `5200 0000 0000 2052` | Frictionless Success: Authenticated without user interaction. | | `5200 0000 0000 1039` | Unavailable: Simulates a scenario where 3DS is not available for the card. | | `5100 0000 0000 0065` | Suspected Fraud: Frictionless failure due to suspected fraud. | > [!NOTE] > > The challenge card `5100 0000 0000 0010` doesn't pass a Luhn check. If your card form validates the number before it submits, allow this number in your sandbox form validation when you test the Mastercard challenge flow. #### American Express | Card Number | Expected 3DS Outcome | | - | - | | `3415 0209 8634 895` | Frictionless Success: Authenticated without user interaction. | | `3486 3826 7931 507` | Challenge Required: Triggers a native 3DS challenge UI. | | `3456 9539 9207 589` | Authentication Failed: Frictionless failure. | ### Challenge screen inputs When a challenge is triggered, use the following values to simulate different transaction results within the native UI. #### OTP (One-Time Password) When the challenge screen asks for a code, enter one of the following: | OTP Code | Resulting Transaction Status | Description | | - | - | - | | `1234` | Y | Success / Authenticated. | | `1111` | N | Not Authenticated. | | `2222` | R | Rejected. | | `3333` | U | Unavailable / Unable to complete. | #### Selection options Some challenges may present a list of options (for example, "Select your favorite city"). **Single-select** * Paris or Nice: Returns Success (Y). **Multi-select** * Paris & Lyon: Returns Success (Y). * Toulouse & Lyon: Returns Success (Y). ## Best practices for testing 1. Verify UI transitions: Ensure your app correctly transitions from the card entry screen to the native 3DS challenge modal and back. 2. Test error states: Use the "Rejected" or "Failed" cards to ensure your app displays helpful messaging to the user when a card cannot be verified. 3. **Check Backend Session:** After a successful test, verify that the `checkout_session_id` on your server reflects the successful authentication before attempting the final transaction. # Card details Source: https://integration.wpay.com.au/guides/payments/native/get-card-details The native SDKs provide a convenient way to retrieve details about a particular card based on its BIN (Bank Identification Number), checkout country, currency, and other parameters. This allows you to identify the card brand, card type, and other relevant information before processing a payment. ## Use cases Get card details to: * Identify the card brand (Visa, Mastercard, etc.) as the user types * Determine the card type (debit, credit, prepaid) * Display the appropriate card logo in your UI * Validate card compatibility with your payment processors * Provide better user experience with card-specific information ### Implementation Create a request with card details (BIN, country, currency, amount) to retrieve information about the card. ```swift Swift (iOS) theme={"system"} let gr4vy = try Gr4vy( gr4vyId: "example", token: "your_jwt_token", merchantId: "merchant_123", // Set the default merchant ID server: .sandbox, debugMode: true ) // Create card details object let cardDetails = Gr4vyCardDetails( currency: "AUD", amount: "1299", bin: "411111", country: "AU", intent: "capture" ) // Create request let request = Gr4vyCardDetailsRequest( cardDetails: cardDetails, timeout: 30.0 ) // Async/await do { let cardDetailsResponse = try await gr4vy.cardDetails.get(request: request) print("Card brand: \(cardDetailsResponse.scheme)") print("Card type: \(cardDetailsResponse.cardType)") } catch { print("Error fetching card details: \(error)") } // Completion handler gr4vy.cardDetails.get(request: request) { result in switch result { case .success(let cardDetailsResponse): print("Card brand: \(cardDetailsResponse.scheme)") print("Card type: \(cardDetailsResponse.cardType)") case .failure(let error): print("Error fetching card details: \(error)") } } ``` ```kotlin Kotlin (Android) theme={"system"} val gr4vy = Gr4vy( gr4vyId = "example", token = "your_jwt_token", merchantId = "merchant_123", // Set the default merchant ID server = Gr4vyServer.SANDBOX, debugMode = true ) // Create card details object val cardDetails = Gr4vyCardDetails( currency = "AUD", amount = "1299", bin = "411111", country = "AU", intent = "capture" ) // Create request val request = Gr4vyCardDetailsRequest( timeout = 30.0, cardDetails = cardDetails ) // Suspend function lifecycleScope.launch { try { val cardDetailsResponse = gr4vy.cardDetails.get(request) println("Card brand: ${cardDetailsResponse.data.scheme}") println("Card type: ${cardDetailsResponse.data.cardType}") } catch (error: Gr4vyError) { println("Error fetching card details: $error") } } // Callback version gr4vy.cardDetails.get(request) { result -> when { result.isSuccess -> { val cardDetailsResponse = result.getOrNull() println("Card brand: ${cardDetailsResponse?.data?.scheme}") println("Card type: ${cardDetailsResponse?.data?.cardType}") } result.isFailure -> { println("Error fetching card details: ${result.exceptionOrNull()}") } } } ``` ## Best practices * **Use BIN lookup early**: Retrieve card details as soon as you have the first 6 digits of the card number to provide instant feedback to users. * **Error handling**: Always handle network and HTTP errors gracefully and show appropriate messages to users. * **Timeout configuration**: Use custom timeouts for card details requests if your network conditions require it. * **Cache wisely**: Consider caching card details for a short period to reduce API calls during the same checkout session. # Create an API key Source: https://integration.wpay.com.au/guides/payments/native/key Before the app can communicate with the API, create a new API key. The API handles authentication using a signed **JSON Web Token** (JWT) which is passed in a HTTP header. ```bash theme={"system"} curl -X GET https://api.example.gr4vy.app/transactions \ -H "authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIi..." ``` > [!TIP] > > Using an SDK is recommended. SDKs make the process of generating a token a lot easier and are probably > available in the preferred programming language. This is covered in the > next step. ## Create a new API key To use the API, generate a new **API key**. Head over to the dashboard and visit the **Integrations** page. ![API key dashboard](https://mintcdn.com/wpay/c50rx3S48hHhNNxb/assets/images/api-keys/add-key.png?fit=max\&auto=format\&n=c50rx3S48hHhNNxb\&q=85\&s=ffbfb085954c55e845050e44a7c9e73e) On this page, click the **Add API key** button and select a name for the key. The name is purely to track what key is for what integration. The downloaded key needs to be stored securely as it is not stored by the system. > [!NOTE] > > Learn more about private keys, key-pairs, and JWT in the > [in-depth guide on authentication](/guides/api/authentication). ## Summary In this step: * Learned about API authentication. * Created a new private key for the API. * Stored this API key in a secure place where the app can access it. # List buyer payment methods Source: https://integration.wpay.com.au/guides/payments/native/list-buyer-payment-methods The native SDKs provide a convenient way to retrieve all stored payment methods for a specific buyer, filtered by the checkout's currency and country. This allows you to display previously saved payment methods to returning customers for a faster checkout experience. ## Use cases List buyer payment methods to: * Display saved payment methods to returning customers * Allow buyers to select from their stored cards and payment methods * Filter payment methods by country and currency for the current checkout * Show payment method details (last 4 digits, expiration date, card brand) * Sort payment methods by last used date or other criteria ### Implementation Create a request with buyer identification (buyer ID or external identifier) and optional filters to retrieve stored payment methods. ```swift Swift (iOS) theme={"system"} let gr4vy = try Gr4vy( gr4vyId: "example", token: "your_jwt_token", merchantId: "merchant_123", // Set the default merchant ID server: .sandbox, debugMode: true ) // Create payment methods criteria let paymentMethods = Gr4vyBuyersPaymentMethods( buyerId: "buyer_123", buyerExternalIdentifier: "external_456", sortBy: .lastUsedAt, orderBy: .desc, country: "AU", currency: "AUD" ) // Create request let request = Gr4vyBuyersPaymentMethodsRequest( paymentMethods: paymentMethods, merchantId: "merchant_123", // Optional timeout: 30.0 ) // Async/await do { let paymentMethodsList = try await gr4vy.paymentMethods.list(request: request) print("Found \(paymentMethodsList.count) payment methods") } catch { print("Error fetching payment methods: \(error)") } // Completion handler gr4vy.paymentMethods.list(request: request) { result in switch result { case .success(let paymentMethodsList): print("Found \(paymentMethodsList.count) payment methods") case .failure(let error): print("Error fetching payment methods: \(error)") } } ``` ```kotlin Kotlin (Android) theme={"system"} val gr4vy = Gr4vy( gr4vyId = "example", token = "your_jwt_token", merchantId = "merchant_123", // Set the default merchant ID server = Gr4vyServer.SANDBOX, debugMode = true ) // Create payment methods criteria val paymentMethods = Gr4vyBuyersPaymentMethods( buyerId = "buyer_123", buyerExternalIdentifier = "external_456", sortBy = Gr4vySortBy.LAST_USED_AT.value, orderBy = Gr4vyOrderBy.DESC.value, country = "AU", currency = "AUD" ) // Create request val request = Gr4vyBuyersPaymentMethodsRequest( merchantId = "merchant_123", // Optional timeout = 30.0, paymentMethods = paymentMethods ) // Suspend function lifecycleScope.launch { try { val paymentMethodsList = gr4vy.paymentMethods.list(request) println("Found ${paymentMethodsList.data.items.size} payment methods") } catch (error: Gr4vyError) { println("Error fetching payment methods: $error") } } // Callback version gr4vy.paymentMethods.list(request) { result -> when { result.isSuccess -> { val paymentMethodsList = result.getOrNull() println("Found ${paymentMethodsList?.data?.items?.size} payment methods") } result.isFailure -> { println("Error fetching payment methods: ${result.exceptionOrNull()}") } } } ``` ## Best practices * **Buyer identification**: Use either `buyerId` (internal Gr4vy ID) or `buyerExternalIdentifier` (your system's ID) to identify the buyer. * **Filter appropriately**: Apply country and currency filters to show only payment methods relevant to the current transaction. * **Sort by usage**: Sort by `lastUsedAt` to show the most recently used payment methods first for better user experience. * **Error handling**: Always handle network and HTTP errors gracefully and show appropriate messages to users. * **Timeout configuration**: Use custom timeouts for payment methods requests if your network conditions require it. * **Display clearly**: Show payment method details (card brand, last 4 digits, expiration date) to help users identify their preferred method. # List payment options Source: https://integration.wpay.com.au/guides/payments/native/list-payment-options The native SDKs provide a convenient way to retrieve available payment options for your checkout. This allows you to display all payment methods that are available for the transaction to your users. ## Use cases List payment options to: * Display available payment methods at checkout * Filter payment options based on merchant, country, currency, and amount * Determine which payment methods to present to your customer * Implement dynamic payment method selection UI ### Implementation Create a request with your checkout details and retrieve available payment options. ```swift Swift (iOS) theme={"system"} let gr4vy = try Gr4vy( gr4vyId: "example", token: "your_jwt_token", merchantId: "merchant_123", // Set the default merchant ID server: .sandbox, debugMode: true ) let request = Gr4vyPaymentOptionRequest( merchantId: "merchant_123", // Optional, uses SDK merchantId if not provided metadata: ["order_id": "12345"], country: "AU", currency: "AUD", amount: 1299, locale: "en-US", cartItems: nil ) // Async/await do { let paymentOptions = try await gr4vy.paymentOptions.list(request: request) print("Available payment options: \(paymentOptions.count)") } catch { print("Error fetching payment options: \(error)") } // Completion handler gr4vy.paymentOptions.list(request: request) { result in switch result { case .success(let paymentOptions): print("Available payment options: \(paymentOptions.count)") case .failure(let error): print("Error fetching payment options: \(error)") } } ``` ```kotlin Kotlin (Android) theme={"system"} val gr4vy = Gr4vy( gr4vyId = "example", token = "your_jwt_token", merchantId = "merchant_123", // Set the default merchant ID server = Gr4vyServer.SANDBOX, debugMode = true ) // Create request val request = Gr4vyPaymentOptionRequest( merchantId = "merchant_123", // Optional, uses SDK merchantId if not provided metadata = mapOf("order_id" to "12345"), country = "AU", currency = "AUD", amount = 1299, locale = "en-US", cartItems = null ) // Suspend function lifecycleScope.launch { try { val paymentOptions = gr4vy.paymentOptions.list(request) println("Available payment options: ${paymentOptions.data.items.size}") } catch (error: Gr4vyError) { println("Error fetching payment options: $error") } } // Callback version gr4vy.paymentOptions.list(request) { result -> when { result.isSuccess -> { val paymentOptions = result.getOrNull() println("Available payment options: ${paymentOptions?.data?.items?.size}") } result.isFailure -> { println("Error fetching payment options: ${result.exceptionOrNull()}") } } } ``` ## Best practices * **Cache results**: Store payment options locally to reduce API calls when the country, currency, and amount haven't changed. * **Error handling**: Always handle network and HTTP errors gracefully and show appropriate messages to users. * **Timeout configuration**: Use custom timeouts for payment options requests if your network conditions require it. * **Localization**: Use the appropriate locale to ensure payment option labels are displayed in the correct language. # Native Mobile SDKs Source: https://integration.wpay.com.au/guides/payments/native/overview Gr4vy provides native mobile SDKs to help you build a high-performance checkout experience tailored specifically for iOS and Android. While Gr4vy offers a web-based "Hosted" Embed experience, these native SDKs allow you to build card forms directly into your app, giving you full control over the user interface, theme, and the overall customer journey. Native **Swift** and Native **Kotlin** SDKs provide: * **Direct Embedding:** You build the card form from native components in your app’s view hierarchy. There is no need to open a web view to capture payment details. * **Total UI Control:** You have full control over the look and feel, ensuring the checkout process is indistinguishable from the rest of your app. * **Native 3DS Support:** Authentication happens within a native interface. This avoids clunky browser redirects and provides a much smoother flow for the customer. * **Performance:** Native components offer better responsiveness and smoother animations compared to web-based alternatives. ## Native 3DS workflow To provide a seamless experience, these SDKs support performing 3DS authentication before a transaction is created. 1. **Vaulting:** Your app captures the card data and uses the native SDK to securely vault it into a Checkout Session. 2. **Authentication:** The SDK initiates the 3DS challenge directly in the client. The user completes the challenge (for example, via a fingerprint or a one-time password) within the app. 3. **Completion:** Once the 3DS flow is finished, the Checkout Session is updated with the authentication data. This session can then be used by your backend to process the transaction. ## SDK repositories You can find the SDKs and implementation examples in the GitHub repositories. > **[Swift SDK](https://github.com/gr4vy/gr4vy-swift)** > > Native Swift SDK for building custom payment experiences on iOS devices. > **[Kotlin SDK](https://github.com/gr4vy/gr4vy-kotlin)** > > Native Kotlin SDK for building custom payment experiences on Android devices. Each repository contains a Sample App that demonstrates how to implement a native card form and handle the 3DS authentication flow. ## Self-certification and scope The native SDKs don't include card entry UI. Your app collects the card number and security code in its own fields and passes them to the SDK, which sends them to Gr4vy. Because your app handles cardholder data, it's in scope for the Payment Card Industry Data Security Standard (PCI DSS). * **SAQ D:** Neither Self-Assessment Questionnaire (SAQ) A nor SAQ A-EP covers a native app that handles the card number, so you typically validate with SAQ D. SAQ D covers every PCI DSS requirement that applies to your environment, including secure software development and penetration testing. * **Keeping card data out of your app:** If you want to reduce your scope, use the [Embed SDKs](/guides/payments/mobile#embed-sdks) instead. Gr4vy renders the card form, so your app never handles the card number. Your acquirer decides how you validate compliance. Depending on your transaction volume, you might need an annual assessment by a Qualified Security Assessor (QSA), regardless of which integration you choose. The integration changes how much of your app and build pipeline that assessment covers. ## Merchant responsibilities Your app handles cardholder data, so you are responsible for protecting it on the device and in your build and release pipeline. You must: * Pass card data to the SDK as soon as the buyer enters it. Never log, cache, or store the card number or security code on the device. * Never transmit, process, or store raw card data on your own servers. Only send it to Gr4vy through the SDK. * Complete your annual PCI DSS validation, either SAQ D or an assessment by a QSA, and the Attestation of Compliance (AoC) that goes with it. # SDK Response Reference Source: https://integration.wpay.com.au/guides/payments/native/responses The Gr4vy native SDKs provide a standardized way to handle the results of vaulting and 3DS authentication operations across iOS and Android. ## Success response When an operation completes successfully, the SDK returns a status object confirming that the card details have been tokenized and, if requested, the 3DS authentication has been attempted. ### Response object | Field | Type | Description | | - | - | - | | `tokenized` | Boolean | Indicates if the card was successfully vaulted/tokenized. | | `authentication` | Object | (Optional) Contains the results of the 3DS authentication flow. | ## The authentication object If authenticate: true was passed to the tokenize method, the authentication object contains the following fields: | Field | Type | Description | | - | - | - | | `transactionStatus` | String | The 3DS transaction status code. See [Authentication results](/guides/features/3ds/statuses) for the full list of values. | | `attempted` | Boolean | Indicates if a 3DS authentication was actually attempted. | | `hasTimedOut` | Boolean | Indicates if the 3DS flow timed out. | | `hasCancelled` | Boolean | Indicates if the user manually dismissed the challenge screen. | | `type` | String | The type of flow performed (for example, "frictionless" or "challenge"). | ## Implementation examples ```swift Swift theme={"system"} case .success(let status): if let auth = status.authentication, auth.user_cancelled { // Handle user cancellation } else if status.tokenized { // Proceed to create a transaction on your backend } ``` ```kotlin Kotlin theme={"system"} is Gr4vyResult.Success -> { val auth = result.data.authentication if (auth?.user_cancelled == true) { // Handle user cancellation } else if (result.data.tokenized) { // Proceed to create a transaction on your backend } } ``` ## Error handling Errors are categorized into two types: 1. SDK Failures (returned via the failure callback) 2. Authentication Failures (status flags within a success response) ### 1. Authentication failures (inside success result) Sometimes the SDK successfully communicates with the 3DS server, but the authentication itself is not "successful" (for example, the user fails the challenge or cancels). You should check the `authentication` object even when the SDK returns a success result. If authentication was `attempted` but the user `hasCancelled` you can retry vaulting, or proceed without 3DS. ### 2. SDK failure types (failure callback) These errors occur when the SDK cannot complete the request due to configuration, network, or validation issues. Please see the generic guides on how to handle these errors in [Swift](https://github.com/gr4vy/gr4vy-swift?tab=readme-ov-file#error-handling) and [Kotlin](https://github.com/gr4vy/gr4vy-kotlin?tab=readme-ov-file#error-handling). ```swift Swift theme={"system"} import gr4vy_swift do { let paymentOptions = try await gr4vy.paymentOptions.list(request: request) // Handle success } catch let error as Gr4vyError { switch error { case .invalidGr4vyId: print("Invalid Gr4vy ID provided") case .badURL(let url): print("Invalid URL: \(url)") case .httpError(let statusCode, _, let message): print("HTTP \(statusCode): \(message ?? "Unknown error")") case .networkError(let urlError): print("Network error: \(urlError.localizedDescription)") case .decodingError(let message): print("Decoding error: \(message)") case .threeDSError(let message): print("3DS authentication error: \(message)") case .uiContextError(let message): print("UI context error: \(message)") } } catch { print("Unexpected error: \(error)") } ``` ```kotlin Kotlin theme={"system"} import com.gr4vy.sdk.* lifecycleScope.launch { try { val paymentOptions = gr4vy.paymentOptions.list(request) // Handle success } catch (error: Gr4vyError) { when (error) { is Gr4vyError.InvalidGr4vyId -> { println("Invalid Gr4vy ID provided") } is Gr4vyError.BadURL -> { println("Invalid URL: ${error.url}") } is Gr4vyError.HttpError -> { println("HTTP ${error.statusCode}: ${error.errorMessage ?: "Unknown error"}") // Access detailed error information if (error.hasDetails()) { println("Detailed errors: ${error.getDetailedErrorMessage()}") } } is Gr4vyError.NetworkError -> { println("Network error: ${error.exception.message}") } is Gr4vyError.DecodingError -> { println("Decoding error: ${error.errorMessage}") } is Gr4vyError.ThreeDSError -> { println("3D Secure authentication error: ${error.message}") } is Gr4vyError.UiContextError -> { println("UI context error (Activity required): ${error.message}") } } } } ``` # Generating tokens for authentication Source: https://integration.wpay.com.au/guides/payments/native/sdks Before adding the SDK to the native app, create a client token that can be used to authenticate the transaction. The mobile native SDKs rely on one of two tokens, depending on the method used. 1. An Embed token (a JWT) used to call most APIs. 2. A checkout session ID used to tokenize card data into a checkout session. The following examples show how to create an Embed token, a checkout session ID, and how these can be tied together. ## Install a server-side SDK Use the package manager in the preferred programming language to install the server-side SDK. Token generation can only be done server side and doing this client side is not recommended as it exposes the API key to customers. ```sh C# theme={"system"} dotnet add package Gr4vy ``` ```sh Go theme={"system"} go get github.com/gr4vy/gr4vy-go ``` ```sh Java theme={"system"} # Please check for the latest version implementation 'com.gr4vy:sdk:1.0.0' ``` ```sh PHP theme={"system"} composer require "gr4vy/gr4vy-php" ``` ```sh Python theme={"system"} pip install gr4vy ``` ```sh TypeScript theme={"system"} npm install @gr4vy/sdk # or: yarn add @gr4vy/sdk ``` > [!NOTE] > > Please always check and install the [latest release](/guides/get-started#server-side-sdks) of the preferred SDK. ## Initialize the SDK client Next, initialize the SDK with the ID of the instance and the private key. ```csharp C# theme={"system"} using Gr4vy; using Gr4vy.Models.Components; using System.Collections.Generic; // Loaded the key from a file, env variable, // or anywhere else var privateKey = "..."; var sdk = new Gr4vySDK( id: "example", server: SDKConfig.Server.Sandbox, bearerAuthSource: Auth.WithToken(privateKey), merchantAccountId: "default" ); ``` ```go Go theme={"system"} package main import ( "context" gr4vy "github.com/gr4vy/gr4vy-go" "github.com/gr4vy/gr4vy-go/models/operations" "log" "os" ) func main() { ctx := context.Background() privateKey := "...." // Private key loaded from disk or env var withToken := gr4vy.WithToken(privateKey, []JWTScope{ReadAll, WriteAll}, 60) s := gr4vy.New( gr4vy.WithID("example"), gr4vy.WithServer(gr4vy.ServerSandbox), gr4vy.WithSecuritySource(withToken), gr4vy.WithMerchantAccountID("default"), ) } ``` ```java Java theme={"system"} package hello.world; import com.gr4vy.sdk.BearerSecuritySource; import com.gr4vy.sdk.Gr4vy; import com.gr4vy.sdk.Gr4vy.AvailableServers; import com.gr4vy.sdk.models.components.AccountUpdaterJobCreate; import com.gr4vy.sdk.models.errors.*; import com.gr4vy.sdk.models.operations.ListTransactionsRequest; import java.lang.Exception; import java.util.List; public class Application { public static void main(String[] args) throws Exception { String privateKey = "-----BEGIN PRIVATE KEY-----\n...."; // a valid private key Gr4vy sdk = Gr4vy.builder() .id("example") .server(AvailableServers.SANDBOX) .merchantAccountId("default") .securitySource(new BearerSecuritySource.Builder(privateKey).build()) .build(); } } ``` ```php PHP theme={"system"} declare(strict_types=1); require 'vendor/autoload.php'; use Gr4vy; use Gr4vy\Auth; // Loaded the key from a file, env variable, // or anywhere else $privateKey = "..."; $sdk = Gr4vy\SDK::builder() ->setId('example') ->setServer('sandbox') ->setSecuritySource(Auth::withToken($privateKey)) ->setMerchantAccountId('default') ->build(); ``` ```python Python theme={"system"} from gr4vy import Gr4vy, auth import os client = Gr4vy( id="example", server="production", merchant_account_id="default", bearer_auth=auth.with_token(open("./private_key.pem").read()) ) ``` ```ts TypeScript theme={"system"} import fs from "fs"; import { Gr4vy, withToken } from "@gr4vy/sdk"; const gr4vy = new Gr4vy({ server: "sandbox", id: "example", bearerAuth: withToken({ privateKey: fs.readFileSync("private_key.pem", "utf8"), }), }); ``` > [!NOTE] > > The instance ID is the unique identifier for the deployment of the system and is included in every API call. > Together with the environment (sandbox or production) it is used to connect to the right APIs, as well as dashboard. > > The possible values for your ID are one of `wpay`, `wpay1`, `wpay2`, or `wpay3`. You are informed > in which instance your account has been set up. ## Generate a token The next step is to create a new token for frontend authentication. The SDK comes with a helper function for exactly that purpose. It requires an amount and currency for the transaction to be created and additionally accepts either the ID of a buyer or an external identifier for a buyer to link the transaction to. For Embed, it is recommended to attach a checkout session to every transaction. The `getEmbedTokenWithCheckoutSession` helper creates a checkout session using your SDK client and returns an Embed token with the resulting `checkout_session_id` already pinned, in a single call. ```csharp C# theme={"system"} using System.Collections.Generic; using Gr4vy; // Loaded the key from a file, env variable, // or anywhere else var privateKey = "..."; var sdk = new Gr4vySDK( id: "example", server: SDKConfig.Server.Sandbox, bearerAuthSource: Auth.WithToken(privateKey), merchantAccountId: "default" ); var token = await Auth.GetEmbedTokenWithCheckoutSession( client: sdk, privateKey: privateKey, embedParams: new Dictionary { ["amount"] = 1299, ["currency"] = "AUD", ["buyer_external_identifier"] = "user-1234", } ); ``` ```go Go theme={"system"} package main import ( "context" gr4vy "github.com/gr4vy/gr4vy-go" "log" ) func main() { privateKey := "...." // Private key loaded from disk or env var client := gr4vy.New( gr4vy.WithID("example"), gr4vy.WithServer(gr4vy.ServerSandbox), gr4vy.WithSecuritySource(gr4vy.WithToken(privateKey, nil, 3600)), gr4vy.WithMerchantAccountID("default"), ) embedParams := map[string]interface{}{ "amount": 1299, "currency": "AUD", "buyer_external_identifier": "user-1234", } token, err := gr4vy.GetEmbedTokenWithCheckoutSession(context.Background(), client, privateKey, embedParams, nil, nil) if err != nil { log.Fatal(err) } log.Println(token) } ``` ```java Java theme={"system"} package hello.world; import com.gr4vy.sdk.Auth; import com.gr4vy.sdk.BearerSecuritySource; import com.gr4vy.sdk.Gr4vy; import com.gr4vy.sdk.Gr4vy.AvailableServers; import java.util.HashMap; import java.util.Map; public class Application { public static void main(String[] args) throws Exception { String privateKey = "-----BEGIN PRIVATE KEY-----\n...."; // a valid private key Gr4vy client = Gr4vy.builder() .id("example") .server(AvailableServers.SANDBOX) .securitySource(new BearerSecuritySource.Builder(privateKey).build()) .merchantAccountId("default") .build(); Map embedParams = new HashMap<>(); embedParams.put("amount", 1299); embedParams.put("currency", "AUD"); embedParams.put("buyer_external_identifier", "user-1234"); String token = Auth.getEmbedTokenWithCheckoutSession(client, privateKey, embedParams); System.out.println(token); } } ``` ```php PHP theme={"system"} declare(strict_types=1); require 'vendor/autoload.php'; use Gr4vy; use Gr4vy\Auth; // Loaded the key from a file, env variable, // or anywhere else $privateKey = "..."; $sdk = Gr4vy\SDK::builder() ->setId('example') ->setServer('sandbox') ->setSecuritySource(Auth::withToken($privateKey)) ->setMerchantAccountId('default') ->build(); $token = Auth::getEmbedTokenWithCheckoutSession( client: $sdk, privateKey: $privateKey, embedParams: [ "amount" => 1299, "currency" => "AUD", "buyer_external_identifier" => "user-1234", ], ); ``` ```python Python theme={"system"} from gr4vy import Gr4vy, auth private_key = open("./private_key.pem").read() g_client = Gr4vy( id="example", server="production", merchant_account_id="default", bearer_auth=auth.with_token(private_key) ) token = auth.get_embed_token_with_checkout_session( g_client, private_key, embed_params={ "amount": 1299, "currency": "AUD", "buyer_external_identifier": "user-1234", }, ) ``` ```ts TypeScript theme={"system"} import { Gr4vy, getEmbedTokenWithCheckoutSession, withToken } from "@gr4vy/sdk"; import fs from "fs"; async function run() { const privateKey = fs.readFileSync("private_key.pem", "utf8") const gr4vy = new Gr4vy({ server: "sandbox", id: "example", bearerAuth: withToken({ privateKey }), }); const token = await getEmbedTokenWithCheckoutSession({ client: gr4vy, privateKey, embedParams: { amount: 1299, currency: 'AUD', buyerExternalIdentifier: 'user-1234', } }); console.log(token); } run(); ``` This token can now be served to the frontend where it is used by the SDK. If generating the client token without using a server-side SDK is preferred, then please read the guide on [manually generating a JWT](/guides/api/authentication#without-sdks). ## Checkout sessions Passing a [checkout session](/reference/checkout-sessions/new-checkout-session) ID to the Embed token function is recommended. This ID can be used to help identify multiple transaction attempts. This is useful to identify if one or more failed/declined transactions eventually result in a successful transaction. Checkout sessions can also store `cart_items`, `metadata`, airline data, and more, removing the need to pass these values on the transaction request. ```csharp C# theme={"system"} using Gr4vy; using Gr4vy.Models.Components; using NodaTime; using System; using System.Collections.Generic; var res = await sdk.CheckoutSessions.CreateAsync(checkoutSessionCreate: new CheckoutSessionCreate() { CartItems = new List() { new CartItem() { Name = "GoPro HD", Quantity = 2, UnitAmount = 1299, DiscountAmount = 0, TaxAmount = 0, ExternalIdentifier = "goprohd", Sku = "GPHD1078", ProductUrl = "https://example.com/catalog/go-pro-hd", ImageUrl = "https://example.com/images/go-pro-hd.jpg", Categories = new List() { "camera", "travel", "gear", }, ProductType = "physical", SellerCountry = "GB", }, new CartItem() { Name = "GoPro HD", Quantity = 2, UnitAmount = 1299, DiscountAmount = 0, TaxAmount = 0, ExternalIdentifier = "goprohd", Sku = "GPHD1078", ProductUrl = "https://example.com/catalog/go-pro-hd", ImageUrl = "https://example.com/images/go-pro-hd.jpg", Categories = new List() { "camera", "travel", "gear", }, ProductType = "physical", SellerCountry = "GB", }, new CartItem() { Name = "GoPro HD", Quantity = 2, UnitAmount = 1299, DiscountAmount = 0, TaxAmount = 0, ExternalIdentifier = "goprohd", Sku = "GPHD1078", ProductUrl = "https://example.com/catalog/go-pro-hd", ImageUrl = "https://example.com/images/go-pro-hd.jpg", Categories = new List() { "camera", "travel", "gear", }, ProductType = "physical", SellerCountry = "AU", }, }, Metadata = new Dictionary() { { "cohort", "cohort-a" }, { "order_id", "order-12345" }, }, Buyer = new GuestBuyer() { DisplayName = "John Doe", ExternalIdentifier = "buyer-12345", BillingDetails = new BillingDetails() { FirstName = "John", LastName = "Doe", EmailAddress = "john@example.com", PhoneNumber = "+1234567890", Address = new Address() { City = "San Jose", Country = "AU", PostalCode = "94560", State = "California", StateCode = "US-CA", HouseNumberOrName = "10", Line1 = "Stafford Apartments", Line2 = "29th Street", Organization = "Gr4vy", }, TaxId = new TaxId() { Value = "12345678931", Kind = "ar.cuit", }, }, ShippingDetails = new ShippingDetailsCreate() { FirstName = "John", LastName = "Doe", EmailAddress = "john@example.com", PhoneNumber = "+1234567890", Address = new Address() { City = "San Jose", Country = "AU", PostalCode = "94560", State = "California", StateCode = "US-CA", HouseNumberOrName = "10", Line1 = "Stafford Apartments", Line2 = "29th Street", Organization = "Gr4vy", }, }, }, }); // handle response ``` ```go Go theme={"system"} package main import( "context" gr4vygo "github.com/gr4vy/gr4vy-go" "os" "github.com/gr4vy/gr4vy-go/models/components" "github.com/gr4vy/gr4vy-go/types" "log" ) func main() { ctx := context.Background() // Init SDK res, err := s.CheckoutSessions.Create(ctx, &components.CheckoutSessionCreate{ CartItems: []components.CartItem{ components.CartItem{ Name: "GoPro HD", Quantity: 2, UnitAmount: 1299, DiscountAmount: gr4vygo.Int64(0), TaxAmount: gr4vygo.Int64(0), ExternalIdentifier: gr4vygo.String("goprohd"), Sku: gr4vygo.String("GPHD1078"), ProductURL: gr4vygo.String("https://example.com/catalog/go-pro-hd"), ImageURL: gr4vygo.String("https://example.com/images/go-pro-hd.jpg"), Categories: []string{ "camera", "travel", "gear", }, ProductType: components.ProductTypePhysical.ToPointer(), SellerCountry: gr4vygo.String("AU"), }, components.CartItem{ Name: "GoPro HD", Quantity: 2, UnitAmount: 1299, DiscountAmount: gr4vygo.Int64(0), TaxAmount: gr4vygo.Int64(0), ExternalIdentifier: gr4vygo.String("goprohd"), Sku: gr4vygo.String("GPHD1078"), ProductURL: gr4vygo.String("https://example.com/catalog/go-pro-hd"), ImageURL: gr4vygo.String("https://example.com/images/go-pro-hd.jpg"), Categories: []string{ "camera", "travel", "gear", }, ProductType: components.ProductTypePhysical.ToPointer(), SellerCountry: gr4vygo.String("AU"), }, }, Metadata: map[string]string{ "cohort": "cohort-a", "order_id": "order-12345", }, Buyer: nil, }) if err != nil { log.Fatal(err) } if res != nil { // handle response } } ``` ```java Java theme={"system"} package hello.world; import com.gr4vy.sdk.Gr4vy; import com.gr4vy.sdk.models.components.*; import com.gr4vy.sdk.models.errors.*; import com.gr4vy.sdk.models.operations.CreateCheckoutSessionResponse; import java.lang.Exception; import java.time.LocalDate; import java.time.OffsetDateTime; import java.util.List; import java.util.Map; import org.openapitools.jackson.nullable.JsonNullable; public class Application { public static void main(String[] args) throws Exception { // Init SDK CreateCheckoutSessionResponse res = sdk.checkoutSessions().create() .checkoutSessionCreate(CheckoutSessionCreate.builder() .cartItems(List.of( CartItem.builder() .name("GoPro HD") .quantity(2L) .unitAmount(1299L) .discountAmount(0L) .taxAmount(0L) .externalIdentifier("goprohd") .sku("GPHD1078") .productUrl("https://example.com/catalog/go-pro-hd") .imageUrl("https://example.com/images/go-pro-hd.jpg") .categories(List.of( "camera", "travel", "gear")) .productType(ProductType.PHYSICAL) .sellerCountry("AU") .build(), CartItem.builder() .name("GoPro HD") .quantity(2L) .unitAmount(1299L) .discountAmount(0L) .taxAmount(0L) .externalIdentifier("goprohd") .sku("GPHD1078") .productUrl("https://example.com/catalog/go-pro-hd") .imageUrl("https://example.com/images/go-pro-hd.jpg") .categories(List.of( "camera", "travel", "gear")) .productType(ProductType.PHYSICAL) .sellerCountry("AU") .build())) .metadata(Map.ofEntries( Map.entry("cohort", "cohort-a"), Map.entry("order_id", "order-12345"))) .buyer(JsonNullable.of(null)) .build()) .call(); if (res.checkoutSession().isPresent()) { // handle response } } } ``` ```php PHP theme={"system"} declare(strict_types=1); require 'vendor/autoload.php'; use Brick\DateTime\LocalDate; use Gr4vy; use Gr4vy\Utils; // Init SDK $checkoutSessionCreate = new Gr4vy\CheckoutSessionCreate( cartItems: [ new Gr4vy\CartItem( name: 'GoPro HD', quantity: 2, unitAmount: 1299, discountAmount: 0, taxAmount: 0, externalIdentifier: 'goprohd', sku: 'GPHD1078', productUrl: 'https://example.com/catalog/go-pro-hd', imageUrl: 'https://example.com/images/go-pro-hd.jpg', categories: [ 'camera', 'travel', 'gear', ], productType: 'physical', sellerCountry: 'AU', ), new Gr4vy\CartItem( name: 'GoPro HD', quantity: 2, unitAmount: 1299, discountAmount: 0, taxAmount: 0, externalIdentifier: 'goprohd', sku: 'GPHD1078', productUrl: 'https://example.com/catalog/go-pro-hd', imageUrl: 'https://example.com/images/go-pro-hd.jpg', categories: [ 'camera', 'travel', 'gear', ], productType: 'physical', sellerCountry: 'AU', ), ], metadata: [ 'cohort' => 'cohort-a', 'order_id' => 'order-12345', ], buyer: new Gr4vy\GuestBuyer( displayName: 'John Doe', externalIdentifier: 'buyer-12345', billingDetails: new Gr4vy\BillingDetails( firstName: 'John', lastName: 'Doe', emailAddress: 'john@example.com', phoneNumber: '+1234567890', address: new Gr4vy\Address( city: 'San Jose', country: 'AU', postalCode: '94560', state: 'California', stateCode: 'US-CA', houseNumberOrName: '10', line1: 'Stafford Apartments', line2: '29th Street', organization: 'Gr4vy', ), taxId: new Gr4vy\TaxId( value: '12345678931', kind: 'my.frp', ), ), shippingDetails: new Gr4vy\ShippingDetailsCreate( firstName: 'John', lastName: 'Doe', emailAddress: 'john@example.com', phoneNumber: '+1234567890', address: null, ), ), ); $response = $sdk->checkoutSessions->create( checkoutSessionCreate: $checkoutSessionCreate ); if ($response->checkoutSession !== null) { // handle response } ``` ```python Python theme={"system"} from datetime import date from gr4vy import Gr4vy, models from gr4vy.utils import parse_datetime import os # Init SDK res = g_client.checkout_sessions.create(checkout_session_create=models.CheckoutSessionCreate( cart_items=[ models.CartItem( name="GoPro HD", quantity=2, unit_amount=1299, discount_amount=0, tax_amount=0, external_identifier="goprohd", sku="GPHD1078", product_url="https://example.com/catalog/go-pro-hd", image_url="https://example.com/images/go-pro-hd.jpg", categories=[ "camera", "travel", "gear", ], product_type="physical", seller_country="GB", ), models.CartItem( name="GoPro HD", quantity=2, unit_amount=1299, discount_amount=0, tax_amount=0, external_identifier="goprohd", sku="GPHD1078", product_url="https://example.com/catalog/go-pro-hd", image_url="https://example.com/images/go-pro-hd.jpg", categories=[ "camera", "travel", "gear", ], product_type="physical", seller_country="GB", ), models.CartItem( name="GoPro HD", quantity=2, unit_amount=1299, discount_amount=0, tax_amount=0, external_identifier="goprohd", sku="GPHD1078", product_url="https://example.com/catalog/go-pro-hd", image_url="https://example.com/images/go-pro-hd.jpg", categories=[ "camera", "travel", "gear", ], product_type="physical", seller_country="GB", ), ], metadata={ "cohort": "cohort-a", "order_id": "order-12345", }, buyer=models.GuestBuyer( display_name="John Doe", external_identifier="buyer-12345", billing_details=models.BillingDetails( first_name="John", last_name="Doe", email_address="john@example.com", phone_number="+1234567890", address=models.Address( city="San Jose", country="AU", postal_code="94560", state="California", state_code="US-CA", house_number_or_name="10", line1="Stafford Apartments", line2="29th Street", organization="Gr4vy", ), tax_id=models.TaxID( value="12345678931", kind="ar.cuit", ), ), shipping_details=models.ShippingDetailsCreate( first_name="John", last_name="Doe", email_address="john@example.com", phone_number="+1234567890", address=models.Address( city="San Jose", country="AU", postal_code="94560", state="California", state_code="US-CA", house_number_or_name="10", line1="Stafford Apartments", line2="29th Street", organization="Gr4vy", ), ), ), )) # Handle response print(res) ``` ```ts Typescript theme={"system"} const result = await gr4vy.checkoutSessions.create(); ``` > [!WARNING] > > **Checkout session expiry** > > Checkout sessions expire after 1 hour by default. Updating a checkout session does not extend the time. ## Loading token The token needs to be shared with the native app via a custom API. Creating a token per checkout and only using a token for a short amount of time is recommended. ## Summary In this step: * Created a signed JWT token using a server-side SDK. * Shared the JWT with your app # Vaulting card data Source: https://integration.wpay.com.au/guides/payments/native/vault Vaulting allows you to securely capture and store card details within a **Checkout Session** without handling 3DS authentication immediately. This is useful for simple payment flows or scenarios where you want to tokenize a card for later use. ## Installation The official Gr4vy mobile SDKs are available for both iOS and Android platforms. Add them to your project to get started. **Swift (iOS)** The official Gr4vy Swift SDK is available via Swift Package Manager. 1. In Xcode, select **File** -> **Add Packages** 2. Enter the repository URL: [https://github.com/gr4vy/gr4vy-swift](https://github.com/gr4vy/gr4vy-swift) 3. Select the version or branch you wish to use. **Kotlin (Android)** The official Gr4vy Kotlin SDK is available via Maven Central. Add it to your build.gradle file: ```kotlin theme={"system"} dependencies { implementation("com.gr4vy:gr4vy-kotlin:1.0.0") } ``` ## The vaulting flow **1. Initialize Checkout Session** Create a checkout session on your backend with the transaction details to store the vaulted card data. **2. Initialize the SDK** Initialize the SDK with your Gr4vy ID, then use your own native UI components to collect the card number, expiry date, and CVV. **3. Vault via SDK** Pass the collected card data and checkout session ID to the native SDK to securely vault the card without 3DS authentication. **4. Process Payment** Use the vaulted session ID on your backend to create a transaction or store the card for future use. ## Implementation ### 1. Initialize Checkout Session First, create a checkout session on your backend. This session stores the vaulted card data. ```csharp C# theme={"system"} using Gr4vy; using Gr4vy.Models.Components; var sdk = new Gr4vySDK( merchantAccountId: "default", bearerAuth: "" ); var checkoutSession = await sdk.CheckoutSessions.CreateAsync(); // Return the session ID to your mobile app var sessionId = checkoutSession.CheckoutSession.Id; ``` ```go Go theme={"system"} package main import( "context" gr4vygo "github.com/gr4vy/gr4vy-go" "os" "log" ) func main() { ctx := context.Background() s := gr4vygo.New( gr4vygo.WithMerchantAccountID("default"), gr4vygo.WithSecurity(os.Getenv("GR4VY_BEARER_AUTH")), ) checkoutSession, err := s.CheckoutSessions.Create(ctx, nil) if err != nil { log.Fatal(err) } if checkoutSession != nil { // Return the session ID to your mobile app log.Printf("Session ID: %s", checkoutSession.CheckoutSession.ID) } } ``` ```java Java theme={"system"} import com.gr4vy.sdk.Gr4vy; import com.gr4vy.sdk.models.operations.CreateCheckoutSessionResponse; import java.lang.Exception; public class Application { public static void main(String[] args) throws Exception { Gr4vy sdk = Gr4vy.builder() .merchantAccountId("default") .bearerAuth("") .build(); CreateCheckoutSessionResponse res = sdk.checkoutSessions().create() .call(); if (res.checkoutSession().isPresent()) { // Return the session ID to your mobile app String sessionId = res.checkoutSession().get().id(); System.out.println("Session ID: " + sessionId); } } } ``` ```php PHP theme={"system"} declare(strict_types=1); require 'vendor/autoload.php'; use Gr4vy; $sdk = Gr4vy\SDK::builder() ->setMerchantAccountId('default') ->setSecurity('') ->build(); $response = $sdk->checkoutSessions->create(); if ($response->checkoutSession !== null) { // Return the session ID to your mobile app echo "Session ID: " . $response->checkoutSession->id; } ``` ```python Python theme={"system"} from gr4vy import Gr4vy import os with Gr4vy( merchant_account_id="default", bearer_auth=os.getenv("GR4VY_BEARER_AUTH", ""), ) as g_client: # Create an empty checkout session checkout_session = g_client.checkout_sessions.create() # Return the session ID to your mobile app print(checkout_session.id) ``` ```ts TypeScript theme={"system"} import { Gr4vy } from '@gr4vy/sdk' const gr4vy = new Gr4vy({ gr4vyId: 'your-gr4vy-id', privateKey: 'your-private-key', environment: 'sandbox', }) async function createCheckoutSession() { const checkoutSession = await gr4vy.checkoutSessions.create() // Return the session ID to your mobile app console.log(checkoutSession.id) } createCheckoutSession() ``` Pass this session ID to your mobile app so it can be used in the next step. ### 2. Initialize the SDK In your mobile app, initialize the SDK with your Gr4vy ID and the correct environment. Then use your own UI components to collect the card number, expiry date, and CVV. ```swift Swift (iOS) theme={"system"} import Gr4vy let gr4vy = Gr4vy( gr4vyId: "your-gr4vy-id", environment: .sandbox ) ``` ```kotlin Kotlin (Android) theme={"system"} import com.gr4vy.sdk.Gr4vy import com.gr4vy.sdk.Gr4vyServer val gr4vy = Gr4vy( gr4vyId = "your-gr4vy-id", server = Gr4vyServer.SANDBOX ) ``` ### 3. Vault via SDK Use the `tokenize` method to send the card details to Gr4vy. In this basic flow, set the `authenticate` parameter to `false` to skip the 3DS process. ```swift Swift (iOS) theme={"system"} // Create card data let cardData = Gr4vyCardData( paymentMethod: .card(CardPaymentMethod( number: "4111111111111111", expirationDate: "12/25", securityCode: "123" )) ) // Tokenize card data into checkout session do { try await gr4vy.tokenize( checkoutSessionId: "session_123", cardData: cardData ) print("Payment method tokenization complete") } catch { print("Error tokenizing payment method: \(error)") } // Completion handler gr4vy.tokenize( checkoutSessionId: "session_123", cardData: cardData ) { result in switch result { case .success: print("Payment method tokenization complete") case .failure(let error): print("Error tokenizing payment method: \(error)") } } ``` ```kotlin Kotlin (Android) theme={"system"} // Create card data val cardData = Gr4vyCardData( paymentMethod = Gr4vyPaymentMethod.Card( number = "4111111111111111", expirationDate = "12/25", securityCode = "123" ) ) // Tokenize card data into checkout session (suspend function) lifecycleScope.launch { try { val response = gr4vy.tokenize( checkoutSessionId = "session_123", cardData = Gr4vyCheckoutSessionRequest(paymentMethod = cardData.paymentMethod) ) println("Payment method tokenized successfully: ${response.data.status}") } catch (error: Gr4vyError) { println("Error tokenizing payment method: $error") } } // Callback version gr4vy.tokenize( checkoutSessionId = "session_123", cardData = Gr4vyCheckoutSessionRequest(paymentMethod = cardData.paymentMethod) ) { result -> when { result.isSuccess -> { val response = result.getOrNull() println("Payment method tokenized successfully: ${response?.data?.status}") } result.isFailure -> { println("Error tokenizing payment method: ${result.exceptionOrNull()}") } } } ``` ### 4. Process payment Once the SDK returns a success result, your backend can use that `checkout_session_id` to create a transaction. Since 3DS was skipped, this can be processed as a standard non-3DS transaction. ```json theme={"system"} POST /transactions { "amount": 1800, "currency": "EUR", "country": "DE", "intent": "capture", "payment_method": { "method": "checkout-session", "id": "[checkout-session-id]" }, ... } ``` # Apple Pay on React Native Source: https://integration.wpay.com.au/guides/payments/react-native/apple-pay On iOS, Apple Pay can be easily integrated both with and without the SDK. In both offerings, sign up for an Apple Developer account and create an Apple merchant ID and processing certificate. | | [Using SDK][mobile] | [Without SDK][mobile-without] | | - | - | - | | Apple Developer account required? | Yes | Yes | | Apple merchant account required? | Yes | Yes | | Customization & Theming? | Limited | Full | | Domain association required? | No | No | | Integrated with Checkout Flow? | Yes | Optional | > [!NOTE] > > \[Apple Pay]\[apple-pay] is also available on the web. [mobile]: /guides/features/apple-pay/ios [mobile-without]: /guides/features/apple-pay/ios-without-sdk [apple-pay]: /guides/features/apple-pay # React Native events Source: https://integration.wpay.com.au/guides/payments/react-native/events The library uses event emitters to send specific events happening in the SDK back to your React Native app. The object `EmbedReactNativeEventEmitter` can be imported and used to listen to such events. ```js theme={"system"} import { EmbedReactNativeEventEmitter, Gr4vyEvent } from '@gr4vy/embed-react-native'; const onEvent = (event: Gr4vyEvent) => { const { name, data } = event console[name === 'generalError' ? 'error' : 'log'](name, data) } useEffect(() => { const onEventSubscription = EmbedReactNativeEventEmitter.addListener( 'onEvent', onEvent ) return () => { onEventSubscription.remove() } }, []) ``` ## Available events `transactionCreated` Returns data about the transaction object when the transaction was successfully created. ```json theme={"system"} { "name": "transactionCreated", "data": { "success": true, "transactionId": "8724fd24-5489-4a5d-90fd-0604df7d3b83", "status": "capture_succeeded", "paymentMethodId": "17d57b9a-408d-49b8-9a97-9db382593003", "approvalUrl": "https://example.com" } } ``` `transactionFailed` Returned when the transaction encounters an error. ```json theme={"system"} { "name": "transactionFailed", "data": { "success": false, "transactionId": "8724fd24-5489-4a5d-90fd-0604df7d3b83", "status": "authorization_failed", "paymentMethodId": "17d57b9a-408d-49b8-9a97-9db382593003" } } ``` `generalError` Returned when the SDK encounters an error. ```json theme={"system"} { "name": "generalError", "data": { "message": "Gr4vy Error: Failed to load" } } ``` `cancelled` Returned when the SDK gets closed. ```json theme={"system"} { "name": "cancelled", "data": { "message": "User cancelled" } } ``` `cardDetailsChanged` Returned when the card BIN changes in the form. It contains information on the inputted card, such as the BIN, card type and scheme. ```json theme={"system"} { "name": "cardDetailsChanged", "data": { "bin": "42424242", "scheme": "visa", "cardType": "debit" } } ``` # Google Pay on React Native Source: https://integration.wpay.com.au/guides/payments/react-native/google-pay On Android, Google Pay can be easily integrated both with and without the SDK. In both offerings, signing up for a Google Developer account is not required, reducing additional financial and maintenance costs. | | [Using SDK][mobile] | [Without SDK][mobile-without] | | - | - | - | | Google Developer account required? | No | No | | Customization & Theming? | Limited | Full | | Integrated with Checkout Flow? | Yes | Optional | > [!NOTE] > > \[Google Pay]\[google-pay] is also available on web. [mobile]: /guides/features/google-pay/mobile [mobile-without]: /guides/features/google-pay/mobile-without-sdk [google-pay]: /guides/features/google-pay # React Native options Source: https://integration.wpay.com.au/guides/payments/react-native/options The SDK can be configured in a few ways. | Field | Optional / Required | Description | | - | - | - | | `gr4vyId` | **`Required`** | The ID automatically sets the `apiHost` to `api..gr4vy.app` for production and `api.sandbox..gr4vy.app` and to `embed.sandbox..gr4vy.app` for the sandbox environment. | | `token` | **`Required`** | The server-side generated JWT token which is used to authenticate any of the API calls. | | `environment` | `Optional` | `.sandbox`, `.production`. Defaults to `.production`. When `.sandbox` is provided the URL contains `sandbox.GR4VY_ID`. | | `amount` | **`Required`** | The amount to authorize or capture in the specified `currency` only. | | `currency` | **`Required`** | A valid, active, 3-character `ISO 4217` currency code to authorize or capture the `amount` for. | | `country` | **`Required`** | A valid `ISO 3166` country code. | | `buyerId` | `Optional` | An optional ID for a buyer. The transaction is automatically associated with a buyer with that ID. If no buyer with this ID exists then it is ignored. | | `buyerExternalIdentifier` | `Optional` | An optional external ID for a buyer. The transaction is automatically associated to a buyer with that external ID. If no buyer with this external ID exists then it is ignored. This option is ignored if the `buyerId` is provided. | | `presentingViewController` | **`Required`** | The view controller that opens the payment sheet. | | `externalIdentifier` | `Optional` | An optional external identifier that can be supplied. This is automatically associated to any resource that opens the payment sheet and subsequently used to find a resource by that ID. | | `store` | `Optional` | `'ask'`, `'preselect'`, `true`, `false` - Explicitly store the payment method, ask the buyer or preselect it by default. Requires `buyerId` or `buyerExternalIdentifier`. | | `display` | `Optional` | `all`, `addOnly`, `storedOnly`, `supportsTokenization` - Filters the payment methods to show stored methods only, new payment methods only or methods that support tokenization. | | `intent` | `Optional` | `authorize`, `preferAuthorize`, `capture` - Defines the intent of this API call. This determines the desired initial state of the transaction. When used, `preferAuthorize` automatically switches to `capture` if the selected payment method doesn't support delayed capture. | | `metadata` | `Optional` | An optional dictionary of keys and values for transaction metadata. All values should be a string. | | `paymentSource` | `Optional` | `installment`, `recurring` - Can be used to signal that Embed is used to capture the first transaction for a subscription or an installment. When used, `store` is implied to be `true` and `display` is implied to be `supportsTokenization`. This means that payment options that do not support tokenization are automatically hidden. | | `cartItems` | `Optional` | An optional array of cart item objects, each object must define a `name`, `quantity`, and `unitAmount`. Other optional properties are `discountAmount`, `taxAmount`, `externalIdentifier`, `sku`, `productUrl`, `imageUrl`, `categories` and `productType`. | | `applePayMerchantId` | `Optional` | The Apple merchant ID to be used during Apple Pay transactions. | | `debugMode` | `Optional` | `true`, `false`. Defaults to `false`, this prints to the console. | | `theme` | `Optional` | Theme customization options [See Theming Options](/guides/payments/embed/theming#options). The SDK also contains an additional two properties within the `colors` object: `headerBackground` and `headerText`. These are used for the navigation background and foreground colors. | | `locale` | `Optional` | An optional locale, this consists of a `ISO 639 Language Code` followed by an optional `ISO 3166 Country Code`, for example `en`, `en-gb` or `pt-br`. | | `statementDescriptor` | `Optional` | An optional object with information about the purchase to construct the statement information the buyer sees in their bank statement. Please note support for these fields varies across payment service providers and underlying banks, so the system can only ensure a best effort approach for each supported platform. As an example, most platforms only support a concatenation of `name` and `description` fields, truncated to a length of 22 characters. The object can contain `name`, `description`, `phoneNumber`, `city`, and `url` keys, with string values. `phoneNumber` should be in E164 format. Avoiding characters outside the alphanumeric range and the dot (`.`) to ensure wide compatibility is recommended. | | `requireSecurityCode` | `Optional` | An optional boolean which forces security code to be prompted for stored card payments. | | `shippingDetailsId` | `Optional` | An optional unique identifier of a set of shipping details stored for the buyer. | | `merchantAccountId` | `Optional` | An optional merchant account ID. | | `connectionOptions` | `Optional` | An optional set of values passed to a connection when processing a transaction (see [New transaction](/reference/transactions/new-transaction)) | | `installmentCount` | `Optional` | An optional value that indicates the number of installments a buyer is required to make. | | `excludedMethods` | `Optional` | An optional list of payment methods to exclude. | | `approvalExpiresAt` | `Optional` | An optional `datetime` string (ISO 8601 format recommended) that sets the expiration date for transaction approval. If set to `null`, it is treated the same as if it were omitted. | > [!NOTE] > > The instance ID is the unique identifier for the deployment of the system and is included in every API call. > Together with the environment (sandbox or production) it is used to connect to the right APIs, as well as dashboard. > > The possible values for your ID are one of `wpay`, `wpay1`, `wpay2`, or `wpay3`. You are informed > in which instance your account has been set up. # Installing the React Native SDK Source: https://integration.wpay.com.au/guides/payments/react-native/quick-start/embed Next, you can embed the SDK in your React Native app to store card details, authorize payments, and capture a transaction. ## Add the SDK The [React Native SDK](https://github.com/gr4vy/gr4vy-react-native) is available as an NPM package. ```sh theme={"system"} yarn add @gr4vy/embed-react-native # OR npm install @gr4vy/embed-react-native ``` This installs the SDK, as well as the native iOS and Android dependencies. # Creating an API key for use with React Native Source: https://integration.wpay.com.au/guides/payments/react-native/quick-start/key Before the app can communicate with the API, create a new API key. The API handles authentication using a signed **JSON Web Token** (JWT) which is passed in an HTTP header. ```bash theme={"system"} curl -X GET https://api.example.gr4vy.app/transactions \ -H "authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIi..." ``` > [!TIP] > > Using an SDK is recommended. SDKs make the process of generating a token a lot easier and > are available in many popular programming languages. This is covered in the > next step. ## Create a new API key To use the API, generate a new **API key**. Head over to the dashboard and visit the **Integrations** page. ![API key dashboard](https://mintcdn.com/wpay/c50rx3S48hHhNNxb/assets/images/api-keys/add-key.png?fit=max\&auto=format\&n=c50rx3S48hHhNNxb\&q=85\&s=ffbfb085954c55e845050e44a7c9e73e) On this page, click the **Add API key** button and select a name for the key. The name is purely to track what key is for what integration. The downloaded key needs to be stored securely as it is not stored by the system. > [!NOTE] > > Learn more about API keys and JWTs in the > [in-depth guide on authentication](/guides/api/authentication). ## Summary In this step: * Learned about API authentication. * Created a new API key for use with the API. * Stored this API key in a secure place where the app can access it. # React Native SDK Source: https://integration.wpay.com.au/guides/payments/react-native/quick-start/overview Get set up with the React Native mobile SDK for your checkout experience. ![Embed](https://mintcdn.com/wpay/RxgiaZJ07iWfuxu1/assets/images/react-native/hero.png?fit=max\&auto=format\&n=RxgiaZJ07iWfuxu1\&q=85\&s=695292c33a9962e2355e1d739d94ca42) In this quick start guide, you are taken through the following steps. 1. [Create an API key](/guides/payments/react-native/quick-start/key) which is needed to generate the signed JWT token in step 2. 2. [Set up a server-side SDK](/guides/payments/react-native/quick-start/sdks) to generate a client token for authenticating to the API. 3. [Install the React Native SDK](/guides/payments/react-native/quick-start/embed) which handles the capture of the payment method details and process the payment. 4. [Process the transaction](/guides/payments/react-native/quick-start/transact) and inspect the result. \` # Generating tokens for authentication Source: https://integration.wpay.com.au/guides/payments/react-native/quick-start/sdks Before adding the SDK to the React Native app, create a client token that can be used to authenticate the transaction. ## Install a server-side SDK Use the package manager in the preferred programming language to install the server-side SDK. Token generation can only be done server side and doing this client side is not recommended as it exposes the API key to customers. ```sh C# theme={"system"} dotnet add package Gr4vy ``` ```sh Go theme={"system"} go get github.com/gr4vy/gr4vy-go ``` ```sh Java theme={"system"} # Please check for the latest version implementation 'com.gr4vy:sdk:1.0.0' ``` ```sh PHP theme={"system"} composer require "gr4vy/gr4vy-php" ``` ```sh Python theme={"system"} pip install gr4vy ``` ```sh TypeScript theme={"system"} npm install @gr4vy/sdk # or: yarn add @gr4vy/sdk ``` > [!NOTE] > > Please always check and install the [latest release](/guides/get-started#server-side-sdks) of the preferred SDK. ## Initialize the SDK client Next, initialize the SDK with the ID of the instance and the private key. ```csharp C# theme={"system"} using Gr4vy; using Gr4vy.Models.Components; using System.Collections.Generic; // Loaded the key from a file, env variable, // or anywhere else var privateKey = "..."; var sdk = new Gr4vySDK( id: "example", server: SDKConfig.Server.Sandbox, bearerAuthSource: Auth.WithToken(privateKey), merchantAccountId: "default" ); ``` ```go Go theme={"system"} package main import ( "context" gr4vy "github.com/gr4vy/gr4vy-go" "github.com/gr4vy/gr4vy-go/models/operations" "log" "os" ) func main() { ctx := context.Background() privateKey := "...." // Private key loaded from disk or env var withToken := gr4vy.WithToken(privateKey, []JWTScope{ReadAll, WriteAll}, 60) s := gr4vy.New( gr4vy.WithID("example"), gr4vy.WithServer(gr4vy.ServerSandbox), gr4vy.WithSecuritySource(withToken), gr4vy.WithMerchantAccountID("default"), ) } ``` ```java Java theme={"system"} package hello.world; import com.gr4vy.sdk.BearerSecuritySource; import com.gr4vy.sdk.Gr4vy; import com.gr4vy.sdk.Gr4vy.AvailableServers; import com.gr4vy.sdk.models.components.AccountUpdaterJobCreate; import com.gr4vy.sdk.models.errors.*; import com.gr4vy.sdk.models.operations.ListTransactionsRequest; import java.lang.Exception; import java.util.List; public class Application { public static void main(String[] args) throws Exception { String privateKey = "-----BEGIN PRIVATE KEY-----\n...."; // a valid private key Gr4vy sdk = Gr4vy.builder() .id("example") .server(AvailableServers.SANDBOX) .merchantAccountId("default") .securitySource(new BearerSecuritySource.Builder(privateKey).build()) .build(); } } ``` ```php PHP theme={"system"} declare(strict_types=1); require 'vendor/autoload.php'; use Gr4vy; use Gr4vy\Auth; // Loaded the key from a file, env variable, // or anywhere else $privateKey = "..."; $sdk = Gr4vy\SDK::builder() ->setId('example') ->setServer('sandbox') ->setSecuritySource(Auth::withToken($privateKey)) ->setMerchantAccountId('default') ->build(); ``` ```python Python theme={"system"} from gr4vy import Gr4vy, auth import os client = Gr4vy( id="example", server="production", merchant_account_id="default", bearer_auth=auth.with_token(open("./private_key.pem").read()) ) ``` ```ts TypeScript theme={"system"} import fs from "fs"; import { Gr4vy, withToken } from "@gr4vy/sdk"; const gr4vy = new Gr4vy({ server: "sandbox", id: "example", bearerAuth: withToken({ privateKey: fs.readFileSync("private_key.pem", "utf8"), }), }); ``` > [!NOTE] > > The instance ID is the unique identifier for the deployment of the system and is included in every API call. > Together with the environment (sandbox or production) it is used to connect to the right APIs, as well as dashboard. > > The possible values for your ID are one of `wpay`, `wpay1`, `wpay2`, or `wpay3`. You are informed > in which instance your account has been set up. ## Generate a token The next step is to create a new token for frontend authentication. The SDK comes with a helper function for exactly that purpose. It requires an amount and currency for the transaction to be created and additionally accepts either the ID of a buyer or an external identifier for a buyer to link the transaction to. For Embed, it is recommended to attach a checkout session to every transaction. The `getEmbedTokenWithCheckoutSession` helper creates a checkout session using your SDK client and returns an Embed token with the resulting `checkout_session_id` already pinned, in a single call. ```csharp C# theme={"system"} using System.Collections.Generic; using Gr4vy; // Loaded the key from a file, env variable, // or anywhere else var privateKey = "..."; var sdk = new Gr4vySDK( id: "example", server: SDKConfig.Server.Sandbox, bearerAuthSource: Auth.WithToken(privateKey), merchantAccountId: "default" ); var token = await Auth.GetEmbedTokenWithCheckoutSession( client: sdk, privateKey: privateKey, embedParams: new Dictionary { ["amount"] = 1299, ["currency"] = "AUD", ["buyer_external_identifier"] = "user-1234", } ); ``` ```go Go theme={"system"} package main import ( "context" gr4vy "github.com/gr4vy/gr4vy-go" "log" ) func main() { privateKey := "...." // Private key loaded from disk or env var client := gr4vy.New( gr4vy.WithID("example"), gr4vy.WithServer(gr4vy.ServerSandbox), gr4vy.WithSecuritySource(gr4vy.WithToken(privateKey, nil, 3600)), gr4vy.WithMerchantAccountID("default"), ) embedParams := map[string]interface{}{ "amount": 1299, "currency": "AUD", "buyer_external_identifier": "user-1234", } token, err := gr4vy.GetEmbedTokenWithCheckoutSession(context.Background(), client, privateKey, embedParams, nil, nil) if err != nil { log.Fatal(err) } log.Println(token) } ``` ```java Java theme={"system"} package hello.world; import com.gr4vy.sdk.Auth; import com.gr4vy.sdk.BearerSecuritySource; import com.gr4vy.sdk.Gr4vy; import com.gr4vy.sdk.Gr4vy.AvailableServers; import java.util.HashMap; import java.util.Map; public class Application { public static void main(String[] args) throws Exception { String privateKey = "-----BEGIN PRIVATE KEY-----\n...."; // a valid private key Gr4vy client = Gr4vy.builder() .id("example") .server(AvailableServers.SANDBOX) .securitySource(new BearerSecuritySource.Builder(privateKey).build()) .merchantAccountId("default") .build(); Map embedParams = new HashMap<>(); embedParams.put("amount", 1299); embedParams.put("currency", "AUD"); embedParams.put("buyer_external_identifier", "user-1234"); String token = Auth.getEmbedTokenWithCheckoutSession(client, privateKey, embedParams); System.out.println(token); } } ``` ```php PHP theme={"system"} declare(strict_types=1); require 'vendor/autoload.php'; use Gr4vy; use Gr4vy\Auth; // Loaded the key from a file, env variable, // or anywhere else $privateKey = "..."; $sdk = Gr4vy\SDK::builder() ->setId('example') ->setServer('sandbox') ->setSecuritySource(Auth::withToken($privateKey)) ->setMerchantAccountId('default') ->build(); $token = Auth::getEmbedTokenWithCheckoutSession( client: $sdk, privateKey: $privateKey, embedParams: [ "amount" => 1299, "currency" => "AUD", "buyer_external_identifier" => "user-1234", ], ); ``` ```python Python theme={"system"} from gr4vy import Gr4vy, auth private_key = open("./private_key.pem").read() g_client = Gr4vy( id="example", server="production", merchant_account_id="default", bearer_auth=auth.with_token(private_key) ) token = auth.get_embed_token_with_checkout_session( g_client, private_key, embed_params={ "amount": 1299, "currency": "AUD", "buyer_external_identifier": "user-1234", }, ) ``` ```ts TypeScript theme={"system"} import { Gr4vy, getEmbedTokenWithCheckoutSession, withToken } from "@gr4vy/sdk"; import fs from "fs"; async function run() { const privateKey = fs.readFileSync("private_key.pem", "utf8") const gr4vy = new Gr4vy({ server: "sandbox", id: "example", bearerAuth: withToken({ privateKey }), }); const token = await getEmbedTokenWithCheckoutSession({ client: gr4vy, privateKey, embedParams: { amount: 1299, currency: 'AUD', buyerExternalIdentifier: 'user-1234', } }); console.log(token); } run(); ``` This token can now be served to the frontend where it is used by the SDK. If generating the client token without using a server-side SDK is preferred, then please read the guide on [manually generating a JWT](/guides/api/authentication#without-sdks). ## Checkout sessions Passing a [checkout session](/reference/checkout-sessions/new-checkout-session) ID to the Embed token function is recommended. This ID can be used to help identify multiple transaction attempts. This is useful to identify if one or more failed/declined transactions eventually result in a successful transaction. Checkout sessions can also store `cart_items`, `metadata`, airline data, and more, removing the need to pass these values on the transaction request. ```csharp C# theme={"system"} using Gr4vy; using Gr4vy.Models.Components; using NodaTime; using System; using System.Collections.Generic; var res = await sdk.CheckoutSessions.CreateAsync(checkoutSessionCreate: new CheckoutSessionCreate() { CartItems = new List() { new CartItem() { Name = "GoPro HD", Quantity = 2, UnitAmount = 1299, DiscountAmount = 0, TaxAmount = 0, ExternalIdentifier = "goprohd", Sku = "GPHD1078", ProductUrl = "https://example.com/catalog/go-pro-hd", ImageUrl = "https://example.com/images/go-pro-hd.jpg", Categories = new List() { "camera", "travel", "gear", }, ProductType = "physical", SellerCountry = "GB", }, new CartItem() { Name = "GoPro HD", Quantity = 2, UnitAmount = 1299, DiscountAmount = 0, TaxAmount = 0, ExternalIdentifier = "goprohd", Sku = "GPHD1078", ProductUrl = "https://example.com/catalog/go-pro-hd", ImageUrl = "https://example.com/images/go-pro-hd.jpg", Categories = new List() { "camera", "travel", "gear", }, ProductType = "physical", SellerCountry = "GB", }, new CartItem() { Name = "GoPro HD", Quantity = 2, UnitAmount = 1299, DiscountAmount = 0, TaxAmount = 0, ExternalIdentifier = "goprohd", Sku = "GPHD1078", ProductUrl = "https://example.com/catalog/go-pro-hd", ImageUrl = "https://example.com/images/go-pro-hd.jpg", Categories = new List() { "camera", "travel", "gear", }, ProductType = "physical", SellerCountry = "AU", }, }, Metadata = new Dictionary() { { "cohort", "cohort-a" }, { "order_id", "order-12345" }, }, Buyer = new GuestBuyer() { DisplayName = "John Doe", ExternalIdentifier = "buyer-12345", BillingDetails = new BillingDetails() { FirstName = "John", LastName = "Doe", EmailAddress = "john@example.com", PhoneNumber = "+1234567890", Address = new Address() { City = "San Jose", Country = "AU", PostalCode = "94560", State = "California", StateCode = "US-CA", HouseNumberOrName = "10", Line1 = "Stafford Apartments", Line2 = "29th Street", Organization = "Gr4vy", }, TaxId = new TaxId() { Value = "12345678931", Kind = "ar.cuit", }, }, ShippingDetails = new ShippingDetailsCreate() { FirstName = "John", LastName = "Doe", EmailAddress = "john@example.com", PhoneNumber = "+1234567890", Address = new Address() { City = "San Jose", Country = "AU", PostalCode = "94560", State = "California", StateCode = "US-CA", HouseNumberOrName = "10", Line1 = "Stafford Apartments", Line2 = "29th Street", Organization = "Gr4vy", }, }, }, }); // handle response ``` ```go Go theme={"system"} package main import( "context" gr4vygo "github.com/gr4vy/gr4vy-go" "os" "github.com/gr4vy/gr4vy-go/models/components" "github.com/gr4vy/gr4vy-go/types" "log" ) func main() { ctx := context.Background() // Init SDK res, err := s.CheckoutSessions.Create(ctx, &components.CheckoutSessionCreate{ CartItems: []components.CartItem{ components.CartItem{ Name: "GoPro HD", Quantity: 2, UnitAmount: 1299, DiscountAmount: gr4vygo.Int64(0), TaxAmount: gr4vygo.Int64(0), ExternalIdentifier: gr4vygo.String("goprohd"), Sku: gr4vygo.String("GPHD1078"), ProductURL: gr4vygo.String("https://example.com/catalog/go-pro-hd"), ImageURL: gr4vygo.String("https://example.com/images/go-pro-hd.jpg"), Categories: []string{ "camera", "travel", "gear", }, ProductType: components.ProductTypePhysical.ToPointer(), SellerCountry: gr4vygo.String("AU"), }, components.CartItem{ Name: "GoPro HD", Quantity: 2, UnitAmount: 1299, DiscountAmount: gr4vygo.Int64(0), TaxAmount: gr4vygo.Int64(0), ExternalIdentifier: gr4vygo.String("goprohd"), Sku: gr4vygo.String("GPHD1078"), ProductURL: gr4vygo.String("https://example.com/catalog/go-pro-hd"), ImageURL: gr4vygo.String("https://example.com/images/go-pro-hd.jpg"), Categories: []string{ "camera", "travel", "gear", }, ProductType: components.ProductTypePhysical.ToPointer(), SellerCountry: gr4vygo.String("AU"), }, }, Metadata: map[string]string{ "cohort": "cohort-a", "order_id": "order-12345", }, Buyer: nil, }) if err != nil { log.Fatal(err) } if res != nil { // handle response } } ``` ```java Java theme={"system"} package hello.world; import com.gr4vy.sdk.Gr4vy; import com.gr4vy.sdk.models.components.*; import com.gr4vy.sdk.models.errors.*; import com.gr4vy.sdk.models.operations.CreateCheckoutSessionResponse; import java.lang.Exception; import java.time.LocalDate; import java.time.OffsetDateTime; import java.util.List; import java.util.Map; import org.openapitools.jackson.nullable.JsonNullable; public class Application { public static void main(String[] args) throws Exception { // Init SDK CreateCheckoutSessionResponse res = sdk.checkoutSessions().create() .checkoutSessionCreate(CheckoutSessionCreate.builder() .cartItems(List.of( CartItem.builder() .name("GoPro HD") .quantity(2L) .unitAmount(1299L) .discountAmount(0L) .taxAmount(0L) .externalIdentifier("goprohd") .sku("GPHD1078") .productUrl("https://example.com/catalog/go-pro-hd") .imageUrl("https://example.com/images/go-pro-hd.jpg") .categories(List.of( "camera", "travel", "gear")) .productType(ProductType.PHYSICAL) .sellerCountry("AU") .build(), CartItem.builder() .name("GoPro HD") .quantity(2L) .unitAmount(1299L) .discountAmount(0L) .taxAmount(0L) .externalIdentifier("goprohd") .sku("GPHD1078") .productUrl("https://example.com/catalog/go-pro-hd") .imageUrl("https://example.com/images/go-pro-hd.jpg") .categories(List.of( "camera", "travel", "gear")) .productType(ProductType.PHYSICAL) .sellerCountry("AU") .build())) .metadata(Map.ofEntries( Map.entry("cohort", "cohort-a"), Map.entry("order_id", "order-12345"))) .buyer(JsonNullable.of(null)) .build()) .call(); if (res.checkoutSession().isPresent()) { // handle response } } } ``` ```php PHP theme={"system"} declare(strict_types=1); require 'vendor/autoload.php'; use Brick\DateTime\LocalDate; use Gr4vy; use Gr4vy\Utils; // Init SDK $checkoutSessionCreate = new Gr4vy\CheckoutSessionCreate( cartItems: [ new Gr4vy\CartItem( name: 'GoPro HD', quantity: 2, unitAmount: 1299, discountAmount: 0, taxAmount: 0, externalIdentifier: 'goprohd', sku: 'GPHD1078', productUrl: 'https://example.com/catalog/go-pro-hd', imageUrl: 'https://example.com/images/go-pro-hd.jpg', categories: [ 'camera', 'travel', 'gear', ], productType: 'physical', sellerCountry: 'AU', ), new Gr4vy\CartItem( name: 'GoPro HD', quantity: 2, unitAmount: 1299, discountAmount: 0, taxAmount: 0, externalIdentifier: 'goprohd', sku: 'GPHD1078', productUrl: 'https://example.com/catalog/go-pro-hd', imageUrl: 'https://example.com/images/go-pro-hd.jpg', categories: [ 'camera', 'travel', 'gear', ], productType: 'physical', sellerCountry: 'AU', ), ], metadata: [ 'cohort' => 'cohort-a', 'order_id' => 'order-12345', ], buyer: new Gr4vy\GuestBuyer( displayName: 'John Doe', externalIdentifier: 'buyer-12345', billingDetails: new Gr4vy\BillingDetails( firstName: 'John', lastName: 'Doe', emailAddress: 'john@example.com', phoneNumber: '+1234567890', address: new Gr4vy\Address( city: 'San Jose', country: 'AU', postalCode: '94560', state: 'California', stateCode: 'US-CA', houseNumberOrName: '10', line1: 'Stafford Apartments', line2: '29th Street', organization: 'Gr4vy', ), taxId: new Gr4vy\TaxId( value: '12345678931', kind: 'my.frp', ), ), shippingDetails: new Gr4vy\ShippingDetailsCreate( firstName: 'John', lastName: 'Doe', emailAddress: 'john@example.com', phoneNumber: '+1234567890', address: null, ), ), ); $response = $sdk->checkoutSessions->create( checkoutSessionCreate: $checkoutSessionCreate ); if ($response->checkoutSession !== null) { // handle response } ``` ```python Python theme={"system"} from datetime import date from gr4vy import Gr4vy, models from gr4vy.utils import parse_datetime import os # Init SDK res = g_client.checkout_sessions.create(checkout_session_create=models.CheckoutSessionCreate( cart_items=[ models.CartItem( name="GoPro HD", quantity=2, unit_amount=1299, discount_amount=0, tax_amount=0, external_identifier="goprohd", sku="GPHD1078", product_url="https://example.com/catalog/go-pro-hd", image_url="https://example.com/images/go-pro-hd.jpg", categories=[ "camera", "travel", "gear", ], product_type="physical", seller_country="GB", ), models.CartItem( name="GoPro HD", quantity=2, unit_amount=1299, discount_amount=0, tax_amount=0, external_identifier="goprohd", sku="GPHD1078", product_url="https://example.com/catalog/go-pro-hd", image_url="https://example.com/images/go-pro-hd.jpg", categories=[ "camera", "travel", "gear", ], product_type="physical", seller_country="GB", ), models.CartItem( name="GoPro HD", quantity=2, unit_amount=1299, discount_amount=0, tax_amount=0, external_identifier="goprohd", sku="GPHD1078", product_url="https://example.com/catalog/go-pro-hd", image_url="https://example.com/images/go-pro-hd.jpg", categories=[ "camera", "travel", "gear", ], product_type="physical", seller_country="GB", ), ], metadata={ "cohort": "cohort-a", "order_id": "order-12345", }, buyer=models.GuestBuyer( display_name="John Doe", external_identifier="buyer-12345", billing_details=models.BillingDetails( first_name="John", last_name="Doe", email_address="john@example.com", phone_number="+1234567890", address=models.Address( city="San Jose", country="AU", postal_code="94560", state="California", state_code="US-CA", house_number_or_name="10", line1="Stafford Apartments", line2="29th Street", organization="Gr4vy", ), tax_id=models.TaxID( value="12345678931", kind="ar.cuit", ), ), shipping_details=models.ShippingDetailsCreate( first_name="John", last_name="Doe", email_address="john@example.com", phone_number="+1234567890", address=models.Address( city="San Jose", country="AU", postal_code="94560", state="California", state_code="US-CA", house_number_or_name="10", line1="Stafford Apartments", line2="29th Street", organization="Gr4vy", ), ), ), )) # Handle response print(res) ``` ```ts Typescript theme={"system"} const result = await gr4vy.checkoutSessions.create(); ``` > [!WARNING] > > **Checkout session expiry** > > Checkout sessions expire after 1 hour by default. Updating a checkout session does not extend the time. ## Loading token The token needs to be shared with the React Native app via a custom API. Creating a token per checkout and only using a token for a short amount of time is recommended. ## Summary In this step: * Created a signed JWT token using a server-side SDK. * Shared the JWT with the React Native app # Creating transactions Source: https://integration.wpay.com.au/guides/payments/react-native/quick-start/transact With the SDK installed, it's time to create a transaction. ## Initialize the SDK To initialize the SDK, call the `.showPaymentSheet()` method on the SDK to open the payment sheet to prompt the buyer to pay. ```js theme={"system"} import EmbedReactNative from '@gr4vy/embed-react-native'; const handleCheckout = () => { EmbedReactNative.showPaymentSheet({ gr4vyId: '[GR4VY_ID]', environment: 'sandbox', token: '[TOKEN]', amount: 1299, currency: 'AUD', country: 'AU', ... }) } ``` > [!NOTE] > > Replace the `[GR4VY_ID]` with your instance identifier and `[TOKEN]` with the JWT created in step 2. > Additional [options](/guides/payments/react-native/options) can be provided when launching the SDK. > [!NOTE] > > The instance ID is the unique identifier for the deployment of the system and is included in every API call. > Together with the environment (sandbox or production) it is used to connect to the right APIs, as well as dashboard. > > The possible values for your ID are one of `wpay`, `wpay1`, `wpay2`, or `wpay3`. You are informed > in which instance your account has been set up. ## Handle events Once launched, the SDK sends events when a transaction was created and for some failures. To handle these events, add a new event listener to the SDK. ```js theme={"system"} import { EmbedReactNativeEventEmitter, Gr4vyEvent } from '@gr4vy/embed-react-native'; const onEvent = (event: Gr4vyEvent) => { const { name, data } = event console[name === 'generalError' ? 'error' : 'log'](name, data) } useEffect(() => { const onEventSubscription = EmbedReactNativeEventEmitter.addListener( 'onEvent', onEvent ) return () => { onEventSubscription.remove() } }, []) ``` > [!NOTE] > > Learn more about the events triggered by the React Native SDK in the [events](/guides/payments/react-native/events) guide. # Secure Fields events Source: https://integration.wpay.com.au/guides/payments/secure-fields/events You can call the `addEventListener` on a Secure Fields instance or on fields, passing one of the supported events and a callback. `removeEventListener` removes a previously attached event handler. ```js theme={"system"} const cardNumberField = secureFields.addCardNumberField() const handleInputChange = (data) => { console.warn('Input changed', data) } cardNumberField.addEventListener('input', handleInputChange) ... cardNumberField.removeEventListener('input', handleInputChange) ``` ## Global events The following events can be listened to by attaching an event handler to the `SecureFields` instance using the `addEventListener` method. | Name | Description | | - | - | | `CARD_VAULT_SUCCESS` | Triggered when the card is successfully vaulted. | | `CARD_VAULT_FAILURE` | Triggered when the card vaulting fails. The data includes the API status and additional data about the failure. | | `READY` | Triggered when Secure Fields is loaded and ready to be used. An object is available as an argument in the callback, containing: - `environment` (sandbox or production) - `gr4vyId` - `sessionId` - `version` (of the Secure Fields SDK in use) | | `FORM_CHANGE` | Triggered when any value of the card form changes. An object is available as an argument in the callback, containing: - `fields` (object of fields, each with validation information) - `complete` (a form is complete if number and expiry date are filled and valid and security code is empty or valid) | ```js theme={"system"} secureFields.addEventListener(SecureFields.Events.CARD_VAULT_SUCCESS, () => { console.log("Card has been vaulted successfully!"); }); secureFields.addEventListener(SecureFields.Events.CARD_VAULT_SUCCESS, ({ scheme }) => { console.log(`Card (${scheme}) has been vaulted successfully!`); }); secureFields.addEventListener( SecureFields.Events.CARD_VAULT_FAILURE, ({ code, message, status }) => { console.log("Couldn't vault the card", { code, status, message }); } ); secureFields.addEventListener(SecureFields.Events.READY, (data) => { console.log("Secure fields loaded", data); }); ``` ## Field events The following events can be listened to by attaching an event handler to a field (returned by the `addCardNumberField`, `addExpiryDateField`, `addSecurityCodeField` and `addField` methods) using the `addEventListener` method. Some of these provide additional useful data like the card BIN, validation status, and scheme. For example, the input event on a card number field might include `{ schema: 'visa', codeLabel: 'CVV', valid: true, ... }`. | Name | Description | | - | - | | `blur` | Triggered when the field loses focus. | | `focus` | Triggered when the field gains focus. | | `input` | Triggered when the field value has been changed. | | `redacted` | Triggered when the field value has been masked (either automatically via `maskInput` or programmatically via `redactValue()`). | | `unredacted` | Triggered when the field value has been unmasked (either automatically via `maskInput` or programmatically via `unredactValue()`). | | `card-details-changed` | Triggered when the API confirms card details for the entered BIN. Includes the confirmed scheme, additional schemes (co-branded networks), the issuing country (`country`, a 2-letter ISO code, in version 2.11.0 or later), and other card metadata. The `country` value is `null` when the issuing country cannot be resolved, and can take more digits to resolve than the scheme does. Only fires on the card number field. | ```js theme={"system"} cardNumberField.addEventListener('blur', (data) => { console.log(data) /* { id: 'number' } */ }) cardNumberField.addEventListener('focus', (data) => { console.log(data) /* { id: 'number' } */ }) cardNumberField.addEventListener('input', (data) => { console.log(data) /* { id: 'number', schema: 'visa', codeLabel: 'CVV', valid: true } */ }) cardNumberField.addEventListener('redacted', () => { console.log('Field redacted') }) cardNumberField.addEventListener('unredacted', () => { console.log('Field unredacted') }) cardNumberField.addEventListener('card-details-changed', (data) => { console.log(data) /* { id: 'number', scheme: 'visa', country: 'AU', additionalSchemes: ['mastercard'], ... } */ }) ``` ### Reject specific card schemes Secure Fields doesn't block card schemes for you. To refuse cards from a scheme you don't accept, check the `scheme` in the `card-details-changed` event, show your own error message, and don't call `submit()` while that scheme is entered. ```js theme={"system"} const blockedSchemes = ['jcb', 'unionpay']; let schemeBlocked = false; cardNumberField.addEventListener('card-details-changed', ({ scheme }) => { schemeBlocked = blockedSchemes.includes(scheme); document.querySelector('#card-error').textContent = schemeBlocked ? 'This card type isn\'t accepted.' : ''; }); document.querySelector('#cc-button').addEventListener('click', () => { if (!schemeBlocked) secureFields.submit(); }); ``` # Adding secure fields Source: https://integration.wpay.com.au/guides/payments/secure-fields/quick-start/embed The next step to is to add **Secure Fields** to your checkout experience. ## Install Secure Fields There are a few ways to install **Secure Fields**, either as a React component, Node library, or straight off the CDN. ```bash React theme={"system"} npm install @gr4vy/secure-fields-react --save # or: yarn add @gr4vy/secure-fields-react ``` ```bash Node theme={"system"} npm install @gr4vy/secure-fields --save # or: yarn add @gr4vy/secure-fields ``` ```html CDN theme={"system"} ``` > [!NOTE] > > When using the CDN the latest version of the library is always pulled straight > from the server for every request. ### Content Security Policy If your checkout page sets a Content Security Policy (CSP), allow the Gr4vy hosts that Secure Fields uses. Each host is on your instance's domain, for example `secure-fields.{instance_id}.gr4vy.app` in production and `secure-fields.sandbox.{instance_id}.gr4vy.app` in sandbox. | Directive | Host | Used for | | - | - | - | | `frame-src` | `secure-fields.{instance_id}.gr4vy.app` | The iframes that hold each card field. | | `frame-src` | `click-to-pay.{instance_id}.gr4vy.app` | The Click to Pay iframe, if you use Click to Pay. | | `connect-src` | `api.{instance_id}.gr4vy.app` | Click to Pay requests, if you use Click to Pay. | | `img-src` | `cdn.{instance_id}.gr4vy.app` | Card scheme icons, if you show them. | | `script-src`, `style-src` | `cdn.{instance_id}.gr4vy.app` | The library, if you load it from the CDN. | The wildcard `https://*.gr4vy.app` covers all of these hosts in both environments. If you only set `default-src`, add the hosts there instead. ## Initializing secure fields With **Secure Fields** installed it's now possible to initialize the connection to your instance. **Secure Fields** expects the ID of your instance (`gr4vyId`), the environment, and a checkout session ID. > [!NOTE] > > The instance ID is the unique identifier for the deployment of the system and is included in every API call. > Together with the environment (sandbox or production) it is used to connect to the right APIs, as well as dashboard. > > The possible values for your ID are one of `wpay`, `wpay1`, `wpay2`, or `wpay3`. You are informed > in which instance your account has been set up. ```jsx React theme={"system"} import '@gr4vy/secure-fields-react/lib/style.css' import { SecureFields } from "@gr4vy/secure-fields-react"; const App = () => { return ( console.warn("Secure Fields loaded", data)} onCardVaultSuccess={() => console.warn("Card has been tokenized successfully!") } onCardVaultFailure={() => console.error("Failed to tokenize card data")} > {/* Form to be added next */}
); }; ``` ```js Node theme={"system"} import SecureFields from "@gr4vy/secure-fields"; // set up the environment const gr4vyId = "example"; const environment = "sandbox"; const sessionId = "..."; // created in step 2 // initialize the SDK const secureFields = new SecureFields({ gr4vyId, environment, sessionId, }); // enable debugging secureFields.setDebug(true); ``` ```js CDN theme={"system"} // set up the environment const gr4vyId = "example"; const environment = "sandbox"; const sessionId = "..."; // initialize the SDK const secureFields = new SecureFields({ gr4vyId, environment, sessionId, }); // enable debugging secureFields.setDebug(true); ``` ## Adding each of the individual fields Next, add any of the 3 individual fields to the form. Mix and match these with custom forms, and add custom labels. ```jsx React theme={"system"} import { CardNumber, ExpiryDate, SecurityCode, } from '@gr4vy/secure-fields-react' const Form = () => { return ( // Custom fields can be added as well as secure fields // These are secure fields ) } ``` ```html Node theme={"system"} {/* You can add your own fields as well as secure fields */} {/* These are secure fields */} ``` ```html CDN theme={"system"} {/* You can add your own fields as well as secure fields */} {/* These are secure fields */} ``` > [!NOTE] > > When not using the React SDK, every field Embed needs to be attached to an HTML element. > In this case, the fields were attached to `` fields. The fields can be attached to > any element using a `querySelector`-compatible query. Other kinds of information can also be collected along with the card data. Postal code is currently supported as an additional field. ```jsx React theme={"system"} import { Field } from '@gr4vy/secure-fields-react' ``` ```html Node theme={"system"} ``` ```html CDN theme={"system"} ``` Each secure field should now be loaded on the page. Please see the more extensive guides on how to add [event listeners](/guides/payments/secure-fields/events) and custom [styles](/guides/payments/secure-fields/theming) to **Secure Fields**. ## Vaulting the card data **Secure Fields** handles the PCI-compliant submission of all the card data to the servers. Once the form is ready, use the submit feature of **Secure Fields** to vault the data. A callback notifies you when the data has been submitted successfully. ```jsx React theme={"system"} import { useSecureFields } from "@gr4vy/secure-fields-react"; const Form = () => { const { secureFields } = useSecureFields(); const onClick = () => { secureFields.submit(); }; }; ``` ```js Node theme={"system"} secureFields.addEventListener(SecureFields.Events.CARD_VAULT_SUCCESS, () => { console.log("Card successfully tokenized"); }); secureFields.addEventListener(SecureFields.Events.CARD_VAULT_FAILURE, () => { console.log("Card tokenization failed"); }); document.querySelector("#cc-button").addEventListener("click", () => { secureFields.submit(); }); ``` ```js CDN theme={"system"} secureFields.addEventListener(SecureFields.Events.CARD_VAULT_SUCCESS, () => { console.log("Card successfully tokenized"); }); secureFields.addEventListener(SecureFields.Events.CARD_VAULT_FAILURE, () => { console.log("Card tokenization failed"); }); document.querySelector("#cc-button").addEventListener("click", () => { secureFields.submit(); }); ``` > [!WARNING] > > **Checkout session expiry** > > A checkout session expires after one hour by default. To change this, set > `expires_in` (in seconds, between 3600 and 86400) when you > [create the checkout session](/reference/checkout-sessions/new-checkout-session). > Use the checkout session to create a transaction or store the details as a > payment method before it expires. ## Summary In this step you: * Installed and initialized Secure Fields. * Rendered each of the individual fields in a custom UI * Submitted the data to the vault and listened for events # Creating an API key for use with Secure Fields Source: https://integration.wpay.com.au/guides/payments/secure-fields/quick-start/key Before the app can communicate with the API, create a new API key. The API handles authentication using a signed **JSON Web Token** (JWT) which is passed in a HTTP header. ```bash theme={"system"} curl -X GET https://api.example.gr4vy.app/transactions \ -H "authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIi..." ``` > [!TIP] > > Using an SDK is recommended. SDKs make the process of generating a token a lot easier and are probably > available in the preferred programming language. This is covered in the > next step. ## Create a new API key To use the API you need to generate a new **API key**. Please reach out to your Wpay representative who provides you with the API key. > [!NOTE] > > Learn more about private keys, key-pairs, and JWT in the > [in-depth guide on authentication](/guides/api/authentication). ## Summary In this step: * Learned about API authentication. * Created a new private key for the API. * Stored this API key in a secure place where the app can access it. # Secure Fields Source: https://integration.wpay.com.au/guides/payments/secure-fields/quick-start/overview Get set up with Secure Fields to vault and process cards. ![Secure Fields](https://mintcdn.com/wpay/RxgiaZJ07iWfuxu1/assets/images/secure-fields/hero.png?fit=max\&auto=format\&n=RxgiaZJ07iWfuxu1\&q=85\&s=b4f5283ccf8bafffbe60243ae040d681) Secure Fields is a light-weight alternative to Embed that can be used to capture card data in a PCI-compliant way, while fitting uniquely into any bespoke payment flow. 1. [Create an API key](/guides/payments/secure-fields/quick-start/key) which is needed to generate the signed JWT token in step 2. 2. [Set up a server-side SDK](/guides/payments/secure-fields/quick-start/sdks) to generate a checkout session. 3. [Add fields](/guides/payments/secure-fields/quick-start/embed) to a web page that handles the capturing of card data. 4. [And finally create a transaction](/guides/payments/secure-fields/quick-start/transact) with the checkout session. Once completed, it's recommended to look into some of the additional options, [theming](../theming), [adding logos](../theming#logos), and how to work with [stored cards](../stored-cards). # Creating checkout sessions Source: https://integration.wpay.com.au/guides/payments/secure-fields/quick-start/sdks Before using Secure Fields in the web checkout, create a checkout session. ## Install a server-side SDK Use the package manager in the preferred programming language to install the server-side SDK. Token generation can only be done server side and doing this client side is not recommended as it exposes the API key to customers. ```sh C# theme={"system"} dotnet add package Gr4vy ``` ```sh Go theme={"system"} go get github.com/gr4vy/gr4vy-go ``` ```sh Java theme={"system"} # Please check for the latest version implementation 'com.gr4vy:sdk:1.0.0' ``` ```sh PHP theme={"system"} composer require "gr4vy/gr4vy-php" ``` ```sh Python theme={"system"} pip install gr4vy ``` ```sh TypeScript theme={"system"} npm install @gr4vy/sdk # or: yarn add @gr4vy/sdk ``` > [!NOTE] > > Please always check and install the [latest release](/guides/get-started#server-side-sdks) of the preferred SDK. ## Initialize the SDK client Next, initialize the SDK with the ID of the instance and the private key. ```csharp C# theme={"system"} using Gr4vy; using Gr4vy.Models.Components; using System.Collections.Generic; // Loaded the key from a file, env variable, // or anywhere else var privateKey = "..."; var sdk = new Gr4vySDK( id: "example", server: SDKConfig.Server.Sandbox, bearerAuthSource: Auth.WithToken(privateKey), merchantAccountId: "default" ); ``` ```go Go theme={"system"} package main import ( "context" gr4vy "github.com/gr4vy/gr4vy-go" "github.com/gr4vy/gr4vy-go/models/operations" "log" "os" ) func main() { ctx := context.Background() privateKey := "...." // Private key loaded from disk or env var withToken := gr4vy.WithToken(privateKey, []JWTScope{ReadAll, WriteAll}, 60) s := gr4vy.New( gr4vy.WithID("example"), gr4vy.WithServer(gr4vy.ServerSandbox), gr4vy.WithSecuritySource(withToken), gr4vy.WithMerchantAccountID("default"), ) } ``` ```java Java theme={"system"} package hello.world; import com.gr4vy.sdk.BearerSecuritySource; import com.gr4vy.sdk.Gr4vy; import com.gr4vy.sdk.Gr4vy.AvailableServers; import com.gr4vy.sdk.models.components.AccountUpdaterJobCreate; import com.gr4vy.sdk.models.errors.*; import com.gr4vy.sdk.models.operations.ListTransactionsRequest; import java.lang.Exception; import java.util.List; public class Application { public static void main(String[] args) throws Exception { String privateKey = "-----BEGIN PRIVATE KEY-----\n...."; // a valid private key Gr4vy sdk = Gr4vy.builder() .id("example") .server(AvailableServers.SANDBOX) .merchantAccountId("default") .securitySource(new BearerSecuritySource.Builder(privateKey).build()) .build(); } } ``` ```php PHP theme={"system"} declare(strict_types=1); require 'vendor/autoload.php'; use Gr4vy; use Gr4vy\Auth; // Loaded the key from a file, env variable, // or anywhere else $privateKey = "..."; $sdk = Gr4vy\SDK::builder() ->setId('example') ->setServer('sandbox') ->setSecuritySource(Auth::withToken($privateKey)) ->setMerchantAccountId('default') ->build(); ``` ```python Python theme={"system"} from gr4vy import Gr4vy, auth import os client = Gr4vy( id="example", server="production", merchant_account_id="default", bearer_auth=auth.with_token(open("./private_key.pem").read()) ) ``` ```ts TypeScript theme={"system"} import fs from "fs"; import { Gr4vy, withToken } from "@gr4vy/sdk"; const gr4vy = new Gr4vy({ server: "sandbox", id: "example", bearerAuth: withToken({ privateKey: fs.readFileSync("private_key.pem", "utf8"), }), }); ``` > [!NOTE] > > The instance ID is the unique identifier for the deployment of the system and is included in every API call. > Together with the environment (sandbox or production) it is used to connect to the right APIs, as well as dashboard. > > The possible values for your ID are one of `wpay`, `wpay1`, `wpay2`, or `wpay3`. You are informed > in which instance your account has been set up. ## Generate a Checkout Session The final step is to create a new checkout session for use by **Secure Fields**. Checkout sessions can also store `cart_items`, `metadata`, `buyer`, and `airline` data, removing the need to pass these values on the transaction request. ```csharp C# theme={"system"} using Gr4vy; using Gr4vy.Models.Components; using NodaTime; using System; using System.Collections.Generic; var sdk = new Gr4vySDK( merchantAccountId: "default", bearerAuth: "" ); var checkoutSession = await sdk.CheckoutSessions.CreateAsync(checkoutSessionCreate: new CheckoutSessionCreate() { CartItems = new List() { new CartItem() { Name = "GoPro HD", Quantity = 2, UnitAmount = 1299, DiscountAmount = 0, TaxAmount = 0, ExternalIdentifier = "goprohd", Sku = "GPHD1078", ProductUrl = "https://example.com/catalog/go-pro-hd", ImageUrl = "https://example.com/images/go-pro-hd.jpg", Categories = new List() { "camera", "travel", "gear", }, ProductType = "physical", SellerCountry = "GB", }, new CartItem() { Name = "GoPro HD", Quantity = 2, UnitAmount = 1299, DiscountAmount = 0, TaxAmount = 0, ExternalIdentifier = "goprohd", Sku = "GPHD1078", ProductUrl = "https://example.com/catalog/go-pro-hd", ImageUrl = "https://example.com/images/go-pro-hd.jpg", Categories = new List() { "camera", "travel", "gear", }, ProductType = "physical", SellerCountry = "GB", }, new CartItem() { Name = "GoPro HD", Quantity = 2, UnitAmount = 1299, DiscountAmount = 0, TaxAmount = 0, ExternalIdentifier = "goprohd", Sku = "GPHD1078", ProductUrl = "https://example.com/catalog/go-pro-hd", ImageUrl = "https://example.com/images/go-pro-hd.jpg", Categories = new List() { "camera", "travel", "gear", }, ProductType = "physical", SellerCountry = "AU", }, }, Metadata = new Dictionary() { { "cohort", "cohort-a" }, { "order_id", "order-12345" }, }, Buyer = new GuestBuyer() { DisplayName = "John Doe", ExternalIdentifier = "buyer-12345", BillingDetails = new BillingDetails() { FirstName = "John", LastName = "Doe", EmailAddress = "john@example.com", PhoneNumber = "+1234567890", Address = new Address() { City = "San Jose", Country = "AU", PostalCode = "94560", State = "California", StateCode = "US-CA", HouseNumberOrName = "10", Line1 = "Stafford Appartments", Line2 = "29th Street", Organization = "Gr4vy", }, TaxId = new TaxId() { Value = "12345678931", Kind = "ar.cuit", }, }, ShippingDetails = new ShippingDetailsCreate() { FirstName = "John", LastName = "Doe", EmailAddress = "john@example.com", PhoneNumber = "+1234567890", Address = new Address() { City = "San Jose", Country = "AU", PostalCode = "94560", State = "California", StateCode = "US-CA", HouseNumberOrName = "10", Line1 = "Stafford Appartments", Line2 = "29th Street", Organization = "Gr4vy", }, }, }, }); // handle response ``` ```go Go theme={"system"} package main import( "context" gr4vygo "github.com/gr4vy/gr4vy-go" "os" "github.com/gr4vy/gr4vy-go/models/components" "github.com/gr4vy/gr4vy-go/types" "log" ) func main() { ctx := context.Background() s := gr4vygo.New( gr4vygo.WithMerchantAccountID("default"), gr4vygo.WithSecurity(os.Getenv("GR4VY_BEARER_AUTH")), ) checkoutSession, err := s.CheckoutSessions.Create(ctx, &components.CheckoutSessionCreate{ CartItems: []components.CartItem{ components.CartItem{ Name: "GoPro HD", Quantity: 2, UnitAmount: 1299, DiscountAmount: gr4vygo.Int64(0), TaxAmount: gr4vygo.Int64(0), ExternalIdentifier: gr4vygo.String("goprohd"), Sku: gr4vygo.String("GPHD1078"), ProductURL: gr4vygo.String("https://example.com/catalog/go-pro-hd"), ImageURL: gr4vygo.String("https://example.com/images/go-pro-hd.jpg"), Categories: []string{ "camera", "travel", "gear", }, ProductType: components.ProductTypePhysical.ToPointer(), SellerCountry: gr4vygo.String("AU"), }, components.CartItem{ Name: "GoPro HD", Quantity: 2, UnitAmount: 1299, DiscountAmount: gr4vygo.Int64(0), TaxAmount: gr4vygo.Int64(0), ExternalIdentifier: gr4vygo.String("goprohd"), Sku: gr4vygo.String("GPHD1078"), ProductURL: gr4vygo.String("https://example.com/catalog/go-pro-hd"), ImageURL: gr4vygo.String("https://example.com/images/go-pro-hd.jpg"), Categories: []string{ "camera", "travel", "gear", }, ProductType: components.ProductTypePhysical.ToPointer(), SellerCountry: gr4vygo.String("AU"), }, }, Metadata: map[string]string{ "cohort": "cohort-a", "order_id": "order-12345", }, Buyer: nil, }) if err != nil { log.Fatal(err) } if checkoutSession != nil { // handle response } } ``` ```java Java theme={"system"} package hello.world; import com.gr4vy.sdk.Gr4vy; import com.gr4vy.sdk.models.components.*; import com.gr4vy.sdk.models.errors.*; import com.gr4vy.sdk.models.operations.CreateCheckoutSessionResponse; import java.lang.Exception; import java.time.LocalDate; import java.time.OffsetDateTime; import java.util.List; import java.util.Map; import org.openapitools.jackson.nullable.JsonNullable; public class Application { public static void main(String[] args) throws Exception { Gr4vy sdk = Gr4vy.builder() .merchantAccountId("default") .bearerAuth("") .build(); CreateCheckoutSessionResponse res = sdk.checkoutSessions().create() .checkoutSessionCreate(CheckoutSessionCreate.builder() .cartItems(List.of( CartItem.builder() .name("GoPro HD") .quantity(2L) .unitAmount(1299L) .discountAmount(0L) .taxAmount(0L) .externalIdentifier("goprohd") .sku("GPHD1078") .productUrl("https://example.com/catalog/go-pro-hd") .imageUrl("https://example.com/images/go-pro-hd.jpg") .categories(List.of( "camera", "travel", "gear")) .productType(ProductType.PHYSICAL) .sellerCountry("AU") .build(), CartItem.builder() .name("GoPro HD") .quantity(2L) .unitAmount(1299L) .discountAmount(0L) .taxAmount(0L) .externalIdentifier("goprohd") .sku("GPHD1078") .productUrl("https://example.com/catalog/go-pro-hd") .imageUrl("https://example.com/images/go-pro-hd.jpg") .categories(List.of( "camera", "travel", "gear")) .productType(ProductType.PHYSICAL) .sellerCountry("AU") .build())) .metadata(Map.ofEntries( Map.entry("cohort", "cohort-a"), Map.entry("order_id", "order-12345"))) .buyer(JsonNullable.of(null)) .build()) .call(); if (res.checkoutSession().isPresent()) { // handle response } } } ``` ```php PHP theme={"system"} declare(strict_types=1); require 'vendor/autoload.php'; use Brick\DateTime\LocalDate; use Gr4vy; use Gr4vy\Utils; $sdk = Gr4vy\SDK::builder() ->setMerchantAccountId('default') ->setSecurity( '' ) ->build(); $checkoutSessionCreate = new Gr4vy\CheckoutSessionCreate( cartItems: [ new Gr4vy\CartItem( name: 'GoPro HD', quantity: 2, unitAmount: 1299, discountAmount: 0, taxAmount: 0, externalIdentifier: 'goprohd', sku: 'GPHD1078', productUrl: 'https://example.com/catalog/go-pro-hd', imageUrl: 'https://example.com/images/go-pro-hd.jpg', categories: [ 'camera', 'travel', 'gear', ], productType: 'physical', sellerCountry: 'AU', ), new Gr4vy\CartItem( name: 'GoPro HD', quantity: 2, unitAmount: 1299, discountAmount: 0, taxAmount: 0, externalIdentifier: 'goprohd', sku: 'GPHD1078', productUrl: 'https://example.com/catalog/go-pro-hd', imageUrl: 'https://example.com/images/go-pro-hd.jpg', categories: [ 'camera', 'travel', 'gear', ], productType: 'physical', sellerCountry: 'AU', ), ], metadata: [ 'cohort' => 'cohort-a', 'order_id' => 'order-12345', ], buyer: new Gr4vy\GuestBuyer( displayName: 'John Doe', externalIdentifier: 'buyer-12345', billingDetails: new Gr4vy\BillingDetails( firstName: 'John', lastName: 'Doe', emailAddress: 'john@example.com', phoneNumber: '+1234567890', address: new Gr4vy\Address( city: 'San Jose', country: 'AU', postalCode: '94560', state: 'California', stateCode: 'US-CA', houseNumberOrName: '10', line1: 'Stafford Appartments', line2: '29th Street', organization: 'Gr4vy', ), taxId: new Gr4vy\TaxId( value: '12345678931', kind: 'my.frp', ), ), shippingDetails: new Gr4vy\ShippingDetailsCreate( firstName: 'John', lastName: 'Doe', emailAddress: 'john@example.com', phoneNumber: '+1234567890', address: null, ), ), ); $response = $sdk->checkoutSessions->create( checkoutSessionCreate: $checkoutSessionCreate ); if ($response->checkoutSession !== null) { // handle response } ``` ```python Python theme={"system"} from datetime import date from gr4vy import Gr4vy, models from gr4vy.utils import parse_datetime import os with Gr4vy( merchant_account_id="default", bearer_auth=os.getenv("GR4VY_BEARER_AUTH", ""), ) as g_client: checkout_session = g_client.checkout_sessions.create(checkout_session_create=models.CheckoutSessionCreate( cart_items=[ models.CartItem( name="GoPro HD", quantity=2, unit_amount=1299, discount_amount=0, tax_amount=0, external_identifier="goprohd", sku="GPHD1078", product_url="https://example.com/catalog/go-pro-hd", image_url="https://example.com/images/go-pro-hd.jpg", categories=[ "camera", "travel", "gear", ], product_type="physical", seller_country="GB", ), models.CartItem( name="GoPro HD", quantity=2, unit_amount=1299, discount_amount=0, tax_amount=0, external_identifier="goprohd", sku="GPHD1078", product_url="https://example.com/catalog/go-pro-hd", image_url="https://example.com/images/go-pro-hd.jpg", categories=[ "camera", "travel", "gear", ], product_type="physical", seller_country="GB", ), models.CartItem( name="GoPro HD", quantity=2, unit_amount=1299, discount_amount=0, tax_amount=0, external_identifier="goprohd", sku="GPHD1078", product_url="https://example.com/catalog/go-pro-hd", image_url="https://example.com/images/go-pro-hd.jpg", categories=[ "camera", "travel", "gear", ], product_type="physical", seller_country="GB", ), ], metadata={ "cohort": "cohort-a", "order_id": "order-12345", }, buyer=models.GuestBuyer( display_name="John Doe", external_identifier="buyer-12345", billing_details=models.BillingDetails( first_name="John", last_name="Doe", email_address="john@example.com", phone_number="+1234567890", address=models.Address( city="San Jose", country="AU", postal_code="94560", state="California", state_code="US-CA", house_number_or_name="10", line1="Stafford Appartments", line2="29th Street", organization="Gr4vy", ), tax_id=models.TaxID( value="12345678931", kind="ar.cuit", ), ), shipping_details=models.ShippingDetailsCreate( first_name="John", last_name="Doe", email_address="john@example.com", phone_number="+1234567890", address=models.Address( city="San Jose", country="AU", postal_code="94560", state="California", state_code="US-CA", house_number_or_name="10", line1="Stafford Appartments", line2="29th Street", organization="Gr4vy", ), ), ), )) # Handle response print(checkout_session) ``` ```ts TypeScript theme={"system"} import { Gr4vy } from '@gr4vy/sdk' const gr4vy = new Gr4vy({ gr4vyId: 'your-gr4vy-id', privateKey: 'your-private-key', environment: 'sandbox', }) async function createCheckoutSession() { const checkoutSession = await gr4vy.checkoutSessions.create({ amount: 1000, currency: 'AUD', country: 'AU', cartItems: [ { name: 'GoPro HD', quantity: 2, unitAmount: 1299, discountAmount: 0, taxAmount: 0, externalIdentifier: 'goprohd', sku: 'GPHD1078', productUrl: 'https://example.com/catalog/go-pro-hd', imageUrl: 'https://example.com/images/go-pro-hd.jpg', categories: ['camera', 'travel', 'gear'], productType: 'physical', sellerCountry: 'GB', }, { name: 'GoPro HD', quantity: 2, unitAmount: 1299, discountAmount: 0, taxAmount: 0, externalIdentifier: 'goprohd', sku: 'GPHD1078', productUrl: 'https://example.com/catalog/go-pro-hd', imageUrl: 'https://example.com/images/go-pro-hd.jpg', categories: ['camera', 'travel', 'gear'], productType: 'physical', sellerCountry: 'GB', }, { name: 'GoPro HD', quantity: 2, unitAmount: 1299, discountAmount: 0, taxAmount: 0, externalIdentifier: 'goprohd', sku: 'GPHD1078', productUrl: 'https://example.com/catalog/go-pro-hd', imageUrl: 'https://example.com/images/go-pro-hd.jpg', categories: ['camera', 'travel', 'gear'], productType: 'physical', sellerCountry: 'AU', }, ], metadata: { cohort: 'cohort-a', order_id: 'order-12345', }, buyer: { displayName: 'John Doe', externalIdentifier: 'buyer-12345', billingDetails: { firstName: 'John', lastName: 'Doe', emailAddress: 'john@example.com', phoneNumber: '+1234567890', address: { city: 'San Jose', country: 'AU', postalCode: '94560', state: 'California', stateCode: 'US-CA', houseNumberOrName: '10', line1: 'Stafford Appartments', line2: '29th Street', organization: 'Gr4vy', }, taxId: { value: '12345678931', kind: 'ar.cuit', }, }, shippingDetails: { firstName: 'John', lastName: 'Doe', emailAddress: 'john@example.com', phoneNumber: '+1234567890', address: { city: 'San Jose', country: 'AU', postalCode: '94560', state: 'California', stateCode: 'US-CA', houseNumberOrName: '10', line1: 'Stafford Appartments', line2: '29th Street', organization: 'Gr4vy', }, }, }, }) console.log(checkoutSession) } createCheckoutSession() ``` The `id` of this session can now be passed to the checkout page, where it can be used by Secure Fields. ### How session values reach the transaction When you create a transaction with the checkout session, Gr4vy copies the session's `cart_items`, `metadata`, `buyer`, and `airline` values into the transaction. Which value wins when both the session and the transaction request set the same field depends on how the transaction uses the session. | How the transaction uses the session | Which value wins | | - | - | | As the payment method (`"method": "checkout-session"`), as with Secure Fields | The value in the transaction request. The session's value fills in only fields that the request leaves out. | | Pinned in the token with the [`checkout_session_id` claim](/guides/api/jwts#claims) | The value on the session. It replaces the value in the transaction request. | Gr4vy replaces each field as a whole and never merges the two values. For example, if both set `cart_items`, the transaction gets one list of cart items, not both lists combined. A transaction that charges a stored payment method by its `id`, without a checkout session, doesn't get any of these values. Send them on the transaction request instead. ## Summary In this step: * Installed the SDK * Created a new checkout session. * Passed the `id` of this session to the front-end app, where it is used by Secure Fields. # Creating transactions Source: https://integration.wpay.com.au/guides/payments/secure-fields/quick-start/transact The final step is to use the card data stored using **Secure Fields** to either create a transaction or store a card on file for later use. ## Create a transaction Card data stored in a checkout session can be used to create a transaction. ```csharp C# theme={"system"} var transaction = await client.Transactions.CreateAsync( transactionCreate: new TransactionCreate() { Amount = 1299, Currency = "AUD", Country = "AU", PaymentMethod = TransactionCreatePaymentMethod.CreateCheckoutSessionWithUrlPaymentMethodCreate( new CheckoutSessionWithUrlPaymentMethodCreate() { Id = checkoutSession.Id, } ), } ); ``` ```go Go theme={"system"} amount := int64(1299) currency := "AUD" checkoutSessionWithURLPaymentMethodCreate := components.CheckoutSessionWithURLPaymentMethodCreate{ ID: checkoutSession.ID, } paymentMethod := components.CreateTransactionCreatePaymentMethodCheckoutSessionWithURLPaymentMethodCreate(checkoutSessionWithURLPaymentMethodCreate) transactionCreate := components.TransactionCreate{ Amount: amount, Currency: currency, PaymentMethod: &paymentMethod, } transaction, err := client.Transactions.Create(ctx, transactionCreate, nil, nil) // handle response ``` ```java Java theme={"system"} CreateTransactionResponse transactionResponse = gr4vyClient.transactions().create() .transactionCreate(TransactionCreate.builder() .amount(1299L) .currency("AUD") .paymentMethod(TransactionCreatePaymentMethod.of(CheckoutSessionWithUrlPaymentMethodCreate.builder() .id(checkoutSession.id()) .build())) .build()) .call(); Transaction transaction = transactionResponse.transaction().orElse(null); // handle response ``` ```php PHP theme={"system"} $transactionCreate = new TransactionCreate(amount: 1299, currency: 'AUD', paymentMethod: new CheckoutSessionWithUrlPaymentMethodCreate(id: $checkoutSession->id)); $response = self::$sdk->transactions->create($transactionCreate); $transaction = $response->transaction; ``` ```python Python theme={"system"} transaction: models.Transaction = client.transactions.create( amount=1299, currency="AUD", payment_method={ "method": "checkout-session", "id": checkout_session.id, } ) ``` ```ts TypeScript theme={"system"} const transaction = await gr4vy.transactions.create({ amount: 1299, currency: "AUD", paymentMethod: { method: "checkout-session", id: checkoutSession.id } }) ``` The returned transaction includes details about the payment method used, and the status of the transaction. ```json Response theme={"system"} { "type": "transaction", "id": "fe26475d-ec3e-4884-9553-f7356683f7f9", "status": "authentication_succeeded", "amount": 1299, "currency": "AUD", "payment_method": { "type": "payment-method", "id": "77a76f7e-d2de-4bbc-ada9-d6a0015e6bd5", "method": "card", "scheme": "visa", "expiration_date": "07/24", ... }, ... } ``` ## Store a card for later use Card data in a checkout session can also be used to create a stored payment method that can be used again later. This can be done either at the time of creating a transaction by setting the [`store` property to `true`](/reference/transactions/new-transaction#body-store), or when creating a [new payment method](/reference/payment-methods/new-payment-method#checkout-session-payment-method-create) directly from the checkout session. > [!WARNING] > > This deletes the `security_code` from the > vault and any transaction created later requires the code to be > requested again. ## 3-D Secure 3-D Secure can be used in conjunction with Secure Fields using the [hosted 3DS solution][hosted-3ds]. To enable hosted 3DS, add a `redirect_url` to the API call to create a transaction, and then redirect the buyer to the `approval_url` in the response. ## Summary In this step you: * Used the checkout session to create a transaction or store a payment method ## Next That's it for the quick start guide. It's recommended to look into some of the additional options, [theming](../theming), [adding logos](../theming#logos), and how to work with [stored cards](../stored-cards). [hosted-3ds]: /guides/features/3ds/hosted # Secure Fields with stored cards Source: https://integration.wpay.com.au/guides/payments/secure-fields/stored-cards Use Secure Fields to collect the CVV for cards previously stored. It is possible to use Secure Fields to collect card details on a profile page to store the card on a buyer's account. Secure Fields can then be used to collect the security code at checkout, allowing a buyer to use a stored card together with a securely collected security code. ## 1. Store card for future use The first step is to store the card data for future use. Use the checkout session as described in the [quick-start](./quick-start) to store the card details as a new payment method in the vault. This can be done either at the time of creating a transaction by setting the [`store` property to `true`](/reference/transactions/new-transaction#body-store), or when creating a [new payment method](/reference/payment-methods/new-payment-method#checkout-session-payment-method-create) directly from the checkout session. Set `store` only when you first save the card. Storing the same card again creates a new payment method instead of reusing the existing one. The returned payment method includes details about the card that was used, as well as the `id` of the payment method that can be used in the next step. ```json theme={"system"} { "type": "payment-method", "id": "77a76f7e-d2de-4bbc-ada9-d6a0015e6bd5", "method": "card", "scheme": "visa", "expiration_date": "07/24", ... } ``` ## 2. Collect the CVV At checkout, Secure Fields can be used to collect the CVV for a previously stored card. When initializing Secure Fields, pass the `paymentMethod` object with both the `id` and `scheme` of the previously stored card. This enables proper validation for the security code based on the card scheme (for example, 3 or 4 digits) and provides the appropriate security code label (CVV, CVC, CID) in the field data. ```jsx React theme={"system"} }; ``` ```js Node / CDN theme={"system"} const secureFields = new SecureFields({ gr4vyId: "example", environment: "sandbox", sessionId: "332a6c3a-c4eb-45f6-9a4e-72af459535e2", paymentMethod: { id: "77a76f7e-d2de-4bbc-ada9-d6a0015e6bd5", scheme: "visa" } }); ``` > [!NOTE] > > The `paymentMethodId` prop is deprecated but still supported for backward compatibility. We recommend using the new `paymentMethod` prop to benefit from scheme-specific CVV validation. With this ID in place, you capture the security code securely. ```jsx React theme={"system"} import { CardNumber, ExpiryDate, SecurityCode, } from '@gr4vy/secure-fields-react' const Form = () => { return ( <> ) } ``` ```html Node / CDN theme={"system"} ``` > [!WARNING] > > When using the `paymentMethodId` or `paymentMethod` object with Secure Fields, only the > security code can be captured. Attempting to add any of the other > fields (number, expiration date) will result in an error. ## 3. Create a transaction When Secure Fields is submitted, it collects the security code for the stored payment method. You can then create a transaction with the checkout session much like a regular Secure Fields transaction. > [!WARNING] > > Don't set `store` to `true` on this transaction. The card is already stored. If you > send `store: true` and the transaction is declined or fails, for example because 3-D > Secure fails, Gr4vy can remove the stored payment method from the buyer. ```csharp C# theme={"system"} var transaction = await client.Transactions.CreateAsync( transactionCreate: new TransactionCreate() { Amount = 1299, Currency = "AUD", Country = "AU", PaymentMethod = TransactionCreatePaymentMethod.CreateCheckoutSessionWithUrlPaymentMethodCreate( new CheckoutSessionWithUrlPaymentMethodCreate() { Id = checkoutSession.Id, } ), } ); ``` ```go Go theme={"system"} amount := int64(1299) currency := "AUD" checkoutSessionWithURLPaymentMethodCreate := components.CheckoutSessionWithURLPaymentMethodCreate{ ID: checkoutSession.ID, } paymentMethod := components.CreateTransactionCreatePaymentMethodCheckoutSessionWithURLPaymentMethodCreate(checkoutSessionWithURLPaymentMethodCreate) transactionCreate := components.TransactionCreate{ Amount: amount, Currency: currency, PaymentMethod: &paymentMethod, } transaction, err := client.Transactions.Create(ctx, transactionCreate, nil, nil) // handle response ``` ```java Java theme={"system"} CreateTransactionResponse transactionResponse = gr4vyClient.transactions().create() .transactionCreate(TransactionCreate.builder() .amount(1299L) .currency("AUD") .paymentMethod(TransactionCreatePaymentMethod.of(CheckoutSessionWithUrlPaymentMethodCreate.builder() .id(checkoutSession.id()) .build())) .build()) .call(); Transaction transaction = transactionResponse.transaction().orElse(null); // handle response ``` ```php PHP theme={"system"} $transactionCreate = new TransactionCreate(amount: 1299, currency: 'AUD', paymentMethod: new CheckoutSessionWithUrlPaymentMethodCreate(id: $checkoutSession->id)); $response = self::$sdk->transactions->create($transactionCreate); $transaction = $response->transaction; ``` ```python Python theme={"system"} transaction: models.Transaction = client.transactions.create( amount=1299, currency="AUD", payment_method={ "method": "checkout-session", "id": checkout_session.id, } ) ``` ```ts TypeScript theme={"system"} const transaction = await gr4vy.transactions.create({ amount: 1299, currency: "AUD", paymentMethod: { method: "checkout-session", id: checkoutSession.id } }) ``` The returned transaction includes details about the payment method used, and the status of the transaction. ```json Response theme={"system"} { "type": "transaction", "id": "fe26475d-ec3e-4884-9553-f7356683f7f9", "status": "authentication_succeeded", "amount": 1299, "currency": "AUD", "payment_method": { "type": "payment-method", "id": "77a76f7e-d2de-4bbc-ada9-d6a0015e6bd5", "method": "card", "scheme": "visa", "expiration_date": "07/24", ... }, ... } ``` ## 3-D Secure with a stored card Native 3-D Secure in Secure Fields also works with a stored card. Secure Fields runs 3-D Secure against the checkout session, which uses the stored card. 1. Create a checkout session with the transaction context, as described in [Secure Fields and 3DS](/guides/payments/secure-fields/three-d-secure#native-3ds-implementation). 2. Initialize Secure Fields with the `paymentMethod` of the stored card, as described in [collect the CVV](#2-collect-the-cvv). 3. Add the 3-D Secure component, as described in [initialization](/guides/payments/secure-fields/three-d-secure#initialization). 4. Call `submit()`. Secure Fields runs 3-D Secure and fires the start and finish events. 5. [Create a transaction](#3-create-a-transaction) with the checkout session. If you don't use Secure Fields, use [hosted 3-D Secure](/guides/features/3ds/hosted) instead. # Secure Fields theming Source: https://integration.wpay.com.au/guides/payments/secure-fields/theming Each field can be customized to match the brand using the `styles` property when adding the field. ## Overview The outer styling (any containers around fields, etc) is completely in your control. Secure Fields also generates classes along with a set of data attributes and attaches them to the outer `