Web API Reference

Web API Reference

#Constructor

Configure and construct your Magic SDK instance.

#Arguments

new Magic(apiKey, options?)

ParameterTypeDefinition
apiKeyStringYour publishable API Key retrieved from the Magic Dashboard.
options.locale?StringCustomize the language of Magic's modal, email and confirmation screen. See Localization for more.
options.testMode?BooleanEnable testMode to assert the desired behavior through the email address you provide to loginWithMagicLink without having to go through the auth flow.
options.network?String | Object(String): A representation of the connected Ethereum network (one of: mainnet, rinkeby, kovan, or ropsten). ⁠ ⁠(Object): A custom Ethereum Node configuration with the following shape: ⁠ ⁠• rpcUrl (String): A URL pointing to your custom Ethereum Node. ⁠ ⁠ ⁠• chainId? (Number): Some Node infrastructures require you to pass an explicit chain ID. If you are aware that your Node requires this configuration, pass it here as an integer.
options.endpoint?StringA URL pointing to the Magic <iframe> application.

#Example

Javascript
01import { Magic } from 'magic-sdk';
02
03let m;
04
05// Construct with an API key:
06m = new Magic('API_KEY');
07
08// Construct with an API key and locale:
09m = new Magic('API_KEY', { locale: 'es' });
10
11// Construct with an API key and testMode enabled:
12m = new Magic('API_KEY', { testMode: true });
13
14// Construct with an API key plus options:
15m = new Magic('API_KEY', { network: 'rinkeby', endpoint: '...' });

👉 Learn more about using Magic SDK with Ethereum!

#Global Methods

Global methods and properties are accessible on the Magic SDK instance itself.

Javascript
01import { Magic } from 'magic-sdk';
02
03const m = new Magic('API_KEY');
04
05m.preload;

#preload

Starts downloading the static assets required to render the Magic iframe context.

#Arguments

None

#Returns

Promise<void>: A Promise that resolves to indicate the <iframe> is ready for requests.

#Example

Javascript
01import { Magic } from 'magic-sdk';
02
03const m = new Magic('API_KEY');
04
05m.preload().then(() => console.log('Magic <iframe> loaded.'));

#Auth Module

The Auth Module and it's members are accessible on the Magic SDK instance by the auth property.

Javascript
01import { Magic } from 'magic-sdk';
02
03const m = new Magic('API_KEY');
04
05m.auth;
06m.auth.loginWithMagicLink;
07m.auth.loginWithSMS;
08m.auth.loginWithCredential;
09m.auth.loginWithEmailOTP;

#loginWithMagicLink

Authenticate a user passwordlessly using a "magic link" sent to the specified user's email address.

#Arguments

loginWithMagicLink({ email, showUI? = true, redirectURI? })

ParameterTypeDefinition
emailStringThe user email to log in with.
showUI?BooleanIf true, show an out-of-the-box pending UI while the request is in flight.
redirectURI?StringYou can provide a redirect URI that Magic will point to after the user clicks their email link. Don't forget to call loginWithCredential at the specified redirect location! ⁠ ⁠Note: If you are securing a resource server and have your own ⁠signup flow after this call resolves, be mindful of where you're calling ⁠signup in your implementation to avoid potential concurrency issues!

#Returns

PromiEvent<string | null>: The promise resolves upon authentication request success and rejects with a specific error code if the request fails. The resolved value is a Decentralized ID token with a default 15-minute lifespan.

#Example

Javascript
01import { Magic } from 'magic-sdk';
02
03const m = new Magic('API_KEY');
04
05// log in a user by their email
06try {
07  await m.auth.loginWithMagicLink({ email: 'hello@example.com' });
08} catch {
09  // Handle errors if required!
10}
11
12// log in a user by their email, without showing an out-of-the box UI.
13try {
14  await m.auth.loginWithMagicLink({ email: 'hello@example.com', showUI: false });
15} catch {
16  // Handle errors if required!
17}

#Error Handling

Relevant Error Codes

To achieve a fully white-labeled experience, you will need to implement some custom error handling according to your UI needs. Here's a short example to illustrate how errors can be caught and identified by their code:

