Skip to main content

Getting started

VayaPin turns a short id like DK:LARSEN12 into a confirmed postal address. Instead of asking a customer to type an address into your checkout, you ask VayaPin for it, and the recipient confirms it.

What you need​

ThingWhere it comes from
API tokenDashboard → VayaCore → API Keys, Generate Key. Shown once.
Webhook endpoint URLA public HTTPS URL in your app, registered under Dashboard → VayaCore → Webhooks
Signing secretThe dashboard shows it once, when you create the webhook

Your account needs an active API subscription. Without one, requests fail with 429 and the message No active API subscription.

Register your webhook endpoint first

POST /v1/requests fails with 422 endpoint_not_provisioned until your account has an enabled webhook endpoint. VayaPin delivers the address only to that endpoint, never in the response. Until one exists, there is nowhere to send it.

Register the endpoint in the dashboard. There is no API for it, so you cannot set up an account from code. The dashboard shows the signing secret once, when you create the webhook. Copy it then.

Base URL​

https://api.vayapin.com

All endpoints live under /v1.

Keep the base URL in configuration rather than hardcoding it.

Authentication​

Send your token as a bearer token on every request:

Authorization: Bearer YOUR_API_TOKEN

Keep the token in an environment variable. Never put it in client-side code and never commit it. The token authenticates your whole account.

A missing or invalid token returns 401:

{ "error": { "code": "unauthorized", "message": "Invalid authentication token", "request_id": "..." } }

401 responses do not count against your quota.

The main flow​

Most integrations do one thing: get a recipient's address without asking the recipient to type it.

  1. Ask for the address. POST /v1/requests with the recipient's email.
  2. VayaPin emails the recipient a claim link.
  3. The recipient confirms.
  4. VayaPin calls your webhook with pin.address_updated.

The address is not in the response to step 1. It arrives in step 4. Your integration needs a webhook endpoint — see Receiving webhooks.

Request an address​

curl -X POST https://api.vayapin.com/v1/requests \
-H "Authorization: Bearer $VAYAPIN_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": {
"name": "John Example",
"email": "john@example.com",
"country": "DK",
"locale": "da-DK",
"idempotency_key": "order-1042"
}
}'

name, email and country are required. email is both how VayaPin matches an existing pin and how the claim link reaches the recipient. Phone-only requests are not supported.

A new request returns 201, and data.status tells you which path it took:

data.statusMeaning
resolvedThe recipient already had a pin. The webhook fires immediately.
pendingNo pin matched. VayaPin emails the recipient a claim link, and the webhook fires once they confirm.

A pending request waits for a person, so the webhook can arrive minutes or days later. Show that state in your own interface. There is no API to resend the claim email or to ask how far the recipient is.

Either way the address arrives by webhook, so your code handles both the same way.

country is the country you expect to ship to. VayaPin uses it to format the address. Ask your customer for it, or take it from the order. Do not guess it in code.

Pass an idempotency_key tied to something stable in your system, such as an order id. Retrying with the same key returns 200 and the original request rather than creating a second one, and does not email the recipient again. Keys are unique per account and do not expire.

Keep the request_id from the response. The webhook carries it back, and that is how you match an incoming address to an order.

request_id is a UUID string, not a number. Store it as a string: a webhook whose request_id does not match any order of yours is silently ignored, and nothing reports the mismatch.

Receive the address​

VayaPin sends a signed POST to your endpoint:

{
"event_type": "pin.address_updated",
"request_id": "c6f8b1d9-7b0e-4d1f-8b2a-7f2b9e88d2b3",
"vayapin_id": "DK:LARSEN12",
"scripts": { "latin": { "display": "Norrebrogade 42, 2200 Kobenhavn N, Denmark" } },
"gps": { "lat": 55.6951, "lng": 12.5521 }
}

Always verify the signature before trusting the body. Receiving webhooks covers verification, retries, and deduplication.

Looking up a pin directly​

If a customer gives you their VayaPin id, read it directly:

curl https://api.vayapin.com/v1/pins/DK:LARSEN12 \
-H "Authorization: Bearer $VAYAPIN_API_TOKEN"

This works only for pins your token can read. Other pins return 403 or 404.

Response shape​

Success responses wrap the result in data:

{ "data": { } }

Errors wrap it in error:

{ "error": { "code": "validation_error", "message": "...", "request_id": "..." } }
StatuscodeMeaning
400bad_requestMalformed request
401unauthorizedMissing or invalid token
403forbiddenToken may not access this resource
404not_foundNo such pin
422invalid_inputA field failed validation
422endpoint_not_provisionedThe account has no enabled webhook endpoint
429usage_limit_exceededQuota exhausted

request_id here identifies the API call in VayaPin's logs. Log it, and quote it in support tickets. It is not the request_id of an address request — that one is returned by POST /v1/requests and carried by the webhook.

Validation errors carry a second object​

A 422 adds a data.errors object beside error, keyed by field name:

{
"error": { "code": "invalid_input", "message": "Invalid input" },
"data": {
"errors": {
"email": ["is invalid"],
"country": ["can't be blank", "is the wrong length (should be 2 characters)"]
}
}
}

error.message is always the generic "Invalid input". Read data.errors to learn which field failed, or your users see an error that names nothing. This envelope has no request_id.

Showing errors to people​

Do not show raw API messages to a customer. Map them:

What happenedWhat the customer should read
401 / 403"We can't reach our address service right now." Alert yourself — this is your configuration.
422The specific field problem, e.g. "That email address doesn't look right."
429 usage_limit_exceeded"We're a bit busy — try again shortly." Retry after Retry-After.
429, no active subscriptionAlert yourself. Retrying never fixes it. Somebody must fix the billing.
5xx"Something went wrong on our side." Retry.

A customer can act on a bad email address. They can do nothing about your expired token.

Usage limits​

There are two quotas, monthly and daily. Your limits depend on your plan — read them from the response headers rather than hardcoding them. Every authenticated request counts, including 4xx. Requests that fail with 401 do not count, and 5xx responses are refunded.

Every response carries your current status:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 487
X-RateLimit-Reset: 1704067200
RateLimit-Policy: 10000;w=2592000, 1000;w=86400

The numbers above are illustrative. RateLimit-Policy states both windows as limit;w=seconds — monthly first, then daily (86400).

X-RateLimit-Reset is a Unix timestamp. RateLimit-Reset is seconds from now. The primary headers always report the tighter of the two quotas.

When you run out, you get 429 with a Retry-After header in seconds:

{
"error": {
"code": "usage_limit_exceeded",
"message": "API quota exceeded. Monthly: 10000/10000, Daily: 412/1000",
"retry_after": 3600
}
}

Wait for Retry-After rather than retrying immediately. Watch X-RateLimit-Remaining and slow down before you hit zero.

Retry-After can be an hour, which is longer than a web request can wait. Queue the call and retry it in a background worker, or show your user a message and let them try again later. Your idempotency_key makes the retry safe.

Check your quota any time without spending much of it:

curl https://api.vayapin.com/v1/usage/alerts \
-H "Authorization: Bearer $VAYAPIN_API_TOKEN"

Next steps​