diff --git a/Example.App.js b/Example.App.js
index 36c6954..04ea0f1 100644
--- a/Example.App.js
+++ b/Example.App.js
@@ -14,15 +14,20 @@ export default class App extends React.Component {
     if (event && event.nativeEvent.data) {
       if (['cancel'].includes(event.nativeEvent.data)) {
         this.captchaForm.hide();
-        this.setState({ code: event.nativeEvent.data});
-      } else if (['error', 'expired'].includes(event.nativeEvent.data)) {
+        this.setState({ code: event.nativeEvent.data });
+      } else if (['error'].includes(event.nativeEvent.data)) {
         this.captchaForm.hide();
-        this.setState({ code: event.nativeEvent.data});
+        this.setState({ code: event.nativeEvent.data });
+        console.log('Verification failed', event.nativeEvent.data);
+      } else if (event.nativeEvent.data === 'expired') {
+        event.reset();
+        console.log('Visual challenge expired, reset...', event.nativeEvent.data);
       } else if (event.nativeEvent.data === 'open') {
         console.log('Visual challenge opened');
       } else {
         console.log('Verified code from hCaptcha', event.nativeEvent.data);
         this.captchaForm.hide();
+        event.markUsed();
         this.setState({ code: event.nativeEvent.data });
       }
     }
diff --git a/Hcaptcha.d.ts b/Hcaptcha.d.ts
index fa7a414..e4bc0d5 100644
--- a/Hcaptcha.d.ts
+++ b/Hcaptcha.d.ts
@@ -81,6 +81,11 @@ type HcaptchaProps = {
    * hCaptcha SDK host identifier. null value means that it will be generated by SDK
    */
   host?: string;
+  /**
+   * The orientation of the challenge.
+   * Default: portrait
+   */
+  orientation?: 'portrait' | 'landscape';
 }
 
 export default class Hcaptcha extends React.Component<HcaptchaProps> {}
diff --git a/Hcaptcha.js b/Hcaptcha.js
index f1e4475..6b8463e 100644
--- a/Hcaptcha.js
+++ b/Hcaptcha.js
@@ -1,4 +1,4 @@
-import React, { useMemo, useCallback } from 'react';
+import React, { useMemo, useCallback, useRef } from 'react';
 import WebView from 'react-native-webview';
 import { Linking, StyleSheet, View, ActivityIndicator } from 'react-native';
 import ReactNativeVersion from 'react-native/Libraries/Core/ReactNativeVersion';
@@ -20,7 +20,7 @@ const patchPostMessageJsCode = `(${String(function () {
   window.ReactNativeWebView.postMessage = patchedPostMessage;
 })})();`;
 
-const buildHcaptchaApiUrl = (jsSrc, siteKey, hl, theme, host, sentry, endpoint, assethost, imghost, reportapi) => {
+const buildHcaptchaApiUrl = (jsSrc, siteKey, hl, theme, host, sentry, endpoint, assethost, imghost, reportapi, orientation) => {
   var url = `${jsSrc || "https://hcaptcha.com/1/api.js"}?render=explicit&onload=onloadCallback`;
 
   let effectiveHost;
@@ -30,7 +30,7 @@ const buildHcaptchaApiUrl = (jsSrc, siteKey, hl, theme, host, sentry, endpoint,
     host = (siteKey || 'missing-sitekey') + '.react-native.hcaptcha.com';
   }
 
-  for (let [key, value] of Object.entries({ host, hl, custom: typeof theme === 'object', sentry, endpoint, assethost, imghost, reportapi })) {
+  for (let [key, value] of Object.entries({ host, hl, custom: typeof theme === 'object', sentry, endpoint, assethost, imghost, reportapi, orientation })) {
     if (value) {
       url += `&${key}=${encodeURIComponent(value)}`
     }
@@ -60,6 +60,7 @@ const buildHcaptchaApiUrl = (jsSrc, siteKey, hl, theme, host, sentry, endpoint,
  * @param {string} imghost: Points loaded hCaptcha challenge images to a user defined image location, used for proxies. Default: https://imgs.hcaptcha.com (Override only if using first-party hosting feature.)
  * @param {string} host: hCaptcha SDK host identifier. null value means that it will be generated by SDK
  * @param {object} debug: debug information
+ * @parem {string} hCaptcha challenge orientation
  */
 const Hcaptcha = ({
   onMessage,
@@ -81,8 +82,10 @@ const Hcaptcha = ({
   imghost,
   host,
   debug,
+  orientation,
 }) => {
-  const apiUrl = buildHcaptchaApiUrl(jsSrc, siteKey, languageCode, theme, host, sentry, endpoint, assethost, imghost, reportapi);
+  const apiUrl = buildHcaptchaApiUrl(jsSrc, siteKey, languageCode, theme, host, sentry, endpoint, assethost, imghost, reportapi, orientation);
+  const tokenTimeout = 120000;
 
   if (theme && typeof theme === 'string') {
     theme = `"${theme}"`;
@@ -151,7 +154,7 @@ const Hcaptcha = ({
             console.log("challenge opened");
           };
           var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
-          var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("cancel"); };
+          var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
           var onDataErrorCallback = function(error) {
             console.log("challenge error callback fired");
             window.ReactNativeWebView.postMessage("error");
@@ -199,8 +202,17 @@ const Hcaptcha = ({
     [loadingIndicatorColor]
   );
 
+  const webViewRef = useRef(null);
+
+  const reset = () => {
+    if (webViewRef.current) {
+      webViewRef.current.injectJavaScript('onloadCallback();');
+    }
+  };
+
   return (
     <WebView
+      ref={webViewRef}
       originWhitelist={['*']}
       onShouldStartLoadWithRequest={(event) => {
         if (event.url.slice(0, 24) === 'https://www.hcaptcha.com') {
@@ -210,7 +222,14 @@ const Hcaptcha = ({
         return true;
       }}
       mixedContentMode={'always'}
-      onMessage={onMessage}
+      onMessage={(e) => {
+        e.reset = reset;
+        if (e.nativeEvent.data.length > 16) {
+          const expiredTokenTimerId = setTimeout(() => onMessage({ nativeEvent: { data: 'expired' }, reset }), tokenTimeout);
+          e.markUsed = () => clearTimeout(expiredTokenTimerId);
+        }
+        onMessage(e);
+      }}
       javaScriptEnabled
       injectedJavaScript={patchPostMessageJsCode}
       automaticallyAdjustContentInsets
diff --git a/README.md b/README.md
index 72881c4..f0c30ec 100644
--- a/README.md
+++ b/README.md
@@ -35,28 +35,54 @@ Also, please note the following special message strings that can be returned via
 
 | name | purpose |
 | --- | --- |
-| expired | passcode response expired and the user must re-verify |
+| expired | passcode response expired and the user must re-verify, or did not answer before session expired |
 | error | there was an error displaying the challenge |
-| cancel | the user closed the challenge, or did not answer before session expired |
+| cancel | the user closed the challenge |
 | open | the visual challenge was opened |
 
 
 Any other string returned by `onMessage` will be a passcode.
 
+
 ### Handling the post-issuance expiration lifecycle
 
 This extension is a lightweight wrapper, and does not currently attempt to manage post-verification state in the same way as the web JS API, e.g. with an on-expire callback.
 
 In particular, if you do **not** plan to immediately consume the passcode returned by submitting it to your backend, you should start a timer to let your application state know that a new passcode is required when it expires.
 
-By default, this value is 120 seconds. Thus, you would want code similar to the following in your app when handling `onMessage` responses that return a passcode:
-
-```
-this.timeoutCheck = setTimeout(() => {
-   this.setPasscodeExpired();
-   }, 120000);
+By default, this value is 120 seconds. So, an `expired` error will be emitted to `onMessage` if you haven't called `event.markUsed()`.
+
+Once you've utilized hCaptcha's token, call `markUsed` on the event object in `onMessage`:
+
+```js
+  onMessage = event => {
+    if (event && event.nativeEvent.data) {
+      if (['cancel'].includes(event.nativeEvent.data)) {
+        this.captchaForm.hide();
+      } else if (['error'].includes(event.nativeEvent.data)) {
+        this.captchaForm.hide();
+        // handle error
+      } else {
+        this.captchaForm.hide();
+        const token = event.nativeEvent.data;
+        // utlize token and call markUsed once you done with it
+        event.markUsed();
+      }
+    }
+  };
+  ...
+  <ConfirmHcaptcha
+    ref={_ref => (this.captchaForm = _ref)}
+    siteKey={siteKey}
+    languageCode="en"
+    onMessage={this.onMessage}
+  />
 ```
 
+### Handling errors and retry
+
+If your app encounters an `error` event, you can reset the hCaptcha SDK flow by calling `event.reset()`` to perform another attempt at verification.
+
 ## Dependencies
 
 1. [react-native-modal](https://github.com/react-native-community/react-native-modal)
@@ -129,6 +155,7 @@ Otherwise, you should pass in the preferred device locale, e.g. fetched from `ge
 | baseUrl _(modal component only)_ | string | The url domain defined on your hCaptcha. You generally will not need to change this. |
 | passiveSiteKey _(modal component only)_ | boolean | Indicates whether the passive mode is enabled; when true, the modal won't be shown at all |
 | hasBackdrop _(modal component only)_ | boolean | Defines if the modal backdrop is shown (true by default) |
+| orientation | string | This specifies the "orientation" of the challenge. It can be `portrait`, `landscape`. Default: `portrait` |
 
 
 ## Status
diff --git a/__tests__/__snapshots__/ConfirmHcaptcha.test.js.snap b/__tests__/__snapshots__/ConfirmHcaptcha.test.js.snap
index 0df6a2a..ea8b473 100644
--- a/__tests__/__snapshots__/ConfirmHcaptcha.test.js.snap
+++ b/__tests__/__snapshots__/ConfirmHcaptcha.test.js.snap
@@ -53,6 +53,7 @@ exports[`ConfirmHcaptcha snapshot tests renders ConfirmHcaptcha with all props 1
 })();"
       javaScriptEnabled={true}
       mixedContentMode="always"
+      onMessage={[Function]}
       onShouldStartLoadWithRequest={[Function]}
       originWhitelist={
         [
@@ -103,7 +104,7 @@ exports[`ConfirmHcaptcha snapshot tests renders ConfirmHcaptcha with all props 1
             console.log("challenge opened");
           };
           var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
-          var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("cancel"); };
+          var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
           var onDataErrorCallback = function(error) {
             console.log("challenge error callback fired");
             window.ReactNativeWebView.postMessage("error");
@@ -208,6 +209,7 @@ exports[`ConfirmHcaptcha snapshot tests renders ConfirmHcaptcha with minimum pro
 })();"
       javaScriptEnabled={true}
       mixedContentMode="always"
+      onMessage={[Function]}
       onShouldStartLoadWithRequest={[Function]}
       originWhitelist={
         [
@@ -258,7 +260,7 @@ exports[`ConfirmHcaptcha snapshot tests renders ConfirmHcaptcha with minimum pro
             console.log("challenge opened");
           };
           var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
-          var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("cancel"); };
+          var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
           var onDataErrorCallback = function(error) {
             console.log("challenge error callback fired");
             window.ReactNativeWebView.postMessage("error");
diff --git a/__tests__/__snapshots__/Hcaptcha.test.js.snap b/__tests__/__snapshots__/Hcaptcha.test.js.snap
index d001e9e..09e3405 100644
--- a/__tests__/__snapshots__/Hcaptcha.test.js.snap
+++ b/__tests__/__snapshots__/Hcaptcha.test.js.snap
@@ -15,6 +15,7 @@ exports[`Hcaptcha snapshot tests renders Hcaptcha with all props 1`] = `
 })();"
   javaScriptEnabled={true}
   mixedContentMode="always"
+  onMessage={[Function]}
   onShouldStartLoadWithRequest={[Function]}
   originWhitelist={
     [
@@ -65,7 +66,7 @@ exports[`Hcaptcha snapshot tests renders Hcaptcha with all props 1`] = `
             console.log("challenge opened");
           };
           var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
-          var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("cancel"); };
+          var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
           var onDataErrorCallback = function(error) {
             console.log("challenge error callback fired");
             window.ReactNativeWebView.postMessage("error");
@@ -130,6 +131,7 @@ exports[`Hcaptcha snapshot tests renders Hcaptcha with minimum props 1`] = `
 })();"
   javaScriptEnabled={true}
   mixedContentMode="always"
+  onMessage={[Function]}
   onShouldStartLoadWithRequest={[Function]}
   originWhitelist={
     [
@@ -180,7 +182,7 @@ exports[`Hcaptcha snapshot tests renders Hcaptcha with minimum props 1`] = `
             console.log("challenge opened");
           };
           var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
-          var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("cancel"); };
+          var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
           var onDataErrorCallback = function(error) {
             console.log("challenge error callback fired");
             window.ReactNativeWebView.postMessage("error");
@@ -244,6 +246,7 @@ exports[`Hcaptcha snapshot tests test debug 1`] = `
 })();"
   javaScriptEnabled={true}
   mixedContentMode="always"
+  onMessage={[Function]}
   onShouldStartLoadWithRequest={[Function]}
   originWhitelist={
     [
@@ -294,7 +297,7 @@ exports[`Hcaptcha snapshot tests test debug 1`] = `
             console.log("challenge opened");
           };
           var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
-          var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("cancel"); };
+          var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
           var onDataErrorCallback = function(error) {
             console.log("challenge error callback fired");
             window.ReactNativeWebView.postMessage("error");
diff --git a/index.js b/index.js
index ff4ec5a..0298b47 100644
--- a/index.js
+++ b/index.js
@@ -28,6 +28,7 @@ class ConfirmHcaptcha extends PureComponent {
       passiveSiteKey,
       baseUrl,
       languageCode,
+      orientation,
       onMessage,
       showLoading,
       backgroundColor,
@@ -110,6 +111,7 @@ ConfirmHcaptcha.propTypes = {
   baseUrl: PropTypes.string,
   onMessage: PropTypes.func,
   languageCode: PropTypes.string,
+  orientation: PropTypes.string,
   backgroundColor: PropTypes.string,
   showLoading: PropTypes.bool,
   loadingIndicatorColor: PropTypes.string,
@@ -130,6 +132,7 @@ ConfirmHcaptcha.defaultProps = {
   size: 'invisible',
   passiveSiteKey: false,
   showLoading: false,
+  orientation: 'portrait',
   backgroundColor: 'rgba(0, 0, 0, 0.3)',
   loadingIndicatorColor: null,
   theme: 'light',