Javascript
01import { Magic, RPCError, RPCErrorCode } from 'magic-sdk';
02
03const m = new Magic('API_KEY');
04
05try {
06  await m.auth.loginWithMagicLink({ email: 'hello@example.com', showUI: false });
07} catch (err) {
08  if (err instanceof RPCError) {
09    switch (err.code) {
10      case RPCErrorCode.MagicLinkFailedVerification:
11      case RPCErrorCode.MagicLinkExpired:
12      case RPCErrorCode.MagicLinkRateLimited:
13      case RPCErrorCode.UserAlreadyLoggedIn:
14        // Handle errors accordingly :)
15        break;
16    }
17  }
18}

#Events

Event NameDefinition
email-not-deliverableDispatched if the magic link email is unable to be delivered.
email-sentDispatched when the magic link email has been successfully sent from the Magic Link server.
retryDispatched when the user restarts the flow. This can only happen if showUI: true.

#loginWithEmailOTP

Authenticate a user passwordlessly using an email one-time code sent to the specified user's email address.

#Arguments

loginWithEmailOTP({ email, showUI? = true })

# ⁠Returns

ParameterTypeDefinition
emailStringThe user email to log in with.

showUI?

Boolean

If true, show an out-of-the-box UI to accept the OTP from user.

PromiEvent<string | null>: The promise resolves upon authentication request success and rejects with a specific error code if the request fails. The resolved value is a Decentralized ID token with a default 15-minute lifespan.

#Example

Javascript
01import { Magic } from 'magic-sdk';
02
03const m = new Magic('API_KEY');
04
05// log in a user by their email
06try {
07  await m.auth.loginWithEmailOTP({ email: 'hello@example.com' });
08} catch {
09  // Handle errors if required!
10}
11
12// log in a user by their email, without showing an out-of-the box UI.
13try {
14  await m.auth.loginWithEmailOTP({ email: 'hello@example.com', showUI: false });
15} catch {
16  // Handle errors if required!
17}

#Event Handling

Relevant Events

A white-label OTP login flow is available when passing showUI: false to this login method. Here's a short example to illustrate listening for and emitting events during the login flow:

Javascript
01import { Magic } from 'magic-sdk';
02
03const magic = new Magic('API_KEY');
04
05try {
06  // Initiate login flow
07  const handle = magic.auth.loginWithEmailOTP({ email: "hello@example.com", showUI: false });
08
09  handle
10  .on('email-otp-sent', () => {
11    // The email has been sent to the user
12
13    // Prompt the user for the OTP
14    const otp = window.prompt('Enter Email OTP');
15
16    // Send the OTP for verification
17    handle.emit('verify-email-otp', otp);
18  })
19  .on('invalid-email-otp', () => {
20    // User entered invalid OTP
21
22    /* 
23      Have the user retry entering the OTP.
24      Then emit the "verify-email-otp" event with the OTP.
25    */
26
27    /*
28      You may limit the amount of retries and
29      emit a "cancel" event to cancel the login request.
30    */
31
32    // cancel login request
33    handle.emit('cancel');
34  })
35  .on('done', (result) => {
36    // is called when the Promise resolves
37
38    // convey login success to user
39    alert('Login complete!');
40		
41    // DID Token returned in result
42    const didToken = result;
43  })
44  .on('error', (reason) => {
45    // is called if the Promise rejects
46    console.error(reason);
47  })
48  .on('settled', () => {
49    // is called when the Promise either resolves or rejects
50  }) 
51} catch (err) {
52  // handle errors
53}

#Events

Event NameDefinition
email-otp-sentDispatched when the OTP email has been successfully sent from the Magic server.
verify-email-otpEmit along with the OTP to verify the code from user.
invalid-email-otpDispatched when the OTP sent fails verification.

cancel

Emit to cancel the login request.

#Error Handling

To achieve a fully white-labeled experience, you will need to implement some custom error handling according to your UI needs. Here's a short example to illustrate how errors can be caught and identified by their code:

Javascript
01import { Magic, RPCError, RPCErrorCode } from 'magic-sdk';
02
03const m = new Magic('API_KEY');
04
05try {
06  await m.auth.loginWithEmailOTP({ email: 'hello@example.com' });
07} catch (err) {
08  if (err instanceof RPCError) {
09    switch (err.code) {
10      case RPCErrorCode.MagicLinkExpired
11      case RPCErrorCode.UserAlreadyLoggedIn:
12        // Handle errors accordingly :)
13        break;
14    }
15  }
16}

