Add VayaPin to my app. VayaPin returns a customer's confirmed postal address, so
I never ask them to type one.

**My app:** <one line — what it is, the language and framework, and where an address is needed>

Everything you need is in this prompt. Do not guess any endpoint, field or header
that is not written here. If something you need is missing, say so and ask me.

Plan first. List the files you will add or change and what each one does, then
wait for my yes before you write code.

## How it works

1. I ask VayaPin for a customer's address.
2. VayaPin answers with a `request_id`. The address is NOT in that answer.
3. The customer confirms their address, which can take minutes or days.
4. VayaPin sends the address to my webhook endpoint.

So every record needs a waiting state from the moment I create the request.

## The two secrets, and where I get them

Both come from the VayaPin dashboard at https://dashboard.vayapin.com/vayacore.
Neither has an API, so I do this part by hand. Tell me to do it before we test
anything against the real API.

**API token** — the `API Keys & BYOK` tab. Press `Generate Key`, name it, and
copy the value. It is shown once. It looks like `vapi_<uuid>.<long hex>`. My
account needs an active API subscription or every call fails with `429` and the
message `No active API subscription`.

**Signing secret** — the `Webhooks` tab at
https://dashboard.vayapin.com/vayacore/webhooks. Register the public HTTPS URL
of my webhook endpoint, and copy the signing secret it gives back. Also shown
once. Until an enabled endpoint exists, `POST /v1/requests` fails with
`422 endpoint_not_provisioned`.

For local work my endpoint still has to be reachable from the internet, so tell
me to run a tunnel such as `ngrok http <port>` and register that URL instead.

A quick-tunnel URL dies when the tunnel process stops, and a registered webhook
then points at nothing. Deliveries fail silently, and after the fifth attempt
the address is gone for good. Use one only while I am actively testing, and tell
me to register a stable URL for anything that has to keep working.

Put both values in the environment, never in source.

## This API acts in the real world

`POST /v1/requests` emails a real person and creates a real pin. It is not a
read, and it cannot be undone. Never call it to test connectivity, to reproduce
an error, or to explore the API.

When I ask you to debug something, reproduce it without calling VayaPin: stub
the HTTP layer, or re-read the response you already logged. If you genuinely
need a live call, ask me first and tell me whose address it will reach.

Use the email address I gave you, for the one request I asked for. Do not reuse
it as a fixture for a second request.

## Asking for an address

`POST https://api.vayapin.com/v1/requests`
Header: `Authorization: Bearer <token>`

```json
{
  "data": {
    "name": "John Example",
    "email": "john@example.com",
    "country": "DK",
    "idempotency_key": "order-1042"
  }
}
```

`name`, `email` and `country` are required. `country` is the country I expect to
ship to. Optional: `phone`, `locale` (language of the email to the customer),
`platform`, `first_name`, `last_name`, `address_line_1`, `address_line_2`,
`zip_code`, `city`, `state`.

`idempotency_key` must be my own record id. It goes in the body — an
`Idempotency-Key` HTTP header is ignored. Keys do not expire, and a replay does
not email the customer twice.

A new request answers `201`:

```json
{ "data": { "request_id": "c6f8b1d9-7b0e-4d1f-8b2a-7f2b9e88d2b3",
            "status": "pending", "vayapin_id": "DK:LARSEN12" } }
```

A replay of the same key answers `200` with the same `request_id`.

`status` is `resolved` (the customer already had a VayaPin, the webhook fires at
once) or `pending` (they were emailed a link). Handle both the same way: the
address still arrives by webhook.

Store `request_id` as a **string**. It is a UUID, and it is the only link between
my record and the webhook.

## Receiving the address

VayaPin sends `POST` to my endpoint with these headers:

- `X-VayaPin-Signature: sha256=<hex>` — HMAC-SHA256 of the raw body
- `X-VayaPin-Event-Id` — same as `event_id` in the body

```json
{
  "event_id": "7f3c1a2e-5d64-4b8f-9a01-2c7e5f8d1b44",
  "event_type": "pin.address_updated",
  "occurred_at": "2026-10-15T09:21:44Z",
  "request_id": "c6f8b1d9-7b0e-4d1f-8b2a-7f2b9e88d2b3",
  "vayapin_id": "DK:LARSEN12",
  "scripts": {
    "latin":  { "label": "...", "display": "Norrebrogade 42, 2200 Kobenhavn N, Denmark" },
    "native": { "label": "...", "display": "Nørrebrogade 42, 2200 København N, Danmark" }
  },
  "gps": { "lat": 55.6951, "lng": 12.5521 },
  "qrcode": { "img_url": "https://..." },
  "contact": { "email": "...", "phone": "...", "name": "..." }
}
```

