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
| Thing | Where it comes from |
|---|---|
| API token | Dashboard → VayaCore → API Keys, Generate Key. Shown once. |
| Webhook endpoint URL | A public HTTPS URL in your app, registered under Dashboard → VayaCore → Webhooks |
| Signing secret | The 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.
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.
- Ask for the address.
POST /v1/requestswith the recipient's email. - VayaPin emails the recipient a claim link.
- The recipient confirms.
- 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.status | Meaning |
|---|---|
resolved | The recipient already had a pin. The webhook fires immediately. |
pending | No 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": "..." } }
| Status | code | Meaning |
|---|---|---|
| 400 | bad_request | Malformed request |
| 401 | unauthorized | Missing or invalid token |
| 403 | forbidden | Token may not access this resource |
| 404 | not_found | No such pin |
| 422 | invalid_input | A field failed validation |
| 422 | endpoint_not_provisioned | The account has no enabled webhook endpoint |
| 429 | usage_limit_exceeded | Quota 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 happened | What the customer should read |
|---|---|
| 401 / 403 | "We can't reach our address service right now." Alert yourself — this is your configuration. |
| 422 | The 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 subscription | Alert 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
- Receiving webhooks — signature verification, retries, deduplication
- API reference — every endpoint and field