Get started in 15 minutes

From your first request to your first webhook. All against the sandbox, without touching anything real.

0. What you need

An hp_test_… credential, provided by the agency that manages your account. It points to a test subaccount with fictitious data.

export HAPEE_KEY="hp_test_..."
export HAPEE_URL="https://beta.hapee.ai/api/v1"

1. Confirm the credential works · 1 min

curl -s "$HAPEE_URL/me" -H "Authorization: Bearer $HAPEE_KEY"
{
  "key": { "id": 44, "name": "Mi ERP sandbox", "mode": "test",
           "read_only": false, "scoped_to_subaccount": true },
  "subaccount": { "id": 91, "name": "Cliente — Sandbox",
                  "timezone": "America/Santiago", "default_currency": "CLP",
                  "sandbox": true, "language": "es" },
  "limits": { "requests_per_minute": 100, "batch_max_items": 100,
              "events_retention_days": 30, "idempotency_window_hours": 24 },
  "api_version": "v1"
}

Check that sandbox says true. If it says false, stop here: that credential points to real data.

2. Create a contact · 2 min

curl -s -X POST "$HAPEE_URL/contacts/upsert" \
  -H "Authorization: Bearer $HAPEE_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: my-first-test-001" \
  -d '{
    "external_id": "ERP-DEMO-1",
    "email": "prueba@ejemplo.cl",
    "first_name": "Ana", "last_name": "Pérez",
    "phone": "+56912345678",
    "rut": "12.345.678-5"
  }'
{ "data": { "id": 90211, "email": "prueba@ejemplo.cl", "…": "…" },
  "created": true }

external_id is the id from your system. It is what makes the load repeatable without duplicating: the second time it updates instead of creating.

3. Test that idempotency works · 2 min

Run the same command again, without changing anything. You will get the same response and the Idempotent-Replay: true header. No second contact was created.

Now run it a third time, changing first_name but keeping the same Idempotency-Key:

{ "error": {
    "code": "idempotency_key_reusada",
    "message": "Esta Idempotency-Key ya se usó con un cuerpo distinto..."
} }

That is correct: the same key with a different body means you reused it for something else. One key per operation.

4. Register a webhook · 5 min

You need a public URL. To test on your machine, start a tunnel:

ngrok http 3000     # gives you something like https://a1b2c3.ngrok-free.app
curl -s -X POST "$HAPEE_URL/webhooks/endpoints" \
  -H "Authorization: Bearer $HAPEE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://a1b2c3.ngrok-free.app/hooks",
    "description": "Local tests",
    "events": ["*"]
  }'

Save the secret now. It is only shown in full once. Afterwards you will only see whsec_…aB3d.

Minimal receiver

const express = require('express');
const crypto = require('crypto');
const app = express();

const SECRETO = 'whsec_aB3dE...';

app.post('/hooks', express.raw({ type: 'application/json' }), (req, res) => {
  const crudo = req.body.toString('utf8');
  const cab = req.get('X-Hapee-Signature') || '';
  const { t, v1 } = Object.fromEntries(
    cab.split(',').map(p => p.split('=').map(s => s.trim())));

  const esperado = crypto.createHmac('sha256', SECRETO)
                         .update(`${t}.${crudo}`).digest('hex');
  if (esperado !== v1) return res.sendStatus(401);

  console.log('event:', JSON.parse(crudo).type);
  res.sendStatus(200);
});

app.listen(3000);

In production you need a constant-time comparison and a freshness window. The full version is in Webhooks, with examples in Node, Python, PHP and C#.

5. Trigger the first event · 1 min

curl -s -X POST "$HAPEE_URL/webhooks/endpoints/12/ping" \
  -H "Authorization: Bearer $HAPEE_KEY"

In your console: evento: webhook.test

And now a real one — update the contact from step 2:

curl -s -X POST "$HAPEE_URL/contacts/upsert" \
  -H "Authorization: Bearer $HAPEE_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: my-first-test-002" \
  -d '{"external_id": "ERP-DEMO-1", "match_by": "external_id",
       "first_name": "Ana María"}'

In your console: evento: contact.updated

Events fire for everything that happens in the subaccount, not just what you do. If someone on the sales team moves a deal to “won” from the app, you still get it.

6. If you can't expose a URL · 2 min

The same thing, but polling yourself:

curl -s "$HAPEE_URL/events?limit=10" -H "Authorization: Bearer $HAPEE_KEY"

Save the next_cursor and pass it next time. Same events, same body.

7. See what happened to a delivery

curl -s "$HAPEE_URL/webhooks/deliveries?endpoint_id=12" \
  -H "Authorization: Bearer $HAPEE_KEY"

Each delivery shows its status, how many attempts it has made, what your endpoint responded and when the next attempt is.

Done. What's next?

Before going to production, the checklist is at the end of the integration guide. Going live means changing the credential and the webhook URL: nothing else.