`pin.address_updated` is the only event VayaPin sends today, so do not filter on
`event_type`. `scripts.native` may be absent. `scripts.latin` is always there.

Print `display`. Prefer `scripts.native.display`, fall back to
`scripts.latin.display`. `latin` is always present. Either `display` can be
`null` when the address could not be geocoded — then keep the record waiting and
never print the word "null".

Every field inside `contact` is optional and can be `null`, so match records on
`request_id`, never on `contact`.

## Rules for the webhook handler

Check the signature over the **raw bytes** of the request, before anything parses
the body. Parsing and re-serializing reorders the keys and the signature stops
matching. In Express, mount `express.raw({ type: "application/json" })` before
`express.json()`. In FastAPI, use `await request.body()`. Compare in constant
time, and strip the `sha256=` prefix.

Answer `2xx` as soon as the event is stored, then do slower work. VayaPin times
out after 10 seconds.

Answer `401` only for a bad signature. Answer `2xx` for everything else,
including a `request_id` I do not recognise, and log it. A `4xx` tells VayaPin my
endpoint is broken and it stops retrying for good.

A `5xx` or a timeout is retried, up to 5 attempts with a widening gap between
them, so answer `5xx` when something on my side is genuinely broken and I want
the event again. After the fifth failure the address is unrecoverable — there is
no endpoint to read it back.

Deduplicate on `event_id`, not on `request_id`:

- same `event_id` → a retry, ignore it
- new `event_id` for a known `request_id` → the customer changed their address,
  so replace the address I hold

An express-path webhook can arrive before `POST /v1/requests` returns to me, so
store `request_id` before doing anything slow.

Make an arrival visible without reading a log file. Print one line to stdout
naming the record and the address when an event lands. An address can arrive
days later, and nothing announces it: if the only evidence is a log file, a
working integration looks broken.

## Errors

Every error looks like this:

```json
{ "error": { "code": "unauthorized", "message": "...", "request_id": "..." } }
```

That `request_id` is a log id for support tickets. It is NOT the address
request's `request_id`.

A `422` adds a second object beside `error`, and this is the only place the field
name appears — `error.message` is always the generic "Invalid input":

```json
{ "error": { "code": "invalid_input", "message": "Invalid input" },
  "data": { "errors": { "email": ["is invalid"] } } }
```

| Status | `code` | What to show |
| --- | --- | --- |
| 401 / 403 | `unauthorized`, `forbidden` | My problem. Alert me, and tell the user the service is unavailable. |
| 422 | `invalid_input` | Name the field, from `data.errors`. |
| 422 | `endpoint_not_provisioned` | My problem. No webhook endpoint is registered yet. |
| 429 | `usage_limit_exceeded` | Quota. Wait for the `Retry-After` header, in seconds. |
| 429 | no active subscription | My problem. Retrying never fixes it. |

`Retry-After` can be an hour, which is longer than a web request can wait. Queue
the call and retry in a background worker, or let the user try again later. The
`idempotency_key` makes that retry safe.

A failure to reach VayaPin at all — DNS, connection refused, TLS, a timeout — is
none of the codes above, because no response came back. Catch it separately and
answer with a distinct error of my own. An unhandled exception here becomes an
opaque `500` and makes my network look like a VayaPin fault.

Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and
`X-RateLimit-Reset`. Read the limits from the headers rather than hardcoding a
number: they depend on the account's plan.

## Hold to these

- The API token and the signing secret come from the environment. Never in
  source, never in a commit, never in the client.
- Do not repeat a token or a signing secret back to me in your reply, a commit
  message or a summary. Write it straight into the environment file. If you
  cannot, tell me where to copy it from instead of reading the value out.
- Tests must not call the real API. Sign a payload with a test secret and post it
  to my own handler. Include a test that signs one body and sends a
  re-serialized one, to prove the signature is checked over raw bytes.

## Finish with a hand-over

What you changed, the environment variables I need, how to run it locally, and
anything you had to assume.

List every call you made to `POST /v1/requests`, with the record id and the
email it reached, so I can tell which of my emails came from which request. Say
plainly if you made a call I did not ask for.

End by repeating the dashboard steps above as a checklist I can follow: the
token from https://dashboard.vayapin.com/vayacore, then the webhook URL and
signing secret from https://dashboard.vayapin.com/vayacore/webhooks, then which
environment variable each value goes into. Name the webhook path you built, so I
register the right URL.
