Gando

Gando

Developer Portal

gando.app
All recipes

Create a deposit

Create a deposit on behalf of a linked rental operator, send the tenant to Gando to secure it, and track the outcome. This recipe covers the full partner integration path from API call to an active deposit.

Time: ~15 minutes
API: POST /api/partner/v1/deposits

Integration path — recommended order for a new partner integration:
1. Connect2. Webhooks3. Deposits (this recipe)

The @gando/partner npm package is forthcoming (Speakeasy). Examples below reflect the intended SDK API.


Prerequisites

  1. Install the SDK

    npm install @gando/partner
  2. Partner API key — issued by Gando, prefix gando_pk_test_… for staging.

  3. Staging environment — point requests at https://stagingv2.gando.app (production: https://gando.app).

  4. Linked rental operator — you need a GANDO_ACCOUNT_ID for an account actively linked to your partner. Complete Recipe 01 — Link a rental operator first if none exist. List linked accounts:

import { Gando } from '@gando/partner';

const gando = new Gando({
  apiKey: process.env.GANDO_API_KEY!,
  serverURL: process.env.GANDO_BASE_URL ?? 'https://stagingv2.gando.app',
});

// Print each linked rental operator account id
for (const acct of (await gando.accounts.list()).object?.data ?? []) {
  console.log(acct.accountId);
}
  1. Runnable examples repo (for the full script at the end): gando-partner-js-examples (forthcoming).
  2. Webhooks configured (recommended) — Recipe 02 — Receive webhooks so you track deposit status via deposit.activated instead of polling.

Flow

Loading diagram…

Step 1 (optional) — Initialize the client

If you have not already, instantiate the API client once and reuse it:

import { Gando } from '@gando/partner';

const gando = new Gando({
  apiKey: process.env.GANDO_API_KEY!,
  serverURL: 'https://stagingv2.gando.app',
});

The SDK sends your key as x-api-key on every request.


Step 2 — Create the deposit

Build the request body. Amounts are in euros (not cents). 800.0 = €800.00.

const response = await gando.deposits.create({
  // Rental operator account — must be linked to your partner account (403 otherwise)
  accountId,

  // Deposit amount in EUR. Min €70. Default max €2,500 per operator (negotiated ceiling)
  amount: 800.0,

  // Your booking / contract reference for reconciliation in your PMS
  rentalContract: 'CTR-2026-042',

  // Rental period start (ISO 8601 UTC)
  contractStartAt: '2026-04-01T00:00:00.000Z',

  // Rental period end — must be on or after contractStartAt
  contractEndAt: '2026-04-10T23:59:59.000Z',

  // Optional: client id from POST /api/partner/v1/clients on the same account
  clientId: undefined,

  // When true, response includes depositUrl for immediate tenant redirect
  depositUrlGeneration: true,

  // HTTPS URL Gando redirects to after checkout (required with depositUrlGeneration)
  returnUrl: 'https://partner.example/checkout/complete',
});

const deposit = response.object?.data;

Response (201):

FieldDescription
idDeposit id (dep_…) — store this on your booking
referenceHuman-readable reference (GAN-…)
statusStarts as pending
depositUrlTenant checkout URL (only when depositUrlGeneration: true)

Tip: Pass an optional Idempotency-Key header (UUID v4) on create to safely retry network failures without duplicate deposits. Same key + same body replays the cached response for 24 hours.


Step 3 — Send the link to the customer

Two delivery options. Pick one per booking flow.

Option A — You deliver the link

Redirect the tenant in your app, or send the link yourself (email, SMS, QR):

// depositUrl is only present when depositUrlGeneration was true
if (deposit?.depositUrl) {
  res.writeHead(302, { Location: deposit.depositUrl });
  res.end();
}

Option B — Gando sends the email

If you already have the tenant's email and prefer Gando to deliver the link:

await gando.deposits.sendDepositMail({
  id: deposit!.id!,
  email: 'tenant@example.com',
});

For multiple recipients, use gando.deposits.sendEmails() with a recipients list.


Step 3 bis — Inline redirect in your booking funnel

For combined booking + deposit checkout, create the deposit before the final confirmation step and redirect immediately:

  1. Set depositUrlGeneration: true and a returnUrl on your booking completion page.

  2. After create, redirect the browser to depositUrl.

  3. When the tenant finishes on Gando, they land on returnUrl with query params:

    ParamValues
    depositIddep_…
    depositStatussecured, declined, or abandoned

    Example return URL:

    https://partner.example/checkout/complete?depositId=dep_abc123&depositStatus=secured
    
  4. Do not trust the query params alone — they reflect the tenant-facing outcome. Always confirm the authoritative API status via polling or webhooks (Step 4).

Handle the return URL in your route handler:

const url = new URL(req.url!, `http://${req.headers.host}`);
const depositId = url.searchParams.get('depositId');
const outcome = url.searchParams.get('depositStatus');

switch (outcome) {
  case 'secured':
    // confirm booking, show success
    break;
  case 'declined':
    // release hold, notify ops
    break;
  case 'abandoned':
    // release hold, offer retry
    break;
  default:
    // verify with GET deposit
    break;
}

Step 4 — Track the deposit status

The deposit moves through statuses as the tenant completes checkout. You need a reliable way to know when it reaches active.

Option A — Polling (deposits.retrieve)

Suitable for prototypes or low volume. Poll until a terminal or active state is reached.

import type { Gando } from '@gando/partner';

