External UID
External UID
Prefer
metadatafor new integrations. Every write endpoint acceptsmetadata: Record<string, string>— a structured key/value bag echoed on the response and on every webhook event tied to the resource.metadatacarries richer reconciliation data (multiple keys, per-key meaning) and is the recommended way to link API resources to your internal ledger.external_uidstill works as a single-value shortcut. See Webhooks for how both fields surface on event payloads.
Every write endpoint accepts an optional external_uid field in the request body. The value is persisted on the underlying resource and included on every webhook event payload. The field exists so you can tag an API resource with an identifier from your own system — a campaign ID, an order number, a row PK — and link the two ledgers later without maintaining a separate mapping table.
The field
POST /v2/airdrops
Content-Type: application/json
Authorization: Bearer $PIRATE_API_KEY
Idempotency-Key: 5f3e2c8a-1d1d-4f9b-a2c2-9a1b2c3d4e5f
{
"payer": "WALLET_PUBKEY",
"platform_id": "pirate-crew",
"mint": "MINT_ADDRESS",
"airdrop_count": 0,
"merkle_root": "0xabc...",
"mode": "unsigned",
"external_uid": "campaign_2026_q2_airdrop_42"
}The response echoes the value verbatim on the wrapped envelope:
{
"data": {
"mode": "unsigned",
"transaction": "AQABAo...",
"external_uid": "campaign_2026_q2_airdrop_42"
},
"meta": { "request_id": "req_01HZ..." }
}Constraints:
| Rule | Value |
|---|---|
| Length | 1–128 chars |
| Character set | letters, digits, _, -, ., : |
| Uniqueness | not enforced — pass the same value to many endpoints if it makes sense |
| Storage | persisted on the resource; surfaced on webhook deliveries |
How it differs from Idempotency-Key
Idempotency-KeyThese two fields look similar but solve different problems. Use both.
Idempotency-Key | external_uid | |
|---|---|---|
| Header or body | Header | Body |
| Purpose | Make retries safe | Link the API resource to your ledger |
| Server uses it to | Cache + return the prior response on retry | Persist on the resource; echo on webhooks |
| Scope | Single logical HTTP operation | Long-lived business object (a campaign, an order, a pool launch) |
| Lifetime | TTL'd, cleared after the operation settles | As long as you want — your value, your meaning |
| Uniqueness | Must be fresh per operation | Reuse freely; the API doesn't enforce uniqueness |
The intuition: Idempotency-Key is per HTTP call, external_uid is per business object.
A worked example — running a quarterly airdrop campaign:
const externalUid = "campaign_2026_q2_airdrop_42"; // your ledger's campaign row PK
await fetch("/v2/merkle-trees", {
headers: { "Idempotency-Key": randomUUID() },
body: JSON.stringify({ ..., external_uid: externalUid }),
});
await fetch("/v2/airdrops", {
headers: { "Idempotency-Key": randomUUID() }, // new key — different operation
body: JSON.stringify({ ..., external_uid: externalUid }), // same uid — same campaign
});
// Months later, a webhook arrives:
// { type: "airdrop.claimed", data: { external_uid: "campaign_2026_q2_airdrop_42", wallet, amount } }
// → look up the campaign by external_uid, credit the user in your DB.Same external_uid across three separate API calls and an inbound webhook, all tied to one row in your campaigns table. Different Idempotency-Key on every call because each is a different operation.
When to use it
Use it on every write you'd later want to look up by your ID rather than the API's:
- Airdrop campaigns — your campaign row ↔ API tree/init/claim events.
- Fee splits — your invoice or partner-payout row ↔ on-chain split tx.
- Token launches — your project record ↔ pool address.
- NFT mints in a drop — your drop's row ↔ each mint instruction.
- Pool creates from your launchpad — your launchpad job ↔ on-chain pool.
Read endpoints (GET) don't accept it — there's no resource to tag.
Reconciliation pattern
A reconciliation job in your service typically looks like:
// Daily: pull every webhook delivery for the last 24h, group by external_uid,
// reconcile against your DB.
for (const event of webhooks.deliveriesSince(yesterday)) {
const row = await db.findCampaignByExternalUid(event.data.external_uid);
if (!row) continue; // not one of ours, skip
await db.recordEvent(row.id, event.type, event.data);
}This works even if the API rotates internal IDs, even if you replay deliveries, even if events arrive out of order — because the linking key is something you control rather than something the API generates.
Edge cases
- Omitted on a retry — the cached response from
Idempotency-Keyreturns whatever the original call sent. If the original call includedexternal_uidand the retry omits it, the response still has it (cached). Don't rely on this for safety — passexternal_uidconsistently across retries. - Different
external_uidon a retry with the sameIdempotency-Key— the API treats it as a409 conflict(body hash mismatch). See Idempotency. - Sensitive data — don't put PII or secrets here; it will appear in API responses, webhook payloads, and (eventually) the developer-portal request log.
Field reference per endpoint
Currently supported on every write endpoint:
| Endpoint | Group |
|---|---|
POST /v2/pools/{address}/fee-claims | Fees |
POST /v2/merkle-trees | Airdrops |
POST /v2/airdrops | Airdrops |
POST /v2/airdrops/{id}/claims | Airdrops |
POST /v2/airdrops/{id}/vault-withdrawals | Airdrops |
POST /v2/stakes · DELETE /v2/stakes/{asset} | Staking |
POST /v2/nfts · DELETE /v2/nfts/{asset} | NFTs |
POST /v2/gold-locks · DELETE /v2/gold-locks/{user} | Gold |
POST /v2/tokens · DELETE /v2/tokens/{mint}/authorities/{type} | Tokens |
POST /v2/transactions | Tx |
Not accepted on GET endpoints or on POST /v2/transaction-simulations (simulation has no resource to tag).