Skip to content

Stripe and Issuing

Many agent payments go through Stripe: PaymentIntents for online payments, and Issuing cards that an agent spends from. This guide records both. HalvorAI observes Stripe’s webhooks after the fact and does not touch the payment.

Stripe objects do not know which agent created them. So you do two things:

  1. Tag the object when you create it, so it carries the agent, owner and instruction.
  2. Forward each Stripe webhook event to the SDK, which maps it to an APE event.

Inside agent context, tagPaymentIntent writes metadata.halvor_* fields from the current context:

import Stripe from 'stripe';
import { withAgentContext } from '@halvorai/ingest';
import { tagPaymentIntent, tagIssuingCard } from '@halvorai/ingest/stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
await withAgentContext(
{
agent: { id: 'agt_procure_03' },
owner: { id: 'own_acme_ltd', type: 'business' },
instruction: { id: 'task_552', text: 'Reorder printer paper' },
},
async () => {
const intent = await stripe.paymentIntents.create(
tagPaymentIntent({ amount: 230000, currency: 'usd', description: 'Printer paper' }),
);
},
);

For Issuing, create one card per agent and tag the card. Payments made with it are attributed to that agent automatically:

await stripe.issuing.cards.create(
tagIssuingCard({ cardholder: cardholderId, currency: 'usd', type: 'virtual' }, { agentId: 'agt_travel_12' }),
);

The helpers add halvor_agent_id, halvor_owner_id, halvor_owner_type and halvor_instruction_id to the object’s metadata and leave your other metadata alone. Pass an overrides object as the second argument to set them explicitly.

In your existing webhook route, after your own signature check, hand the event to the SDK:

import { HalvorClient } from '@halvorai/ingest';
import { stripeWebhookHandler } from '@halvorai/ingest/stripe';
const halvor = new HalvorClient();
const forward = stripeWebhookHandler(halvor);
app.post('/stripe/webhook', async (req, res) => {
const event = stripe.webhooks.constructEvent(req.rawBody, req.headers['stripe-signature']!, endpointSecret);
forward(event); // maps and queues; events it does not handle are ignored
res.sendStatus(200);
});

forward returns the new event id, or nothing for an event type it ignores. It never throws, so it cannot break your webhook handler. You can also call halvor.stripe(event) directly.

Use the Stripe event as it arrives. Retries from Stripe carry the same event id, so a redelivered webhook produces a duplicate, not a second record.

Stripe eventAPE event
payment_intent.amount_capturable_updatedpayment.authorized
payment_intent.succeededpayment.settled
payment_intent.payment_failedpayment.failed, with failure_reason from last_payment_error.code
charge.refundedpayment.refunded
charge.dispute.createdpayment.disputed
issuing_authorization.created, issuing_authorization.updatedpayment.authorized on the card rail
issuing_transaction.createdpayment.settled, or payment.refunded for a refund

The mapping reads:

  • The amount and upper-cased currency, as minor units.
  • The PaymentIntent id as payment.payment_id.
  • description as payment.description.
  • The Connect destination as payee.id.
  • For Issuing, the merchant name, category code, country and network id as the payee.
  • A Shared Payment Token, when used, as instrument.type: stripe_spt.

The Stripe object itself is attached as stripe.object evidence. Card numbers are redacted before sending.

If a PaymentIntent has no halvor_agent_id, the event is still recorded, under the agent unattributed. It lowers your attribution coverage but is never lost. Fix it by tagging at creation.

  1. Trigger a test payment in Stripe’s test mode, or run stripe trigger payment_intent.succeeded.
  2. Open Payments in the sandbox dashboard. The event appears with its agent and payee.
  3. For Issuing, make a test authorization and look for payment.authorized followed by payment.settled.