const TERMINAL_STATUSES = ['active', 'cancelled', 'payment_issue', 'close'] as const;

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function waitForDeposit(
  gando: Gando,
  depositId: string,
  maxAttempts = 30,
): Promise<{ status: string } | null> {
  const intervals = [2_000, 3_000, 5_000, 5_000, 10_000]; // milliseconds between attempts

  for (let i = 0; i < maxAttempts; i++) {
    const deposit = (await gando.deposits.retrieve({ id: depositId })).object?.data;
    const status = deposit?.status;

    if (status && (TERMINAL_STATUSES as readonly string[]).includes(status)) {
      return { status };
    }

    await sleep(intervals[Math.min(i, intervals.length - 1)]!);
  }

  return null; // timeout — check again later or rely on webhooks
}

Recommended intervals:

PhaseIntervalRationale
First 30 severy 2–3 sTenant is likely still on Gando checkout
30 s – 2 minevery 5 sScoring + 3DS can take time
After 2 minevery 10–30 sBack off; switch to webhooks for production

Stop polling once status is active, cancelled, payment_issue, or close.

Option B — Webhooks (recommended)

Register an HTTPS endpoint and subscribe to deposit events. Gando pushes status changes in real time — no polling loops, no missed transitions.

Subscribe to at minimum:

  • deposit.status_changed — wildcard for every transition
  • deposit.activated — deposit became active

See Recipe 02 — Receive webhooks for endpoint setup, HMAC verification, and event handling.


Step 5 — Handle the active deposit

When status is active, the deposit is secured. The tenant's funds are not blocked; Gando holds a collection guarantee for the rental operator.

What to do:

  1. Mark the booking as deposit-secured in your PMS — link deposit.id to your booking record.
  2. Store reference (GAN-…) for support and reconciliation.
  3. Respect the securing window — coverage lasts until expiresAt on the deposit (derived from contractEndAt, default max 60 days from activation). After expiry the deposit moves to close.
  4. On damage during the rental — the rental operator triggers a capture (encaissement) via dashboard or POST /api/partner/v1/deposits/{id}/capture. That is outside this recipe.

Status reference:

StatusMeaning
pendingCreated, awaiting tenant checkout
incompleteTenant started but did not finish
activeSecured — deposit covers the lease
cancelledVoided before or during checkout
payment_issueFee payment failed
closeNatural end of contract / expiry
capturedAmount collected on claim

Common errors

All errors return the same envelope: { "error": { "code", "message", "requestId" } }.

400 — Bad request

CauseFix
Missing or invalid field (accountId, amount, dates)Validate body against the schema; all required fields must be present
amount below €70 or above operator maxUse 70–2500 EUR (default ceiling); check error message for resolved limit
contractEndAt before contractStartAtEnsure end ≥ start
returnUrl uses http:// (non-localhost)Use HTTPS, or http://localhost for local dev
Invalid clientIdCreate the client first via POST /api/partner/v1/clients on the same accountId

401 — Authentication failed

error.codeCauseFix
missing_api_keyNo x-api-key or Authorization headerPass gando_pk_… in every request
invalid_api_keyWrong or unknown keyCopy the key from Gando dashboard; use test key on staging
api_key_revokedKey was revokedGenerate a new partner API key

403 — Forbidden

error.codeCauseFix
account_not_linkedaccountId is not linked to your partnerComplete partner connect for that rental operator, or pick a linked account from accounts.list()
account_revokedLink was revokedRe-link the rental operator via connect
deposit_access_deniedDeposit belongs to another partnerUse the correct accountId and partner key

Log requestId from every error when contacting Gando support.


Full code

Runnable end-to-end script — see gando-partner-js-examples (forthcoming).

git clone https://github.com/Gando-Solutions/gando-partner-js-examples.git
cd gando-partner-js-examples
npm install
cp .env.example .env
# Set GANDO_API_KEY, GANDO_ACCOUNT_ID
npx tsx examples/03-create-deposit.ts
/**
 * Create a deposit for a linked rental operator (Partner API).
 *
 * Usage: npx tsx examples/03-create-deposit.ts
 */

import { Gando } from '@gando/partner';

const accountId = process.env.GANDO_ACCOUNT_ID;
if (!accountId) {
  console.error('Set GANDO_ACCOUNT_ID');
  process.exit(1);
}

const gando = new Gando({
  apiKey: process.env.GANDO_API_KEY!,
  serverURL: process.env.GANDO_BASE_URL ?? 'https://stagingv2.gando.app',
});

const year = new Date().getFullYear();
const contractStartAt = new Date().toISOString();
const contractEndAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();

try {
  const response = await gando.deposits.create({
    accountId,
    amount: 800.0,
    rentalContract: `CTR-${year}-${Math.floor(Math.random() * 900) + 100}`,
    contractStartAt,
    contractEndAt,
    clientId: undefined,
    depositUrlGeneration: true,
    returnUrl: 'https://partner.example/checkout/complete',
  });

  const deposit = response.object?.data;

  console.log('Deposit created');
  console.log(`  id:          ${deposit?.id}`);
  console.log(`  reference:   ${deposit?.reference}`);
  console.log(`  status:      ${deposit?.status}`);
  if (deposit?.depositUrl) {
    console.log(`  deposit_url: ${deposit.depositUrl}`);
  }
} catch (err) {
  console.error('API error:', err);
  process.exit(1);
}

Next steps

You have completed the core integration path (Connect → Webhooks → Deposits).

Optional enhancements:

  • Create a client via POST /api/partner/v1/clients — pre-fill tenant info before creating the deposit
  • Partner API reference — capture, cancel, list deposits, and more

Earlier in the path (if you skipped a step):