#loginWithSMS

Authenticate a user passwordlessly using a one-time code sent to the specified phone number.

List of Currently Blocked Country Codes

#Arguments

loginWithSMS({ phoneNumber })

ParameterTypeDefinition
phoneNumberStringE.164 formatted phone number.

#Returns

PromiEvent<string | null>: The promise resolves upon authentication request success and rejects with a specific error code if the request fails. The resolved value is a Decentralized ID token with a default 15-minute lifespan.

#Example

01import { Magic } from 'magic-sdk';
02
03const magicClient = new Magic('API_KEY');
04
05// log in a user by their phone number
06try {
07  await magicClient.auth.loginWithSMS({ '+14151231234' });
08} catch {
09  // Handle errors if required!
10}

#Error Handling

Relevant Error Codes

To achieve a fully white-labeled experience, you will need to implement some custom error handling according to your UI needs. Here's a short example to illustrate how errors can be caught and identified by their code:

01import { Magic, RPCError, RPCErrorCode } from 'magic-sdk';
02
03const m = new Magic('API_KEY');
04
05try {
06  await m.auth.loginWithSMS({ phoneNumber: "+14151231234" });
07} catch (err) {
08  if (err instanceof RPCError) {
09    switch (err.code) {
10      case RPCErrorCode.AccessDeniedToUser:
11      case RPCErrorCode.MagicLinkRateLimited:
12      case RPCErrorCode.UserAlreadyLoggedIn:
13        // Handle errors accordingly :)
14        break;
15    }
16  }
17}

#loginWithCredential

Authenticate a user via a "Magic Credential," a special, one-time-use DID Token created by the user to hydrate their authentication between page reloads. For example: when executing the loginWithMagicLink flow with a redirectURI specified, you can invoke this method to complete the authentication "callback," similar in principal to OAuth 2.0 flows.

If given no arguments, this method will parse the credential token automatically from window.location.search.

#Arguments

loginWithCredential(credentialOrQueryString?)

