Skip to main content
You own the auth experience. Build your own credential collection and verification flow with full control over the look, feel, and UX, and Deck handles authentication and navigation behind it. This guide walks through the complete integration, from collecting credentials to handling MFA to managing connection lifecycle, with code examples you can adapt to your stack.

How it works

Credentials go directly from the user to your server over HTTPS. Your server opens the connection with Deck. Deck handles everything from there.

Connection states

A connection moves through a defined set of states. Your UI should map to each one.
StatusWhat to show
connectingLoading indicator. “Connecting to {source name}…”
interaction_requiredMFA or verification form built from the interaction.fields array
connectedSuccess confirmation. The connection is ready for task runs.
disconnectedError message with option to retry
terminatedConnection was intentionally ended

Step-by-step implementation

1

Build a credential form

Start with a form that collects the credentials required by the source. Most sources use username_password, but Deck also supports google_sso and none for sources that don’t require credentials.
function ConnectAccount({ sourceId }: { sourceId: string }) {
  const [status, setStatus] = useState<'idle' | 'connecting' | 'mfa' | 'connected' | 'error'>('idle')
  const [connection, setConnection] = useState(null)

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    setStatus('connecting')

    const form = new FormData(e.currentTarget)
    const res = await fetch('/api/connections', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        source_id: sourceId,
        email: form.get('email'),
        password: form.get('password'),
      }),
    })

    const conn = await res.json()
    setConnection(conn)
  }

  if (status === 'connecting') return <Connecting sourceName="Hilton" />
  if (status === 'mfa') return <MfaPrompt connection={connection} onComplete={() => setStatus('connecting')} />
  if (status === 'connected') return <Connected connection={connection} />

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required />

      <label htmlFor="password">Password</label>
      <input id="password" name="password" type="password" required />

      <button type="submit">Connect Account</button>
    </form>
  )
}
2

Open the connection server-side

Your server receives the credentials and opens a connection with Deck. This can be an Express handler, a Next.js API route, or any server-side endpoint.
app.post('/api/connections', async (req, res) => {
  const { source_id, email, password } = req.body

  const connection = await fetch('https://api.deck.co/v2/connections', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.DECK_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      source_id,
      auth_method: 'username_password',
      auth_credentials: { username: email, password },
      external_id: req.user.id,
    }),
  })

  const data = await connection.json()
  res.json(data)
})
The response comes back immediately with status: "connecting". Deck is now authenticating in the background.
3

Listen for events

Deck sends events to your event destination as the connection progresses. Use them to relay state changes to your frontend.
app.post('/webhooks/deck', async (req, res) => {
  const event = req.body

  switch (event.type) {
    case 'connection.connected':
      // Auth succeeded, notify your frontend
      await notifyClient(event.data.external_id, {
        status: 'connected',
        connection_id: event.data.id,
      })
      break

    case 'connection.interaction_required':
      // MFA or verification needed, send interaction details to frontend
      await notifyClient(event.data.external_id, {
        status: 'mfa',
        connection_id: event.data.id,
        interaction: event.data.interaction,
      })
      break

    case 'connection.disconnected':
      await notifyClient(event.data.external_id, {
        status: 'error',
        message: 'Connection lost. Please try again.',
      })
      break
  }

  res.sendStatus(200)
})
Use WebSockets, server-sent events, or polling to push state changes to your client. The external_id on the connection maps back to the user in your system.
4

Handle MFA and interactions

When Deck encounters MFA, security questions, or other verification prompts, it pauses and sends an interaction_required event. The event payload tells you exactly what to ask the user.The interaction object contains:
FieldDescription
typeThe kind of challenge: mfa, security_question, account_selection
messageA human-readable prompt from the source, e.g. “Enter the 6-digit code sent to your phone”
fieldsAn array of inputs to collect, each with name, type, and label
Build your form dynamically from the fields array so it adapts to whatever the source requires:
function MfaPrompt({ connection, onComplete }) {
  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    const form = new FormData(e.currentTarget)

    const body: Record<string, string> = {}
    for (const field of connection.interaction.fields) {
      body[field.name] = form.get(field.name) as string
    }

    await fetch(`/api/connections/${connection.id}/interaction`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    })

    onComplete()
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>{connection.interaction.message}</p>

      {connection.interaction.fields.map((field) => (
        <div key={field.name}>
          <label htmlFor={field.name}>{field.label}</label>
          <input
            id={field.name}
            name={field.name}
            type={field.type === 'string' ? 'text' : field.type}
            required
          />
        </div>
      ))}

      <button type="submit">Verify</button>
    </form>
  )
}
Your server forwards the response to Deck:
app.post('/api/connections/:id/interaction', async (req, res) => {
  const response = await fetch(
    `https://api.deck.co/v2/connections/${req.params.id}/interaction`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.DECK_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ inputs: req.body }),
    }
  )

  res.json(await response.json())
})
Deck resumes where it left off. If authentication succeeds, Deck sends a connection.connected event. If the source requires another round of verification, you get another interaction_required event. Your UI handles it the same way.
5

Show success

Once the connection is established, show a success state and move the user forward in your product.
function Connected({ connection }) {
  return (
    <div>
      <h3>Account connected</h3>
      <p>You're all set. Your account has been linked successfully.</p>
    </div>
  )
}

Handling errors

Build your UI to handle common failure cases gracefully:
ScenarioWhat happenedRecommended UX
Invalid credentialsSource rejected the username/passwordShow an error on the form. Let the user retry.
MFA timeoutUser took too long to enter a codeExplain the timeout. Offer to restart the connection.
Source unavailableThe target website is down or blockingShow a temporary error. Suggest trying again later.
Connection disconnectedSession expired between task runsPrompt the user to reconnect. Deck will re-authenticate using stored credentials if available.

UX recommendations

  • Show the source name and logo in your credential form so users know where they’re connecting
  • Use the interaction.message directly as your form label. It comes from the source and is specific to the prompt.
  • Add a timeout indicator during MFA so users know they have limited time to respond
  • Store the connection_id and external_id mapping so you can look up connections by your own user IDs
  • Consider a “Connected accounts” settings page where users can see active connections, disconnect, and reconnect