ParameterTypeDefinition
credentialOrQueryString?StringA credential token or a valid query string (prefixed with ? or #). By default, this method will look for the a credential token on the magic_credential key of window.location.search.

#Returns

PromiEvent<string | null>: The promise resolves upon authentication request success and rejects with a specific error code if the request fails. The resolved value is a Decentralized ID token with a default 15-minute lifespan.

#Example

From your login page:

Javascript
01import { Magic } from 'magic-sdk';
02
03const m = new Magic('API_KEY');
04
05try {
06  await m.auth.loginWithMagicLink({
07    email: 'hello@example.com',
08    redirectURI: 'https://yourdomain.com/your/magic/link/callback',
09  });
10
11  // When the user clicks their magic link, they will be logged-in here
12  // and in the "callback" context.
13} catch {
14  // Handle errors if required!
15}

From your authentication callback page:

Javascript
01import { Magic } from 'magic-sdk';
02
03const m = new Magic('API_KEY');
04
05try {
06  await m.auth.loginWithCredential();
07} catch {
08  // Handle errors if required!
09}
10
11// You can also provide the credential yourself
12try {
13  await m.auth.loginWithCredential('iamacredentialtoken');
14} catch {
15  // Handle errors if required!
16}
17
18// You can also provide the credential as a query string
19try {
20  await m.auth.loginWithCredential(window.location.search);
21} catch {
22  // Handle errors if required!
23}

#User Module

The User Module and it's members are accessible on the Magic SDK instance by the user property.

Javascript
01import { Magic } from 'magic-sdk';
02
03const m = new Magic('API_KEY');
04
05m.user;
06m.user.updateEmail;
07m.user.updatePhoneNumber;
08m.user.getIdToken;
09m.user.generateIdToken;
10m.user.getMetadata;
11m.user.isLoggedIn;
12m.user.logout;

#updateEmail

Initiates the update email flow that allows a user to change their email address.

#Arguments

updateEmail({ email, showUI? = true })

ParameterTypeDefinition
emailStringThe new email to update to.
showUI?BooleanIf true, shows an out-of-the-box pending UI which includes instructions on which step of the confirmation process the user is on. Dismisses automatically when the process is complete.

#Returns

PromiEvent<boolean>: The promise resolves with a true boolean value if update email is successful and rejects with a specific error code if the request fails.

#Example

Javascript
01import { Magic } from 'magic-sdk';
02
03const m = new Magic('API_KEY');
04
05// Initiates the flow to update a user's current email to a new one.
06try {
07  ...
08  /* Assuming user is logged in */
09  await magic.user.updateEmail({ email: 'new_user_email@example.com' });
10} catch {
11  // Handle errors if required!
12}
13
14/**
15 * Initiates the flow to update a user's current email to a new one,
16 * without showing an out-of-the box UI.
17 */
18try {
19  /* Assuming user is logged in */
20  await magic.user.updateEmail({ email: 'new_user_email@example.com', showUI: false });
21} catch {
22  // Handle errors if required!
23}

#Error Handling

Relevant Error Codes

To achieve a fully white-labeled experience, you will need to implement some custom error handling according to your UI needs. Here's a short example to illustrate how errors can be caught and identified by their code:

Javascript
01import { Magic, RPCError, RPCErrorCode } from 'magic-sdk';
02
03const m = new Magic('API_KEY');
04
05try {
06  await m.user.updateEmail({ email: 'hello@example.com', showUI: false });
07} catch (err) {
08  if (err instanceof RPCError) {
09    switch (err.code) {
10      case RPCErrorCode.UpdateEmailFailed:
11        // Handle errors accordingly :)
12        break;
13    }
14  }
15}

#Events

Event NameDefinition
new-email-confirmedDispatched when the magic link has been clicked from the user’s new email address.
email-sentDispatched when the magic link email has been successfully sent from the Magic Link server to the user’s new email address.
email-not-deliverableDispatched if the magic link email is unable to be delivered to the user’s new email address.
old-email-confirmed Dispatched when the magic link has been clicked from the user’s previous email address.
retryDispatched when the user restarts the flow. This can only happen if showUI: true.

#updatePhoneNumber

Initiates the update phone number flow that allows a user to change their phone number.

#Arguments

None

#Returns

PromiEvent<string>: The promise resolves to a string value of the updated phone number if update is successful and rejects if the request fails.

#Example

Javascript
01import { Magic } from 'magic-sdk';
02
03const m = new Magic('API_KEY');
04
05// Initiates the flow to update a user's phone number to a new one.
06try {
07  ...
08  /* Assuming user is logged in */
09  await m.user.updatePhoneNumber();
10} catch {
11  // Handle errors if required!
12}

#getIdToken

Generates a Decentralized Id Token which acts as a proof of authentication to resource servers.

#Arguments

getIdToken({ lifespan? = 900 })

ParameterTypeDefinition
lifespan?NumberWill set the lifespan of the generated token. Defaults to 900s (15 mins)

#Returns

PromiEvent<string>: Base64-encoded string representation of a JSON tuple representing

#Example

Javascript
01import { Magic } from 'magic-sdk';
02
03const m = new Magic('API_KEY');
04
05// Assumes a user is already logged in
06try {
07  const idToken = await m.user.getIdToken();
08} catch {
09  // Handle errors if required!
10}

#generateIdToken

Generates a Decentralized ID token with optional serialized data.

#Arguments

generateIdToken({ lifespan? = 900, attachment? = 'none' })

ParameterTypeDefinition
lifespan?NumberWill set the lifespan of the generated token. Defaults to 900s (15 mins)
attachment?StringWill set a signature of serialized data in the generated token. Defaults to "none"

#Returns

PromiEvent<string>: Base64-encoded string representation of a JSON tuple representing [proof, claim]

#Example

Javascript
01import { Magic } from 'magic-sdk';
02
03const m = new Magic('API_KEY');
04
05// Assumes a user is already logged in
06try {
07  const idToken = await m.user.generateIdToken({ attachment: 'SERVER_SECRET' });
08} catch {
09  // Handle errors if required!
10}

#getMetadata

Retrieves information for the authenticated user.

#Arguments

None

#Returns

PromiEvent<{ issuer, email, publicAddress }>: an object containing the issuer, email and cryptographic public address of the authenticated user.

ValueTypeDefinition
issuerStringThe Decentralized ID of the user. In server-side use-cases, we recommend this value to be used as the user ID in your own tables.
emailStringEmail address of the authenticated user.
phoneNumberStringThe phone number of the authenticated user.
publicAddressStringThe authenticated user's public address (a.k.a.: public key).

#Example

Javascript
01import { Magic } from 'magic-sdk';
02
03const m = new Magic('API_KEY');
04
05// Assumes a user is already logged in
06try {
07  const { email, publicAddress } = await m.user.getMetadata();
08} catch {
09  // Handle errors if required!
10}

#isLoggedIn

Checks if a user is currently logged in to the Magic SDK.

#Arguments

None

#Returns

PromiEvent<Boolean>

#Example

Javascript
01import { Magic } from 'magic-sdk';
02
03const m = new Magic('API_KEY');
04
05try {
06  const isLoggedIn = await m.user.isLoggedIn();
07  console.log(isLoggedIn); // => `true` or `false`
08} catch {
09  // Handle errors if required!
10}

#logout

Logs out the currently authenticated Magic user

#Arguments

None

#Returns

PromiEvent<Boolean>

#Example

Javascript
01import { Magic } from 'magic-sdk';
02
03const m = new Magic('API_KEY');
04
05try {
06  await m.user.logout();
07  console.log(await m.user.isLoggedIn()); // => `false`
08} catch {
09  // Handle errors if required!
10}

#Errors & Warnings

There are three types of error class to be aware of when working with Magic's client-side JavaScript SDK:

  • SDKError: Raised by the SDK to indicate missing parameters, communicate deprecation notices, or other internal issues. A notable example would be a MISSING_API_KEY error, which informs the required API key parameter was missing from new Magic(...).
  • RPCError: Errors associated with specific method calls to the Magic <iframe> context. These methods are formatted as JSON RPC 2.0 payloads, so they return error codes as integers. This type of error is raised by methods like AuthModule.loginWithMagicLink.
  • ExtensionError: Errors associated with method calls to Magic SDK Extensions. Extensions are an upcoming/experimental feature of Magic SDK. More information will be available once Extensions are officially released.

#SDKError

The SDKError class is exposed for instanceof operations.

Javascript
01import { SDKError } from 'magic-sdk';
02
03try {
04  // Something async...
05catch (err) {
06  if (err instanceof SDKError) {
07    // Handle...
08  }
09}

SDKError instances expose the code field which may be used to deterministically identify the error. Additionally, an enumeration of error codes is exposed for convenience and readability:

Javascript
01import { SDKErrorCode } from 'magic-sdk';
02
03SDKErrorCode.MissingApiKey;
04SDKErrorCode.ModalNotReady;
05SDKErrorCode.MalformedResponse;
06// and so forth...
07// Please reference the `Enum Key` column of the error table below.

#Error Codes

Enum KeyDescription
MissingApiKeyIndicates the required Magic API key is missing or invalid.
ModalNotReadyIndicates the Magic iframe context is not ready to receive events. This error should be rare and usually indicates an environmental issue or improper async/await usage.
MalformedResponseIndicates the response received from the Magic iframe context is malformed. We all make mistakes (even us), but this should still be a rare exception. If you encounter this, please be aware of phishing!
InvalidArgumentRaised if an SDK method receives an invalid argument. Generally, TypeScript saves us all from simple bugs, but there are validation edge cases it cannot solve—this error type will keep you informed!
ExtensionNotInitializedIndicates an extension method was invoked before the Magic SDK instance was initialized. Make sure to access extension methods only from the Magic SDK instance to avoid this error.

#RPCError

The RPCError class is exposed for instanceof operations:

Javascript
01import { RPCError } from 'magic-sdk';
02
03try {
04  // Something async...
05catch (err) {
06  if (err instanceof RPCError) {
07    // Handle...
08  }
09}

RPCError instances expose the code field which may be used to deterministically identify the error. Additionally, an enumeration of error codes is exposed for convenience and readability:

Javascript
01import { RPCErrorCode } from 'magic-sdk';
02
03RPCErrorCode.MagicLinkExpired;
04RPCErrorCode.UserAlreadyLoggedIn;
05RPCErrorCode.ParseError;
06RPCErrorCode.MethodNotFound;
07RPCErrorCode.InternalError;
08// and so forth...
09// Please reference the `Enum Key` column of the error table below.

#Magic Link Error Codes

CodeEnum KeyDescription
-10000MagicLinkFailedVerificationThe magic link failed verification, possibly due to an internal service error or a generic network error.
-10001MagicLinkExpiredThe user clicked their magic link after it had expired (this can happen if the user takes more than 10 minutes to check their email).
-10002MagicLinkRateLimitedIf the showUI parameter is set to false, this error will communicate the email rate limit has been reached. Please debounce your method calls if this occurs.
-10006MagicLinkInvalidRedirectURLIf using the redirectURI parameter for the magic link flow, this error is recevied if the provided value is invalid for some reason.
-10003UserAlreadyLoggedInA user is already logged in. If a new user should replace the existing user, make sure to call logout before proceeding.
-10004UpdateEmailFailedAn update email request was unsuccessful, either due to an invalid email being supplied or the user canceled the action.
-10005UserRequestEditEmailThe user has stopped the login request because they want to edit the provided email.

#Standard JSON RPC 2.0 Error Codes

CodeEnum KeyDescription
-32700ParseErrorInvalid JSON was received by the server. An error occurred on the server while parsing the JSON text.
-32600InvalidRequestThe JSON sent is not a valid Request object.
-32601MethodNotFoundThe method does not exist / is not available.
-32602InvalidParamsInvalid method parameter(s).
-32603InternalErrorInternal JSON-RPC error. These can manifest as different generic issues (i.e.: attempting to access a protected endpoint before the user is logged in).

#ExtensionError

The ExtensionError class is exposed for instanceof operations:

Typescript
01import { ExtensionError } from 'magic-sdk';
02
03try {
04  // Something async...
05catch (err) {
06  if (err instanceof ExtensionError) {
07    // Handle...
08  }
09}

ExtensionError instances expose the code field which may be used to deterministically identify the error. Magic SDK does not export a global enumeration of Extension error codes. Instead, Extension authors are responsible for exposing and documenting error codes relevant to the Extension's use-case.

#PromiEvents

Magic SDK provides a flexible interface for handling methods which encompass multiple "stages" of an action. Promises returned by Magic SDK resolve when a flow has reached finality, but certain methods also contain life-cycle events that dispatch throughout. We refer to this interface as a PromiEvent. There is prior art to inspire this approach in Ethereum's Web3 standard.

PromiEvent is a portmanteau of Promise and EventEmitter. Browser and React Native SDK methods return this object type, which is a native JavaScript Promise overloaded with EventEmitter methods. This value can be awaited in modern async/await code, or you may register event listeners to handle method-specific life-cycle hooks. Each PromiEvent contains the following default event types:

  • "done": Called when the Promise resolves. This is equivalent to Promise.then.
  • "error": Called if the Promise rejects. This is equivalent to Promise.catch.
  • "settled": Called when the Promise either resolves or rejects. This is equivalent to Promise.finally.

Look for additional event types documented near the method they relate to. Events are strongly-typed by TypeScript to offer developer hints and conveniant IDE auto-complete.

#Example

It's possible to chain Promise methods like .then and .catch with EventEmitter methods like .on and .once seamlessly. There are no limitations to either chaining interface, they all return an awaitable PromiEvent, as expected. The species of the object type is always a native JavaScript Promise.

Typescript
01const req = magic.auth.loginWithMagicLink({ email: 'hello@magic.link' });
02
03req
04  .on('email-sent', () => {
05    /* ... */
06  })
07  .then(DIDToken => {
08    /* ... */
09  })
10  .once('email-not-deliverable', () => {
11    /* ... */
12  })
13  .catch(error => {
14    /* ... */
15  })
16  .on('error', error => {
17    /* ... */
18  });

#Examples

#Re-authenticate Users

A user's Magic SDK session persists up to 7 days by default, so re-authentication is usually friction-less.

Note: the session length is customizable by the developer through the Magic dashboard.

0. Prerequisite: Install Magic Client SDK​

1. Re-authenticate the user:

Typescript
01import { Magic } from 'magic-sdk';
02const m = new Magic('API_KEY');
03
04const email = 'example@magic.link';
05
06if (await m.user.isLoggedIn()) {
07  const didToken = await m.user.getIdToken();
08
09  // Do something with the DID token.
10  // For instance, this could be a `fetch` call
11  // to a protected backend endpoint.
12  document.getElementById('your-access-token').innerHTML = didToken;
13} else {
14  // Log in the user
15  const user = await m.auth.loginWithMagicLink({ email });
16}

Did you find what you were looking for?